diff --git a/src/sage/algebras/catalog.py b/src/sage/algebras/catalog.py
index ec48a6debed..afa5db12e8e 100644
--- a/src/sage/algebras/catalog.py
+++ b/src/sage/algebras/catalog.py
@@ -59,6 +59,8 @@
`
- :class:`algebras.QuantumMatrixCoordinate
`
+- :class:`algebras.QuantumOscillator
+ `
- :class:`algebras.QSym `
- :class:`algebras.Partition `
- :class:`algebras.PlanarPartition `
@@ -128,6 +130,7 @@
lazy_import('sage.combinat.ncsf_qsym.qsym', 'QuasiSymmetricFunctions', 'QSym')
lazy_import('sage.combinat.grossman_larson_algebras', 'GrossmanLarsonAlgebra', 'GrossmanLarson')
lazy_import('sage.algebras.quantum_clifford', 'QuantumCliffordAlgebra', 'QuantumClifford')
+lazy_import('sage.algebras.quantum_oscillator', 'QuantumOscillatorAlgebra', 'QuantumOscillator')
lazy_import('sage.algebras.quantum_matrix_coordinate_algebra',
'QuantumMatrixCoordinateAlgebra', 'QuantumMatrixCoordinate')
lazy_import('sage.algebras.quantum_matrix_coordinate_algebra', 'QuantumGL')
diff --git a/src/sage/algebras/commutative_dga.py b/src/sage/algebras/commutative_dga.py
index c537c3e64a8..13565314140 100644
--- a/src/sage/algebras/commutative_dga.py
+++ b/src/sage/algebras/commutative_dga.py
@@ -1581,6 +1581,83 @@ def dict(self):
"""
return self.lift().dict()
+ def __call__(self, *values, **kwargs):
+ r"""
+ Evaluate the reduced expression of this element at ``x``, where ``x``
+ is either the tuple of values to evaluate in, a dictionary indicating
+ to which value is each generator evaluated, or keywords giving
+ the value to which generators should be evaluated.
+
+ INPUT:
+
+ - ``values`` -- (optional) either the values in which the variables
+ will be evaluated or a dictionary
+
+ OUTPUT:
+
+ this element evaluated at the given values
+
+ EXAMPLES::
+
+ sage: A. = GradedCommutativeAlgebra(QQ, degrees=(1, 2, 2, 3))
+ sage: f = x*y - 5*y*z + 7*x*y^2*z^3*t
+ sage: f(3, y, x^2, x*z)
+ 3*y
+ sage: f(x=3)
+ 21*y^2*z^3*t - 5*y*z + 3*y
+ sage: f({x:3, z:x^2})
+ 3*y
+
+ If the wrong number of values is provided, it results in an error::
+
+ sage: f(3, 5, y)
+ Traceback (most recent call last):
+ ...
+ ValueError: number of arguments does not match number of variables in parent
+
+ It is also possible to use keywords like this::
+
+ sage: A. = GradedCommutativeAlgebra(QQ, degrees=(1, 2, 2, 3))
+ sage: f = x*y - 5*y*z + 7*x*y^2*z^3*t
+ sage: f(x=3)
+ 21*y^2*z^3*t - 5*y*z + 3*y
+ sage: f(t=x,y=z)
+ -5*z^2 + x*z
+
+ If both a dictionary and keywords are used, only the dictionary is
+ considered::
+
+ sage: A. = GradedCommutativeAlgebra(QQ, degrees=(1, 2, 2, 3))
+ sage: f = x*y - 5*y*z + 7*x*y^2*z^3*t
+ sage: f({x:1}, t=x,y=z)
+ 7*y^2*z^3*t - 5*y*z + y
+ """
+ gens = self.parent().gens()
+ images = list(gens)
+ if values and not isinstance(values[0], dict):
+ for (i, p) in enumerate(values):
+ images[i] = p
+ if len(values) == 1 and isinstance(values[0], dict):
+ images = list(gens)
+ for (i, g) in enumerate(gens):
+ if g in values[0]:
+ images[i] = values[0][g]
+ elif len(values) == len(gens):
+ images = list(values)
+ elif values:
+ raise ValueError("number of arguments does not match number of variables in parent")
+ else:
+ images = list(gens)
+ for (i, g) in enumerate(gens):
+ gstr = str(g)
+ if gstr in kwargs:
+ images[i] = kwargs[gstr]
+ res = 0
+ for (m, c) in self.dict().items():
+ term = prod((gen**y for (y, gen) in zip(m, images)), c)
+ res += term
+ return res
+
def basis_coefficients(self, total=False):
"""
Return the coefficients of this homogeneous element with
diff --git a/src/sage/algebras/lie_algebras/center_uea.py b/src/sage/algebras/lie_algebras/center_uea.py
new file mode 100644
index 00000000000..61f5c6e05d7
--- /dev/null
+++ b/src/sage/algebras/lie_algebras/center_uea.py
@@ -0,0 +1,765 @@
+"""
+Center of a Universal Enveloping Algebra
+
+AUTHORS:
+
+- Travis Scrimshaw (2024-01-02): Initial version
+"""
+
+#*****************************************************************************
+# Copyright (C) 2024 Travis Scrimshaw
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+# http://www.gnu.org/licenses/
+#*****************************************************************************
+
+#from sage.structure.unique_representation import UniqueRepresentation
+#from sage.structure.parent import Parent
+from sage.combinat.free_module import CombinatorialFreeModule
+from sage.combinat.integer_lists.invlex import IntegerListsLex
+from sage.matrix.constructor import matrix
+from sage.monoids.indexed_free_monoid import IndexedFreeAbelianMonoid #, IndexedFreeAbelianMonoidElement
+from sage.monoids.indexed_free_monoid import IndexedMonoid
+from sage.combinat.root_system.coxeter_group import CoxeterGroup
+from sage.combinat.integer_vector_weighted import iterator_fast as intvecwt_iterator
+from sage.sets.non_negative_integers import NonNegativeIntegers
+from sage.sets.finite_enumerated_set import FiniteEnumeratedSet
+from sage.sets.family import Family
+from sage.rings.integer_ring import ZZ
+from sage.categories.kac_moody_algebras import KacMoodyAlgebras
+from sage.categories.finite_dimensional_lie_algebras_with_basis import FiniteDimensionalLieAlgebrasWithBasis
+from sage.categories.graded_algebras_with_basis import GradedAlgebrasWithBasis
+from sage.categories.fields import Fields
+from sage.categories.monoids import Monoids
+from sage.categories.enumerated_sets import EnumeratedSets
+from sage.misc.cachefunc import cached_method
+from sage.misc.lazy_attribute import lazy_attribute
+from sage.data_structures.blas_dict import iaxpy
+from collections import deque
+
+
+class CenterIndices(IndexedFreeAbelianMonoid):
+ r"""
+ Set of basis indices for the center of a universal enveloping algebra.
+
+ This also constructs the lift from the center to the universal enveloping
+ algebra as part of computing the generators and basis elements. The
+ basic algorithm is to construct the centralizer of each filtered
+ component in increasing order (as each is a finite dimensional vector
+ space). For more precise details, see [Motsak2006]_.
+ """
+ @staticmethod
+ def __classcall__(cls, center):
+ r"""
+ Normalize input to ensure a unique representation.
+
+ EXAMPLES::
+
+ sage: from sage.algebras.lie_algebras.center_uea import CenterIndices
+ sage: g = lie_algebras.pwitt(GF(3), 3)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: CenterIndices(Z) is CenterIndices(Z)
+ True
+ """
+ return super(IndexedMonoid, cls).__classcall__(cls, center)
+
+ def __init__(self, center, indices=None):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(5), 5)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: TestSuite(I).run(max_runs=7)
+ """
+ if indices is None:
+ indices = NonNegativeIntegers()
+ category = Monoids() & EnumeratedSets().Infinite()
+ IndexedFreeAbelianMonoid.__init__(self, indices, prefix='Z', category=category)
+
+ self._center = center
+ self._envelop_alg = self._center._envelop_alg
+ self._g = self._envelop_alg._g
+ # The _lift_map will be a dict with the keys being the degree and the values
+ # given as dicts with the keys being the leading support. This will be
+ # used to do the corresponding reductions.
+ self._lift_map = {0: {self._envelop_alg.one_basis(): self._envelop_alg.one()}}
+ self._cur_deg = 0
+ self._cur_vecs = deque() # we do a lot of deletions in the middle
+ # The _cur_basis is a mapping from the leading supports to a monoid element of self
+ self._cur_basis = {self._envelop_alg.one_basis(): self.one()}
+ self._cur_basis_inv = {self.one(): self._envelop_alg.one_basis()}
+ self._gen_degrees = {}
+ self._cur_num_gens = 0
+
+ def _repr_(self):
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(5), 5)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: Z.indices()
+ Basis indices of Center of Universal enveloping algebra of
+ The 5-Witt Lie algebra over Finite Field of size 5 in the Poincare-Birkhoff-Witt basis
+ """
+ return "Basis indices of {}".format(self._center)
+
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(5), 5)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: latex(I)
+ B\left( Z\left( PBW\left( \mathcal{W}(5)_{\Bold{F}_{5}} \right) \right) \right)
+ """
+ from sage.misc.latex import latex
+ return r"B\left( {} \right)".format(latex(self._center))
+
+ def lift_on_basis(self, m):
+ r"""
+ Return the image of the basis element indexed by ``m`` in the
+ universal enveloping algebra.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.Heisenberg(QQ, 3)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: z0 = I.monoid_generators()[0]
+ sage: I._lift_map
+ {0: {1: 1}}
+ sage: I.lift_on_basis(z0)
+ PBW['z']
+ sage: I._lift_map
+ {0: {1: 1}, 1: {PBW['z']: PBW['z']}}
+ sage: I.lift_on_basis(z0^3)
+ PBW['z']^3
+ sage: I._lift_map
+ {0: {1: 1}, 1: {PBW['z']: PBW['z']}}
+ sage: I._construct_next_degree()
+ sage: I._construct_next_degree()
+ sage: I._lift_map
+ {0: {1: 1},
+ 1: {PBW['z']: PBW['z']},
+ 2: {PBW['z']^2: PBW['z']^2},
+ 3: {PBW['z']^3: PBW['z']^3}}
+ sage: I.lift_on_basis(z0^3)
+ PBW['z']^3
+ """
+ while m not in self._cur_basis_inv:
+ supp = m.support()
+ # We might have not computed the correct degree, but we can lift the
+ # element if we have computed all of the corresponding generators.
+ if all(i in self._gen_degrees and self._gen_degrees[i] in self._lift_map
+ for i in supp):
+ ret = self._envelop_alg.one()
+ divisors = [mp for mp in self._cur_basis_inv if mp.divides(m) and not mp.is_one()]
+ while not m.is_one():
+ div = max(divisors, key=lambda elt: len(elt))
+ ls = self._cur_basis_inv[div]
+ deg = ls.length()
+ ret *= self._lift_map[deg][ls]
+ m = m // div
+ divisors = [mp for mp in divisors if mp.divides(m)]
+ return ret
+ self._construct_next_degree()
+ ls = self._cur_basis_inv[m]
+ deg = ls.length()
+ return self._lift_map[deg][ls]
+
+ def __iter__(self):
+ r"""
+ Iterate over ``self`` in degree increasing order.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(3), 6)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: it = iter(I)
+ sage: [next(it) for _ in range(10)]
+ [1, Z[0], Z[1], Z[2], Z[3], Z[4], Z[5], Z[6], Z[7], Z[0]^2]
+ """
+ yield self.one() # start with the identity
+ deg = 1
+ while True:
+ while deg not in self._lift_map:
+ self._construct_next_degree()
+ for ls in self._lift_map[deg]:
+ yield self._cur_basis[ls]
+ deg += 1
+
+ def some_elements(self):
+ r"""
+ Return some elements of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(3), 3)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: I.some_elements()
+ [1, Z[0], Z[1], Z[2], Z[0]*Z[1]*Z[2], Z[0]*Z[2]^4, Z[0]^4*Z[1]^3]
+ """
+ it = iter(self)
+ gens = [next(it) for _ in range(4)]
+ # We construct it as a set in case we introduce duplicates.
+ ret = set(gens)
+ ret.update([self.prod(gens), gens[1] * gens[3]**4, gens[1]**4 * gens[2]**3])
+ # Sort the output for uniqueness
+ ret = sorted(ret, key=lambda m: (self.degree(m), m.to_word_list()))
+ return ret
+
+ def degree(self, m):
+ r"""
+ Return the degre of ``m`` in ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['E', 6])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: [I.degree(g) for g in I.monoid_generators()]
+ [2, 5, 6, 8, 9, 12]
+ sage: [(elt, I.degree(elt)) for elt in I.some_elements()]
+ [(1, 0), (Z[0], 2), (Z[0]^2, 4), (Z[1], 5), (Z[0]^3*Z[1], 11),
+ (Z[0]^10, 20), (Z[0]*Z[1]^4, 22)]
+ """
+ return ZZ.sum(e * self._gen_degrees[i] for i, e in m._monomial.items())
+
+ def _construct_next_degree(self):
+ r"""
+ Construct the next elements of ``self`` for the next (uncomputed) degree.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.three_dimensional_by_rank(QQ, 2, 1)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: I._lift_map
+ {0: {1: 1}}
+ sage: I._construct_next_degree()
+ sage: I._construct_next_degree()
+ sage: I._construct_next_degree()
+ sage: I._construct_next_degree()
+ sage: I._lift_map
+ {0: {1: 1}, 1: {}, 2: {}, 3: {}, 4: {}}
+ """
+ UEA = self._envelop_alg
+ gens = UEA.algebra_generators()
+ monoid = UEA.basis().keys()
+ self._cur_deg += 1
+
+ # We first update the lift map with all possible products
+ # Note that we are using the fact that the elements are central
+ # so the product order doesn't matter.
+ # Since we always update this, it is sufficient to compute it
+ new_red = {}
+ for i in range(1, self._cur_deg//2+1):
+ for ls, lelt in self._lift_map[self._cur_deg-i].items():
+ for rs, relt in self._lift_map[i].items():
+ supp = ls * rs
+ new_red[supp] = lelt * relt
+ mon = self._cur_basis[ls] * self._cur_basis[rs]
+ self._cur_basis[supp] = mon
+ self._cur_basis_inv[mon] = supp
+ # TODO: Determine if we need to or benefit from another reduction of the new elements
+ self._lift_map[self._cur_deg] = new_red
+
+ # Determine the PBW elements of the current degree that are not reduced
+ # modulo the currently computed center.
+ for exps in IntegerListsLex(n=self._cur_deg, length=len(gens)):
+ elt = monoid.element_class(monoid, {k: p for k, p in zip(monoid._indices, exps) if p})
+ if elt in new_red: # already has a central element with this leading term
+ continue
+ # A new basis element to consider
+ self._cur_vecs.append(UEA.monomial(elt))
+
+ # Perform the centralization
+ R = UEA.base_ring()
+ vecs = list(self._cur_vecs)
+ for g in gens:
+ # TODO: We should hold onto previously computed values under the adjoint action
+ ad = [g * v - v * g for v in vecs]
+ # Compute the kernel
+ supp = set()
+ for v in ad:
+ supp.update(v._monomial_coefficients)
+ supp = sorted(supp, key=UEA._monomial_key, reverse=True)
+ if not supp: # no support for the image, so everything is in the kernel
+ continue
+ M = matrix(R, [[v[s] for v in ad] for s in supp])
+ ker = M.right_kernel_matrix()
+ vecs = [self._reduce(UEA.linear_combination((vecs[i], c) for i, c in kv.iteritems()))
+ for kv in ker.rows()]
+
+ # Lastly, update the appropriate data
+ if not vecs: # No new central elements, so nothing to do
+ return
+ new_gens = {}
+ for v in vecs:
+ v = self._reduce(v) # possibly not needed to check this
+ if not v:
+ continue
+ ls = v.trailing_support(key=UEA._monomial_key)
+ self._cur_vecs.remove(UEA.monomial(ls))
+ new_gens[ls] = self._reduce(v)
+ assert (self._cur_num_gens not in self._gen_degrees
+ or self._gen_degrees[self._cur_num_gens] == self._cur_deg)
+ self._gen_degrees[self._cur_num_gens] = self._cur_deg
+ mon = self.gen(self._cur_num_gens)
+ self._cur_basis[ls] = mon
+ self._cur_basis_inv[mon] = ls
+ self._cur_num_gens += 1
+ self._lift_map[self._cur_deg].update(new_gens)
+
+ def _reduce(self, vec):
+ r"""
+ Return the UEA vector ``vec`` by the currently computed center.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 1])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: z0 = I.gen(0)
+ sage: I._reduce(I.lift_on_basis(z0))
+ 0
+ sage: max(I._lift_map)
+ 2
+ sage: I._reduce(I.lift_on_basis(z0^2))
+ 4*PBW[alpha[1]]^2*PBW[-alpha[1]]^2
+ + 2*PBW[alpha[1]]*PBW[alphacheck[1]]^2*PBW[-alpha[1]]
+ + 1/4*PBW[alphacheck[1]]^4 - PBW[alphacheck[1]]^3
+ + PBW[alphacheck[1]]^2
+ sage: I._reduce(I.lift_on_basis(z0^2) - I.lift_on_basis(z0))
+ 4*PBW[alpha[1]]^2*PBW[-alpha[1]]^2
+ + 2*PBW[alpha[1]]*PBW[alphacheck[1]]^2*PBW[-alpha[1]]
+ + 1/4*PBW[alphacheck[1]]^4 - PBW[alphacheck[1]]^3
+ + PBW[alphacheck[1]]^2
+ sage: I._construct_next_degree()
+ sage: I._construct_next_degree()
+ sage: max(I._lift_map)
+ 4
+ sage: I._reduce(I.lift_on_basis(z0^2) - I.lift_on_basis(z0))
+ 0
+ """
+ # This is replicating what SubmoduleWithBasis does
+ ret = dict(vec._monomial_coefficients)
+ for data in self._lift_map.values():
+ for m, qv in data.items():
+ if m not in ret:
+ continue
+ iaxpy(-ret[m] / qv[m], qv._monomial_coefficients, ret)
+ return self._envelop_alg._from_dict(ret, remove_zeros=False)
+
+
+class SimpleLieCenterIndices(CenterIndices):
+ r"""
+ Set of basis indices for the center of a universal enveloping algebra of
+ a simple Lie algebra.
+
+ For more information, see
+ :class:`~sage.algebras.lie_algebras.center_uea.CenterIndices`.
+ """
+ def __init__(self, center):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['E', 6])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: TestSuite(I).run()
+ """
+ self._cartan_type = center._envelop_alg._g.cartan_type()
+ r = self._cartan_type.rank()
+ super().__init__(center, indices=FiniteEnumeratedSet(range(r)))
+ W = CoxeterGroup(self._cartan_type)
+ self._gen_degrees = dict(enumerate(W.degrees()))
+
+ def __iter__(self):
+ r"""
+ Iterate over ``self`` in degree increasing order.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['E', 6])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: it = iter(I)
+ sage: [next(it) for _ in range(10)]
+ [1, Z[0], Z[0]^2, Z[1], Z[2], Z[0]^3, Z[0]*Z[1], Z[3], Z[0]*Z[2], Z[0]^4]
+ """
+ deg = 0
+ n = len(self._gen_degrees)
+ wts = sorted(self._gen_degrees.values(), reverse=True)
+ while True:
+ total = 0
+ for exps in intvecwt_iterator(deg, wts):
+ yield self.element_class(self, {n-1-i: e for i, e in enumerate(exps) if e})
+ deg += 1
+
+
+class CenterUEA(CombinatorialFreeModule):
+ r"""
+ The center of a universal enveloping algebra.
+
+ .. TODO::
+
+ Generalize this to be the centralizer of any set of the UEA.
+
+ .. TODO::
+
+ For characteristic `p > 0`, implement the `p`-center of a simple
+ Lie algebra. See, e.g.,
+
+ - Theorem 5.12 of [Motsak2006]_
+ - http://www.math.kobe-u.ac.jp/icms2006/icms2006-video/slides/059.pdf
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 2])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: B = Z.basis()
+ sage: it = iter(B)
+ sage: center_elts = [next(it) for _ in range(6)]; center_elts
+ [1, Z[0], Z[1], Z[0]^2, Z[0]*Z[1], Z[1]^2]
+ sage: elts = [U(v) for v in center_elts] # long time
+ sage: all(v * g == g * v for g in U.algebra_generators() for v in elts) # long time
+ True
+
+ The Heisenberg Lie algebra `H_4` over a finite field; note the basis
+ elements `b^p \in Z(U(H_4))` for the basis elements `b \in H_4`::
+
+ sage: g = lie_algebras.Heisenberg(GF(3), 4)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: B = Z.basis()
+ sage: it = iter(B)
+ sage: center_elts = [next(it) for _ in range(12)]; center_elts
+ [1, Z[0], Z[0]^2, Z[0]^3, Z[1], Z[2], Z[3], Z[4], Z[5], Z[6], Z[7], Z[8]]
+ sage: elts = [U(v) for v in center_elts]; elts
+ [1, PBW['z'], PBW['z']^2, PBW['z']^3, PBW['p1']^3, PBW['p2']^3, PBW['p3']^3,
+ PBW['p4']^3, PBW['q1']^3, PBW['q2']^3, PBW['q3']^3, PBW['q4']^3]
+ sage: all(v * g == g * v for g in U.algebra_generators() for v in elts)
+ True
+
+ An example with a free 4-step nilpotent Lie algebras on 2 generators::
+
+ sage: L = LieAlgebra(QQ, 2, step=4); L
+ Free Nilpotent Lie algebra on 8 generators
+ (X_1, X_2, X_12, X_112, X_122, X_1112, X_1122, X_1222) over Rational Field
+ sage: U = L.pbw_basis()
+ sage: Z = U.center()
+ sage: it = iter(Z.basis())
+ sage: center_elts = [next(it) for _ in range(10)]; center_elts
+ [1, Z[0], Z[1], Z[2], Z[0]^2, Z[0]*Z[1], Z[0]*Z[2], Z[1]^2, Z[1]*Z[2], Z[2]^2]
+ sage: elts = [U(v) for v in center_elts]; elts
+ [1, PBW[(1, 1, 1, 2)], PBW[(1, 1, 2, 2)], PBW[(1, 2, 2, 2)], PBW[(1, 1, 1, 2)]^2,
+ PBW[(1, 1, 1, 2)]*PBW[(1, 1, 2, 2)], PBW[(1, 1, 1, 2)]*PBW[(1, 2, 2, 2)],
+ PBW[(1, 1, 2, 2)]^2, PBW[(1, 1, 2, 2)]*PBW[(1, 2, 2, 2)], PBW[(1, 2, 2, 2)]^2]
+ sage: all(v * g == g * v for g in U.algebra_generators() for v in elts)
+ True
+
+ Using the Engel Lie algebra::
+
+ sage: L. = LieAlgebra(QQ, {('X','Y'): {'Z': 1}}, nilpotent=True)
+ sage: U = L.pbw_basis()
+ sage: Z = U.center()
+ sage: it = iter(Z.basis())
+ sage: center_elts = [next(it) for _ in range(6)]; center_elts
+ [1, Z[0], Z[0]^2, Z[0]^3, Z[0]^4, Z[0]^5]
+ sage: elts = [U(v) for v in center_elts]; elts
+ [1, PBW['Z'], PBW['Z']^2, PBW['Z']^3, PBW['Z']^4, PBW['Z']^5]
+ sage: all(v * g == g * v for g in U.algebra_generators() for v in elts)
+ True
+ """
+ def __init__(self, g, UEA):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(ZZ['t'].fraction_field(), cartan_type=['D', 4])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: TestSuite(Z).run()
+
+ sage: g = lie_algebras.Heisenberg(GF(3), 4)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: TestSuite(Z).run()
+ """
+ if g not in FiniteDimensionalLieAlgebrasWithBasis:
+ raise NotImplementedError("only implemented for finite dimensional Lie algebras with a distinguished basis")
+
+ R = UEA.base_ring()
+ if R not in Fields():
+ raise NotImplementedError("only implemented for the base ring a field")
+
+ self._g = g
+ self._envelop_alg = UEA
+ if (self._g in KacMoodyAlgebras
+ and self._g.cartan_type().is_finite()
+ and R.characteristic() == 0):
+ indices = SimpleLieCenterIndices(self)
+ else:
+ indices = CenterIndices(self)
+ category = UEA.category()
+ base = category.base()
+ category = GradedAlgebrasWithBasis(base).Commutative() | category.Subobjects()
+ CombinatorialFreeModule.__init__(self, R, indices, category=category,
+ prefix='', bracket=False, latex_bracket=False,
+ sorting_key=self._sorting_key)
+ self.lift.register_as_coercion()
+
+ def _repr_(self):
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A',2])
+ sage: U = g.pbw_basis()
+ sage: U.center()
+ Center of Universal enveloping algebra of Lie algebra of ['A', 2]
+ in the Chevalley basis in the Poincare-Birkhoff-Witt basis
+ """
+ return "Center of " + repr(self._envelop_alg)
+
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(5), 5)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: latex(Z)
+ Z\left( PBW\left( \mathcal{W}(5)_{\Bold{F}_{5}} \right) \right)
+ """
+ from sage.misc.latex import latex
+ return r"Z\left( {} \right)".format(latex(self._envelop_alg))
+
+ def _sorting_key(self, m):
+ r"""
+ Return a key for ``m`` used in sorting elements of ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 2])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: z0, z1 = Z.algebra_generators()
+ sage: z0 * z1 # indirect doctest
+ Z[0]*Z[1]
+ sage: z1^2 + z0*z1 + z0^2 # indirect doctest
+ Z[0]^2 + Z[0]*Z[1] + Z[1]^2
+ sage: z1^3 + z0*z1^2 + z0^2*z1 + z0^3 # indirect doctest
+ Z[0]^3 + Z[0]^2*Z[1] + Z[0]*Z[1]^2 + Z[1]^3
+ """
+ return (-m.length(), m.to_word_list())
+
+ @cached_method
+ def algebra_generators(self):
+ r"""
+ Return the algebra generators of ``self``.
+
+ .. WARNING::
+
+ When the universal enveloping algebra is not known to have
+ a finite generating set, the generating set will be the basis
+ of ``self`` in a degree (weakly) increasing order indexed by
+ `\ZZ_{\geq 0}`. In particular, the `0`-th generator will be
+ the multiplicative identity `1`.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.Heisenberg(QQ, 3)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: Z.algebra_generators()[0]
+ 1
+ sage: Z.algebra_generators()[1]
+ Z[0]
+
+ sage: g = LieAlgebra(QQ, cartan_type=['G', 2])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: Z.algebra_generators()
+ Finite family {0: Z[0], 1: Z[1]}
+ """
+ mon_gens = self._indices.monoid_generators()
+ if mon_gens.cardinality() == float("inf"):
+ return Family(NonNegativeIntegers(), lambda m: self.monomial(self._indices.unrank(m)))
+ return Family({i: self.monomial(mon_gens[i]) for i in mon_gens.keys()})
+
+ @cached_method
+ def one_basis(self):
+ r"""
+ Return the basis index of `1` in ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ['t'].fraction_field(), cartan_type=['B', 5])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: ob = Z.one_basis(); ob
+ 1
+ sage: ob.parent() is Z.indices()
+ True
+ """
+ return self._indices.one()
+
+ def ambient(self):
+ r"""
+ Return the ambient algebra of ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(GF(5), cartan_type=['A', 2])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: Z.ambient() is U
+ True
+ """
+ return self._envelop_alg
+
+ def product_on_basis(self, left, right):
+ r"""
+ Return the product of basis elements indexed by ``left`` and ``right``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['E', 6])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: mg = Z.indices().monoid_generators()
+ sage: Z.product_on_basis(mg[1]*mg[2], mg[0]*mg[1]^3*mg[2]*mg[3]^3)
+ Z[0]*Z[1]^4*Z[2]^2*Z[3]^3
+ """
+ return self.monomial(left * right)
+
+ def degree_on_basis(self, m):
+ r"""
+ Return the degree of the basis element indexed by ``m`` in ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['E', 6])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: I = Z.indices()
+ sage: it = iter(I)
+ sage: supports = [next(it) for _ in range(10)]; supports
+ [1, Z[0], Z[0]^2, Z[1], Z[2], Z[0]^3, Z[0]*Z[1], Z[3], Z[0]*Z[2], Z[0]^4]
+ sage: [Z.degree_on_basis(m) for m in supports]
+ [0, 2, 4, 5, 6, 6, 7, 8, 8, 8]
+ """
+ return self._indices.degree(m)
+
+ @lazy_attribute
+ def lift(self):
+ r"""
+ The lift map from ``self`` to the universal enveloping algebra.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 1])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: gens = Z.algebra_generators()
+ sage: U(gens[0]^2 + gens[0])
+ 4*PBW[alpha[1]]^2*PBW[-alpha[1]]^2
+ + 2*PBW[alpha[1]]*PBW[alphacheck[1]]^2*PBW[-alpha[1]]
+ + 1/4*PBW[alphacheck[1]]^4 - PBW[alphacheck[1]]^3
+ - 2*PBW[alpha[1]]*PBW[-alpha[1]] + 1/2*PBW[alphacheck[1]]^2
+ + PBW[alphacheck[1]]
+ sage: U(-1/4*gens[0]) == U.casimir_element()
+ True
+ """
+ # This is correct if we are using key=self._envelop_alg._monomial_key,
+ # but we are currently unable to pass such an option.
+ return self.module_morphism(self._indices.lift_on_basis, codomain=self._envelop_alg, unitriangular='upper')
+
+ def retract(self, elt):
+ r"""
+ The retraction map to ``self`` from the universal enveloping algebra.
+
+ .. TODO::
+
+ Implement a version of this that checks if the leading term of
+ ``elt`` is divisible by a product of all of the currently known
+ generators in order to avoid constructing the full centralizer
+ of larger degrees than needed.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.Heisenberg(QQ, 3)
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: z0 = Z.algebra_generators()[1]; z0
+ Z[0]
+ sage: Z.retract(U(z0^2) - U(3*z0))
+ Z[0]^2 - 3*Z[0]
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 2])
+ sage: U = g.pbw_basis()
+ sage: Z = U.center()
+ sage: z0, z1 = Z.algebra_generators()
+ sage: Z.retract(U(z0*z0) - U(z1)) # long time
+ Z[0]^2 - Z[1]
+ sage: zc = Z.retract(U.casimir_element()); zc
+ -1/3*Z[0]
+ sage: U(zc) == U.casimir_element()
+ True
+ """
+ # This should work except it needs the monomials of the PBW basis to be
+ # compariable. However, this does not work for, e.g., Lie algebras
+ # in the Chevalley basis as ee are unable to pass a key for the
+ # module morphism. Additionally, the implementation below does more
+ # operations in-place than the module morphism.
+ #return self.lift.section()
+ UEA = self._envelop_alg
+ elt = UEA(elt)
+ # We manipulate the dictionary (in place) to avoid creating elements
+ data = elt.monomial_coefficients(copy=True)
+ indices = self._indices
+ ret = {}
+ while data:
+ lm = min(data, key=UEA._monomial_key)
+ while indices._cur_deg < UEA.degree_on_basis(lm):
+ indices._construct_next_degree()
+ ind = indices._cur_basis[lm]
+ other = indices.lift_on_basis(ind).monomial_coefficients(copy=False)
+ coeff = data[lm] / other[lm]
+ ret[ind] = coeff
+ iaxpy(-coeff, other, data)
+ return self.element_class(self, ret)
diff --git a/src/sage/algebras/lie_algebras/classical_lie_algebra.py b/src/sage/algebras/lie_algebras/classical_lie_algebra.py
index 4466cf6e46e..e7a1b9a8b33 100644
--- a/src/sage/algebras/lie_algebras/classical_lie_algebra.py
+++ b/src/sage/algebras/lie_algebras/classical_lie_algebra.py
@@ -1796,6 +1796,19 @@ def _repr_(self):
"""
return "Lie algebra of {} in the Chevalley basis".format(self._cartan_type)
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 2])
+ sage: latex(g)
+ \mathfrak{g}(A_{2})_{\Bold{Q}}
+ """
+ from sage.misc.latex import latex
+ return r"\mathfrak{{g}}({})_{{{}}}".format(latex(self._cartan_type), latex(self.base_ring()))
+
def _test_structure_coeffs(self, **options):
"""
Check the structure coefficients against the GAP implementation.
diff --git a/src/sage/algebras/lie_algebras/lie_algebra_element.pyx b/src/sage/algebras/lie_algebras/lie_algebra_element.pyx
index ca53753153b..9e46da295c4 100644
--- a/src/sage/algebras/lie_algebras/lie_algebra_element.pyx
+++ b/src/sage/algebras/lie_algebras/lie_algebra_element.pyx
@@ -156,10 +156,6 @@ cdef class LieAlgebraElement(IndexedFreeModuleElement):
PBW[-1] + PBW[0] - 3*PBW[1]
"""
UEA = self._parent.universal_enveloping_algebra()
- try:
- gen_dict = UEA.algebra_generators()
- except (TypeError, AttributeError):
- gen_dict = UEA.gens_dict()
s = UEA.zero()
if not self:
return s
@@ -167,9 +163,14 @@ cdef class LieAlgebraElement(IndexedFreeModuleElement):
# does not match the generators index set of the UEA.
if hasattr(self._parent, '_UEA_names_map'):
names_map = self._parent._UEA_names_map
+ gen_dict = UEA.gens_dict()
for t, c in self._monomial_coefficients.items():
s += c * gen_dict[names_map[t]]
else:
+ try:
+ gen_dict = UEA.algebra_generators()
+ except (TypeError, AttributeError):
+ gen_dict = UEA.gens_dict()
for t, c in self._monomial_coefficients.items():
s += c * gen_dict[t]
return s
diff --git a/src/sage/algebras/lie_algebras/poincare_birkhoff_witt.py b/src/sage/algebras/lie_algebras/poincare_birkhoff_witt.py
index 723a4e04179..3b59303d0dc 100644
--- a/src/sage/algebras/lie_algebras/poincare_birkhoff_witt.py
+++ b/src/sage/algebras/lie_algebras/poincare_birkhoff_witt.py
@@ -4,10 +4,11 @@
AUTHORS:
- Travis Scrimshaw (2013-11-03): Initial version
+- Travis Scrimshaw (2024-01-02): Adding the center
"""
#*****************************************************************************
-# Copyright (C) 2013-2017 Travis Scrimshaw
+# Copyright (C) 2013-2024 Travis Scrimshaw
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -96,6 +97,14 @@ class PoincareBirkhoffWittBasis(CombinatorialFreeModule):
PBW[2]*PBW[3] + PBW[5]
sage: G[-2] * G[3] * G[2]
PBW[-2]*PBW[2]*PBW[3] + PBW[-2]*PBW[5]
+
+ .. TODO::
+
+ When the Lie algebra is finite dimensional, set the ordering of the
+ basis elements, translate the structure coefficients, and work with
+ fixed-length lists as the exponent vectors. This way we only will
+ run any nontrivial sorting only once and avoid other potentially
+ expensive comparisons between keys.
"""
@staticmethod
def __classcall_private__(cls, g, basis_key=None, prefix='PBW', **kwds):
@@ -247,6 +256,20 @@ def _repr_(self):
"""
return "Universal enveloping algebra of {} in the Poincare-Birkhoff-Witt basis".format(self._g)
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(3), 6)
+ sage: U = g.pbw_basis()
+ sage: latex(U)
+ PBW\left( \mathcal{W}(6)_{\Bold{F}_{3}} \right)
+ """
+ from sage.misc.latex import latex
+ return r"PBW\left( {} \right)".format(latex(self._g))
+
def _coerce_map_from_(self, R):
"""
Return ``True`` if there is a coercion map from ``R`` to ``self``.
@@ -496,7 +519,7 @@ def degree_on_basis(self, m):
"""
return m.length()
- def casimir_element(self, order=2):
+ def casimir_element(self, order=2, *args, **kwds):
r"""
Return the Casimir element of ``self``.
@@ -534,7 +557,32 @@ def casimir_element(self, order=2):
from sage.rings.infinity import Infinity
if self._g.dimension() == Infinity:
raise ValueError("the Lie algebra must be finite dimensional")
- return self._g.casimir_element(order=order, UEA=self)
+ return self._g.casimir_element(order=order, UEA=self, *args, **kwds)
+
+ def center(self):
+ r"""
+ Return the center of ``self``.
+
+ .. SEEALSO::
+
+ :class:`~sage.algebras.lie_algebras.center_uea.CenterUEA`
+
+ EXAMPLES::
+
+ sage: g = LieAlgebra(QQ, cartan_type=['A', 2])
+ sage: U = g.pbw_basis()
+ sage: U.center()
+ Center of Universal enveloping algebra of Lie algebra of ['A', 2]
+ in the Chevalley basis in the Poincare-Birkhoff-Witt basis
+
+ sage: g = lie_algebras.Heisenberg(GF(3), 4)
+ sage: U = g.pbw_basis()
+ sage: U.center()
+ Center of Universal enveloping algebra of Heisenberg algebra of rank 4
+ over Finite Field of size 3 in the Poincare-Birkhoff-Witt basis
+ """
+ from sage.algebras.lie_algebras.center_uea import CenterUEA
+ return CenterUEA(self._g, self)
class Element(CombinatorialFreeModule.Element):
def _act_on_(self, x, self_on_left):
diff --git a/src/sage/algebras/lie_algebras/virasoro.py b/src/sage/algebras/lie_algebras/virasoro.py
index 297ccf184a8..aa11a9ed089 100644
--- a/src/sage/algebras/lie_algebras/virasoro.py
+++ b/src/sage/algebras/lie_algebras/virasoro.py
@@ -80,6 +80,19 @@ def _repr_(self):
"""
return "The Lie algebra of regular vector fields over {}".format(self.base_ring())
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.regular_vector_fields(QQ)
+ sage: latex(g)
+ \mathcal{W}_{\Bold{Q}}
+ """
+ from sage.misc.latex import latex
+ return r"\mathcal{{W}}_{{{}}}".format(latex(self.base_ring()))
+
# For compatibility with CombinatorialFreeModuleElement
_repr_term = IndexedGenerators._repr_generator
_latex_term = IndexedGenerators._latex_generator
@@ -217,6 +230,19 @@ def _repr_(self):
"""
return "The {}-Witt Lie algebra over {}".format(self._p, self.base_ring())
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.pwitt(GF(3), 15)
+ sage: latex(g)
+ \mathcal{W}(15)_{\Bold{F}_{3}}
+ """
+ from sage.misc.latex import latex
+ return r"\mathcal{{W}}({})_{{{}}}".format(latex(self._p), latex(self.base_ring()))
+
# For compatibility with CombinatorialFreeModuleElement
_repr_term = IndexedGenerators._repr_generator
_latex_term = IndexedGenerators._latex_generator
@@ -444,6 +470,19 @@ def _repr_(self):
"""
return "The Virasoro algebra over {}".format(self.base_ring())
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: g = lie_algebras.VirasoroAlgebra(QQ)
+ sage: latex(g)
+ \mathcal{V}_{\Bold{Q}}
+ """
+ from sage.misc.latex import latex
+ return r"\mathcal{{V}}_{{{}}}".format(latex(self.base_ring()))
+
@cached_method
def lie_algebra_generators(self):
"""
diff --git a/src/sage/algebras/quantum_oscillator.py b/src/sage/algebras/quantum_oscillator.py
new file mode 100644
index 00000000000..a688283705f
--- /dev/null
+++ b/src/sage/algebras/quantum_oscillator.py
@@ -0,0 +1,621 @@
+r"""
+Quantum Oscillator Algebras
+
+AUTHORS:
+
+- Travis Scrimshaw (2023-12): initial version
+"""
+
+#*****************************************************************************
+# Copyright (C) 2023 Travis Scrimshaw
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+# https://www.gnu.org/licenses/
+#*****************************************************************************
+
+from sage.misc.cachefunc import cached_method
+from sage.misc.misc_c import prod
+from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
+from sage.rings.integer_ring import ZZ
+from sage.categories.algebras import Algebras
+from sage.combinat.free_module import CombinatorialFreeModule
+from sage.categories.cartesian_product import cartesian_product
+from sage.sets.family import Family
+from sage.sets.non_negative_integers import NonNegativeIntegers
+
+
+class QuantumOscillatorAlgebra(CombinatorialFreeModule):
+ r"""
+ The quantum oscillator algebra.
+
+ Let `R` be a commutative algebra and `q \in R` be a unit.
+ The *quantum oscillator algebra*, or `q`-oscillator algebra,
+ is the unital associative `R`-algebra with generators `a^+`,
+ `a^-` and `k^{\pm 1}` satisfying the following relations:
+
+ .. MATH::
+
+ k a^{\pm} = q^{\pm 1} a^{\pm} k, \qquad
+ a^- a^+ = 1 - q^2 k^2, \qquad
+ a^+ a^- = 1 - k^2.
+
+ INPUT:
+
+ - ``q`` -- (optional) the parameter `q`
+ - ``R`` -- (default: `\QQ(q)`) the base ring that contains ``q``
+
+ EXAMPLES:
+
+ We construct the algebra and perform some basic computations::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: ap, am, k, ki = O.algebra_generators()
+ sage: q = O.q()
+ sage: k^-3 * ap * ki * am^2 * k - q^3 * ap * k^3
+ q^5*a-*k^-3 - q^3*a-*k^-1 - q^3*a+*k^3
+
+ We construct representations of the type `A_1` quantum coordinate ring
+ using the quantum oscillator algebra and verify the quantum determinant::
+
+ sage: pi = matrix([[am, k], [-q*k, ap]]); pi
+ [ a- k]
+ [-q*k a+]
+ sage: pi[0,0] * pi[1,1] - q * pi[0,1] * pi[1,0]
+ 1
+
+ Next, we use this to build representations for type `A_2`::
+
+ sage: def quantum_det(M):
+ ....: n = M.nrows()
+ ....: return sum((-q)**sigma.length()
+ ....: * prod(M[i,sigma[i]-1] for i in range(n))
+ ....: for sigma in Permutations(n))
+ sage: def build_repr(wd, gens):
+ ....: n = gens[0].nrows()
+ ....: ret = gens[wd[0]-1]
+ ....: for ind in wd[1:]:
+ ....: g = gens[ind-1]
+ ....: temp = [[None]*n for _ in range(n)]
+ ....: for i in range(n):
+ ....: for j in range(n):
+ ....: temp[i][j] = sum(tensor([ret[i,k], g[k,j]]) for k in range(n))
+ ....: ret = matrix(temp)
+ ....: return ret
+ sage: pi1 = matrix.block_diagonal(pi, matrix.identity(1)); pi1
+ [ a- k| 0]
+ [-q*k a+| 0]
+ [---------+----]
+ [ 0 0| 1]
+ sage: pi2 = matrix.block_diagonal(matrix.identity(1), pi); pi2
+ [ 1| 0 0]
+ [----+---------]
+ [ 0| a- k]
+ [ 0|-q*k a+]
+ sage: quantum_det(pi1) == 1
+ True
+ sage: quantum_det(pi2) == 1
+ True
+ sage: pi12 = build_repr([1,2], [pi1, pi2]); pi12
+ [ a- # 1 k # a- k # k]
+ [-q*k # 1 a+ # a- a+ # k]
+ [ 0 -q*1 # k 1 # a+]
+ sage: quantum_det(pi12)
+ 1 # 1
+ sage: pi121 = build_repr([1,2,1], [pi1, pi2]); pi121
+ [ a- # 1 # a- - q*k # a- # k a- # 1 # k + k # a- # a+ k # k # 1]
+ [-q*k # 1 # a- - q*a+ # a- # k -q*k # 1 # k + a+ # a- # a+ a+ # k # 1]
+ [ q^2*1 # k # k -q*1 # k # a+ 1 # a+ # 1]
+ sage: quantum_det(pi121)
+ 1 # 1 # 1
+ sage: pi212 = build_repr([2,1,2], [pi1, pi2]); pi212
+ [ 1 # a- # 1 1 # k # a- 1 # k # k]
+ [ -q*a- # k # 1 a- # a+ # a- - q*k # 1 # k a- # a+ # k + k # 1 # a+]
+ [ q^2*k # k # 1 -q*k # a+ # a- - q*a+ # 1 # k -q*k # a+ # k + a+ # 1 # a+]
+ sage: quantum_det(pi212)
+ 1 # 1 # 1
+
+ REFERENCES:
+
+ - [Kuniba2022]_ Section 3.2
+ """
+ @staticmethod
+ def __classcall_private__(cls, q=None, R=None):
+ r"""
+ Standardize input to ensure a unique representation.
+
+ TESTS::
+
+ sage: O1 = algebras.QuantumOscillator()
+ sage: q = PolynomialRing(ZZ, 'q').fraction_field().gen()
+ sage: O2 = algebras.QuantumOscillator(q=q)
+ sage: O3 = algebras.QuantumOscillator(q, q.parent())
+ sage: O1 is O2 and O2 is O3
+ True
+ """
+ if q is None:
+ q = PolynomialRing(ZZ, 'q').fraction_field().gen()
+ if R is None:
+ R = q.parent()
+ q = R(q)
+
+ return super().__classcall__(cls, q, R)
+
+ def __init__(self, q, R):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: TestSuite(O).run()
+ """
+ self._q = q
+ self._k_poly = PolynomialRing(R, 'k')
+ indices = cartesian_product([ZZ, ZZ])
+
+ cat = Algebras(R).WithBasis()
+ CombinatorialFreeModule.__init__(self, R, indices, category=cat)
+ self._assign_names(('ap', 'am', 'k', 'ki'))
+
+ def _repr_(self) -> str:
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: algebras.QuantumOscillator()
+ Quantum oscillator algebra with q=q over
+ Fraction Field of Univariate Polynomial Ring in q over Integer Ring
+ """
+ return "Quantum oscillator algebra with q={} over {}".format(
+ self._q, self.base_ring())
+
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: latex(O)
+ \operatorname{Osc}_{q}
+ """
+ return "\\operatorname{Osc}_{%s}" % self._q
+
+ def q(self):
+ r"""
+ Return the `q` of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.q()
+ q
+ sage: O = algebras.QuantumOscillator(q=QQ(-5))
+ sage: O.q()
+ -5
+ """
+ return self._q
+
+ @cached_method
+ def algebra_generators(self):
+ r"""
+ Return the algebra generators of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.algebra_generators()
+ Finite family {'am': a-, 'ap': a+, 'k': k, 'ki': k^-1}
+ """
+ d = {'ap': self.monomial((ZZ.one(), ZZ.zero())),
+ 'am': self.monomial((-ZZ.one(), ZZ.zero())),
+ 'k': self.monomial((ZZ.zero(), ZZ.one())),
+ 'ki': self.monomial((ZZ.zero(), -ZZ.one()))}
+ return Family(d)
+
+ @cached_method
+ def gens(self) -> tuple:
+ r"""
+ Return the generators of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.gens()
+ (a+, a-, k, k^-1)
+ """
+ return tuple(self.algebra_generators())
+
+ @cached_method
+ def one_basis(self) -> tuple:
+ r"""
+ Return the index of the basis element of `1`.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.one_basis()
+ (0, 0)
+ """
+ return (ZZ.zero(), ZZ.zero())
+
+ def some_elements(self) -> tuple:
+ r"""
+ Return some elements of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.some_elements()
+ (a+, a-, k, k^-1, 1, a+^3, a-^4, k^2, k^-5, a+*k,
+ a-^4*k^-3, 1 + 3*k + 2*a+ + a+*k)
+ """
+ ap, am, k, ki = self.gens()
+ return (ap, am, k, ki, self.one(),
+ ap**3, am**4, k**2, ki**5, ap*k, am**4*ki**3,
+ self.an_element())
+
+ def fock_space_representation(self):
+ r"""
+ Return the Fock space representation of ``self``.
+
+ .. SEEALSO::
+
+ :class:`~sage.algebras.quantum_oscillator.FockSpaceRepresentation`
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O.fock_space_representation()
+ Fock space representation of Quantum oscillator algebra with q=q
+ over Fraction Field of Univariate Polynomial Ring in q over Integer Ring
+ """
+ return FockSpaceRepresentation(self)
+
+ def _repr_term(self, m) -> str:
+ r"""
+ Return a string representation of the basis element indexed by ``m``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O._repr_term((1, 3))
+ 'a+*k^3'
+ sage: O._repr_term((-1, 1))
+ 'a-*k'
+ sage: O._repr_term((5, 0))
+ 'a+^5'
+ sage: O._repr_term((-4, -2))
+ 'a-^4*k^-2'
+ sage: O._repr_term((0, -4))
+ 'k^-4'
+ sage: O._repr_term((0, 0))
+ '1'
+
+ sage: O(5)
+ 5
+ """
+ a, k = m
+
+ astr = ''
+ if a == 1:
+ astr = 'a+'
+ elif a > 1:
+ astr = 'a+^{}'.format(a)
+ elif a == -1:
+ astr = 'a-'
+ elif a < -1:
+ astr = 'a-^{}'.format(-a)
+
+ kstr = ''
+ if k == 1:
+ kstr = 'k'
+ elif k != 0:
+ kstr = 'k^{}'.format(k)
+
+ if astr:
+ if kstr:
+ return astr + '*' + kstr
+ return astr
+ if kstr:
+ return kstr
+ return '1'
+
+ def _latex_term(self, m):
+ r"""
+ Return a latex representation for the basis element indexed by ``m``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: O._latex_term((1, 3))
+ 'a^+ k^{3}'
+ sage: O._latex_term((-1, 1))
+ 'a^- k'
+ sage: O._latex_term((5, 0))
+ '(a^+)^{5}'
+ sage: O._latex_term((-4, -2))
+ '(a^-)^{4} k^{-2}'
+ sage: O._latex_term((0, -4))
+ 'k^{-4}'
+ sage: O._latex_term((0, 0))
+ '1'
+
+ sage: latex(O(5))
+ 5
+ """
+ a, k = m
+
+ astr = ''
+ if a == 1:
+ astr = 'a^+'
+ elif a > 1:
+ astr = '(a^+)^{{{}}}'.format(a)
+ elif a == -1:
+ astr = 'a^-'
+ elif a < -1:
+ astr = '(a^-)^{{{}}}'.format(-a)
+
+ kstr = ''
+ if k == 1:
+ kstr = 'k'
+ elif k != 0:
+ kstr = 'k^{{{}}}'.format(k)
+
+ if astr:
+ if kstr:
+ return astr + ' ' + kstr
+ return astr
+ if kstr:
+ return kstr
+ return '1'
+
+ @cached_method
+ def product_on_basis(self, ml, mr):
+ r"""
+ Return the product of the basis elements indexed by ``ml`` and ``mr``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: ap, am, k, ki = O.algebra_generators()
+ sage: O.product_on_basis((-2, 3), (-4, 5))
+ 1/q^12*a-^6*k^8
+ sage: O.product_on_basis((2, 3), (4, -5))
+ q^12*a+^6*k^-2
+ sage: O.product_on_basis((2, 3), (0, -3))
+ a+^2
+ sage: k^5 * ki^10
+ k^-5
+ sage: k^10 * ki^5
+ k^5
+ sage: ap^3 * k^5
+ a+^3*k^5
+ sage: am^3 * k^5
+ a-^3*k^5
+ sage: k^5 * ap^3
+ q^15*a+^3*k^5
+ sage: k^5 * am^3
+ 1/q^15*a-^3*k^5
+ sage: ki^5 * ap^3
+ 1/q^15*a+^3*k^-5
+ sage: ki^5 * am^3
+ q^15*a-^3*k^-5
+ sage: ap * am
+ 1 - k^2
+ sage: am * ap
+ 1 - q^2*k^2
+
+ sage: (ap + am + k + ki)^2
+ a-^2 + (q+1)*a-*k^-1 + ((q+1)/q)*a-*k + k^-2 + 4 - q^2*k^2
+ + ((q+1)/q)*a+*k^-1 + (q+1)*a+*k + a+^2
+
+ sage: (ap)^3 * (am)^5
+ a-^2 + ((-q^4-q^2-1)/q^8)*a-^2*k^2 + ((q^4+q^2+1)/q^14)*a-^2*k^4 - 1/q^18*a-^2*k^6
+ sage: (ap)^5 * (am)^3
+ a+^2 + ((-q^4-q^2-1)/q^4)*a+^2*k^2 + ((q^4+q^2+1)/q^6)*a+^2*k^4 - 1/q^6*a+^2*k^6
+ sage: (am)^3 * (ap)^5
+ a+^2 + (-q^10-q^8-q^6)*a+^2*k^2 + (q^18+q^16+q^14)*a+^2*k^4 - q^24*a+^2*k^6
+ sage: (am)^5 * (ap)^3
+ a-^2 + (-q^6-q^4-q^2)*a-^2*k^2 + (q^10+q^8+q^6)*a-^2*k^4 - q^12*a-^2*k^6
+ """
+ q = self._q
+ k = self._k_poly.gen()
+ al, kl = ml
+ ar, kr = mr
+ coeff = q ** (kl * ar)
+ if (al <= 0 and ar <= 0) or (al >= 0 and ar >= 0):
+ return self.element_class(self, {(al + ar, kl + kr): coeff})
+ # now al and ar have different signs
+ if al < 0: # a^- * a^+ case
+ kp = self._k_poly.prod(1 - q**(2*(ar-i)) * k**2 for i in range(min(-al,ar)))
+ else: # a^+ * a^- case
+ kp = self._k_poly.prod(1 - q**(2*(ar+i)) * k**2 for i in range(1,min(al,-ar)+1))
+ a = al + ar
+ return self.element_class(self, {(a, kl+kr+i): c * coeff for i, c in enumerate(kp) if c})
+
+ class Element(CombinatorialFreeModule.Element):
+ def __invert__(self):
+ r"""
+ Return the inverse if ``self`` is a basis element.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: ap, am, k, ki = O.algebra_generators()
+ sage: k.inverse()
+ k^-1
+ sage: ~k^5
+ k^-5
+ sage: ~ki^2
+ k^2
+ sage: O.zero().inverse()
+ Traceback (most recent call last):
+ ...
+ ZeroDivisionError
+ sage: ~ap
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: only implemented for monomials in k
+ sage: ~(k + ki)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: only implemented for monomials in k
+ """
+ if not self:
+ raise ZeroDivisionError
+ if len(self) != 1 or self.leading_support()[0] != 0:
+ raise NotImplementedError("only implemented for monomials in k")
+
+ ((a, k), coeff), = list(self._monomial_coefficients.items())
+ O = self.parent()
+ return O.element_class(O, {(a, -k): coeff.inverse_of_unit()})
+
+
+class FockSpaceRepresentation(CombinatorialFreeModule):
+ r"""
+ The unique Fock space representation of the
+ :class:`~sage.algebras.quantum_oscillator.QuantumOscillatorAlgebra`.
+ """
+ def __init__(self, oscillator_algebra):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: F = O.fock_space_representation()
+ sage: TestSuite(F).run()
+ """
+ self._O = oscillator_algebra
+ ind = NonNegativeIntegers()
+ CombinatorialFreeModule.__init__(self, oscillator_algebra.base_ring(), ind, prefix='', bracket=['|', '>'],
+ latex_bracket=[r'\lvert', r'\rangle'])
+
+ def _test_representation(self, **options):
+ r"""
+ Test that ``self`` is a representation of the quantum
+ oscillator algebra.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator(q=GF(7)(3))
+ sage: F = O.fock_space_representation()
+ sage: F._test_representation()
+ """
+ tester = self._tester(**options)
+ S = self._O.some_elements()
+ num_trials = 0
+ from itertools import product
+ for a, b in product(S, repeat=2):
+ for elt in tester.some_elements():
+ num_trials += 1
+ if num_trials > tester._max_runs:
+ return
+ tester.assertEqual((a*b)*elt, a*(b*elt))
+
+ def _repr_(self) -> str:
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator(q=GF(5)(2))
+ sage: O.fock_space_representation()
+ Fock space representation of Quantum oscillator algebra
+ with q=2 over Finite Field of size 5
+ """
+ return "Fock space representation of {}".format(self._O)
+
+ def _latex_(self):
+ r"""
+ Return a latex representation of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: F = O.fock_space_representation()
+ sage: latex(F)
+ \mathfrak{F}_{q}
+ """
+ return r"\mathfrak{{F}}_{{{}}}".format(self._O._q)
+
+ def vacuum(self):
+ r"""
+ Return the vacuum element `|0\rangle` of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: F = O.fock_space_representation()
+ sage: F.vacuum()
+ |0>
+ """
+ return self.basis()[0]
+
+ def some_elements(self):
+ r"""
+ Return some elements of ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: F = O.fock_space_representation()
+ sage: F.some_elements()
+ (|0>, |1>, |52>, |0> + 2*|1> + 3*|2> + |42>)
+ """
+ B = self.basis()
+ return (B[0], B[1], B[52], self.an_element())
+
+ class Element(CombinatorialFreeModule.Element):
+ def _acted_upon_(self, scalar, self_on_left=True):
+ r"""
+ Return the action of ``scalar`` on ``self``.
+
+ EXAMPLES::
+
+ sage: O = algebras.QuantumOscillator()
+ sage: ap, am, k, ki = O.gens()
+ sage: F = O.fock_space_representation()
+ sage: B = F.basis()
+ sage: [ap * B[i] for i in range(3)]
+ [|1>, |2>, |3>]
+ sage: [am * B[i] for i in range(3)]
+ [0, (-q^2+1)*|0>, (-q^4+1)*|1>]
+ sage: [k * B[i] for i in range(3)]
+ [|0>, q*|1>, q^2*|2>]
+ sage: [ki * B[i] for i in range(3)]
+ [|0>, 1/q*|1>, 1/q^2*|2>]
+ sage: (am)^3 * B[5]
+ (-q^24+q^18+q^16+q^14-q^10-q^8-q^6+1)*|2>
+ sage: (7*k^3 + am) * (B[0] + B[1] + B[2])
+ (-q^2+8)*|0> + (-q^4+7*q^3+1)*|1> + 7*q^6*|2>
+ sage: 5 * (B[2] + B[3])
+ 5*|2> + 5*|3>
+ """
+ # Check for scalars first
+ ret = super()._acted_upon_(scalar, self_on_left)
+ if ret is not None:
+ return ret
+ P = self.parent()
+ if self_on_left or scalar not in P._O: # needs to be a left Osc-action
+ return None
+ scalar = P._O(scalar)
+ q = P._O._q
+
+ ret = []
+ for om, oc in scalar:
+ a, k = om
+ for fm, fc in self:
+ if fm < -a: # the result will be 0
+ continue
+ c = q ** (fm*k)
+ if a < 0:
+ c *= prod(1 - q**(2*(fm-i)) for i in range(-a))
+ if c:
+ ret.append((fm+a, oc * fc * c))
+ return P.sum_of_terms(ret)
diff --git a/src/sage/algebras/quatalg/quaternion_algebra.py b/src/sage/algebras/quatalg/quaternion_algebra.py
index 849a5bdd727..2c97fd70429 100644
--- a/src/sage/algebras/quatalg/quaternion_algebra.py
+++ b/src/sage/algebras/quatalg/quaternion_algebra.py
@@ -919,6 +919,29 @@ def invariants(self):
"""
return self._a, self._b
+ def is_definite(self):
+ """
+ Checks whether the quaternion algebra ``self`` is definite, i.e. whether it ramifies at the
+ unique Archimedean place of its base field QQ. This is the case if and only if both
+ invariants of ``self`` are negative.
+
+ EXAMPLES::
+
+ sage: QuaternionAlgebra(QQ,-5,-2).is_definite()
+ True
+ sage: QuaternionAlgebra(1).is_definite()
+ False
+
+ sage: QuaternionAlgebra(RR(2.),1).is_definite()
+ Traceback (most recent call last):
+ ...
+ ValueError: base field must be rational numbers
+ """
+ if not is_RationalField(self.base_ring()):
+ raise ValueError("base field must be rational numbers")
+ a, b = self.invariants()
+ return a < 0 and b < 0
+
def __eq__(self, other):
"""
Compare self and other.
@@ -2101,16 +2124,20 @@ def ternary_quadratic_form(self, include_basis=False):
else:
return Q
- def isomorphism_to(self, other, *, conjugator=False):
+ def isomorphism_to(self, other, *, conjugator=False, B=10):
r"""
Compute an isomorphism from this quaternion order `O`
to another order `O'` in the same quaternion algebra.
- If the optional keyword argument ``conjugator`` is set
- to ``True``, this method returns a single quaternion
- `\gamma \in O \cap O'` of minimal norm such that
- `O' = \gamma^{-1} O \gamma`,
- rather than the ring isomorphism it defines.
+ INPUT:
+
+ - ``conjugator`` -- bool (default: False), if True this
+ method returns a single quaternion `\gamma \in O \cap O'`
+ of minimal norm such that `O' = \gamma^{-1} O \gamma`,
+ rather than the ring isomorphism it defines.
+
+ - ``B`` -- postive integer, bound on theta series
+ coefficients to rule out non isomorphic orders.
.. NOTE::
@@ -2183,6 +2210,15 @@ def isomorphism_to(self, other, *, conjugator=False):
sage: {iso(g * h) == iso(g) * iso(h) for g in els for h in els}
{True}
+ Test edge case::
+
+ sage: Quat. = QuaternionAlgebra(419)
+ sage: O = Quat.quaternion_order([1/2 + 3/2*j + k, 1/18*i + 25/9*j + 5/6*k, 3*j + 2*k, 3*k])
+ sage: Oconj = j.inverse() * O * j
+ sage: Oconj = Quat.quaternion_order(Oconj.basis())
+ sage: O.isomorphism_to(Oconj, conjugator=True)
+ -j
+
Test error cases::
sage: Quat. = QuaternionAlgebra(-1,-11)
@@ -2235,36 +2271,85 @@ def isomorphism_to(self, other, *, conjugator=False):
...
ValueError: quaternion orders not isomorphic
+ ::
+
+ sage: Quat. = QuaternionAlgebra(-5, -17)
+ sage: O1 = Quat.quaternion_order([1, i, j, 1/2 + 1/2*i + 1/2*j + 1/2*k])
+ sage: O2 = Quat.quaternion_order([1/2 + 1/2*i + 1/6*j + 13/6*k, i, 1/3*j + 4/3*k, 3*k])
+ sage: O1.isomorphism_to(O2)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: isomorphism_to was not able to recognize the given orders as isomorphic
+
ALGORITHM:
Find a generator of the principal lattice `N\cdot O\cdot O'`
where `N = [O : O cap O']` using
:meth:`QuaternionFractionalIdeal_rational.minimal_element()`.
An isomorphism is given by conjugation by such an element.
- """
+ Works providing reduced norm of conjugation element is not
+ a ramified prime times a square. To cover cases where it is
+ we repeat the check for orders conjugated by i, j, and k.
+ """
+
+ # Method to find isomorphism, which might not work when O2 is
+ # O1 conjugated by an alpha such that nrd(alpha) is a
+ # ramified prime times a square
+ def attempt_isomorphism(self, other):
+ N = self.intersection(other).free_module().index_in(self.free_module())
+ I = N * self * other
+ gamma = I.minimal_element()
+ if self*gamma != I:
+ return False, None
+ if gamma*other != I:
+ return False, None
+ return True, gamma
+
if not isinstance(other, QuaternionOrder):
raise TypeError('not a quaternion order')
Q = self.quaternion_algebra()
if other.quaternion_algebra() != Q:
raise TypeError('not an order in the same quaternion algebra')
- if not self.quadratic_form().is_positive_definite():
+ if not is_RationalField(Q.base_ring()):
+ raise NotImplementedError('only implemented for orders in a rational quaternion algebra')
+ if not Q.is_definite():
raise NotImplementedError('only implemented for definite quaternion orders')
if not (self.discriminant() == Q.discriminant() == other.discriminant()):
raise NotImplementedError('only implemented for maximal orders')
- N = self.intersection(other).free_module().index_in(self.free_module())
- I = N * self * other
- gamma = I.minimal_element()
- if self*gamma != I:
+ # First try a theta series check, up to bound B
+ if self.unit_ideal().theta_series_vector(B) != other.unit_ideal().theta_series_vector(B):
raise ValueError('quaternion orders not isomorphic')
- assert gamma*other == I
- if conjugator:
- return gamma
+ # Want to iterate over elements alpha where the square-free part of nrd(alpha) divides prod(Q.ramified_primes()),
+ # and each time try attempt_isomorphism with the order conjugated by alpha.
+ # But in general finding all such elements alpha is hard,
+ # so we just try 1, i, j, k first.
+ for alpha in [1] + list(Q.gens()):
+ other_conj = other
+ if alpha != 1:
+ other_conj = Q.quaternion_order((alpha * other * alpha.inverse()).basis())
+ found, gamma = attempt_isomorphism(self, other_conj)
+ if found:
+ gamma = gamma * alpha
+ if conjugator:
+ return gamma
+ else:
+ ims = [~gamma * gen * gamma for gen in Q.gens()]
+ return self.hom(ims, other, check=False)
+
+ # We can tell if 1, i, j, k cover all the alpha we need to test,
+ # by checking if we have additional ramified primes which are not the square-free parts of nrd(i), nrd(j) or nrd(k)
+ a, b = -Q.invariants()[0], -Q.invariants()[1]
+ square_free_invariants = [a.squarefree_part(), b.squarefree_part(), (a*b).squarefree_part()]
+ is_result_guaranteed = len([a for a in Q.ramified_primes() if a not in square_free_invariants]) == 0
+
+ if is_result_guaranteed:
+ raise ValueError('quaternion orders not isomorphic')
- ims = [~gamma * gen * gamma for gen in Q.gens()]
- return self.hom(ims, other, check=False)
+ # Otherwise, there might be other unknown alpha's giving isomorphism. If so we can't find them.
+ raise NotImplementedError("isomorphism_to was not able to recognize the given orders as isomorphic")
class QuaternionFractionalIdeal(Ideal_fractional):
@@ -2316,6 +2401,8 @@ def __init__(self, Q, basis, left_order=None, right_order=None, check=True):
raise TypeError("basis must be a list or tuple")
basis = tuple([Q(v) for v in
(QQ**4).span([Q(v).coefficient_tuple() for v in basis], ZZ).basis()])
+ if len(basis) != 4:
+ raise ValueError("fractional ideal must have rank 4")
self.__left_order = left_order
self.__right_order = right_order
Ideal_fractional.__init__(self, Q, basis)
@@ -2667,6 +2754,39 @@ def basis_matrix(self):
C, d = B._clear_denom()
return C.hermite_form() / d
+ def reduced_basis(self):
+ r"""
+ Let `I` = ``self`` be a fractional ideal in a (rational) definite quaternion algebra.
+ This function returns an LLL reduced basis of I.
+
+ OUTPUT:
+
+ - A tuple of four elements in I forming an LLL reduced basis of I as a lattice
+
+ EXAMPLES::
+
+ sage: B = BrandtModule(2,37); I = B.right_ideals()[0]
+ sage: I
+ Fractional ideal (2 + 2*i + 2*j + 2*k, 4*i + 108*k, 4*j + 44*k, 148*k)
+ sage: I.reduced_basis()
+ (2 + 2*i + 2*j + 2*k, 4, -2 - 2*i - 14*j + 14*k, -16*i + 12*k)
+ sage: l = I.reduced_basis()
+ sage: assert all(l[i].reduced_norm() <= l[i+1].reduced_norm() for i in range(len(l) - 1))
+
+ sage: B = QuaternionAlgebra(next_prime(2**50))
+ sage: O = B.maximal_order()
+ sage: i,j,k = B.gens()
+ sage: alpha = 1/2 - 1/2*i + 3/2*j - 7/2*k
+ sage: I = O*alpha + O*3089622859
+ sage: I.reduced_basis()[0]
+ 1/2*i + j + 5/2*k
+ """
+ if not self.quaternion_algebra().is_definite():
+ raise TypeError("The quaternion algebra must be definite")
+
+ U = self.gram_matrix().LLL_gram().transpose()
+ return tuple(sum(c * g for c, g in zip(row, self.basis())) for row in U)
+
def theta_series_vector(self, B):
r"""
Return theta series coefficients of ``self``, as a vector
@@ -2757,10 +2877,9 @@ def minimal_element(self):
sage: el.reduced_norm()
282
"""
- qf = self.quadratic_form()
- if not qf.is_positive_definite():
+ if not self.quaternion_algebra().is_definite():
raise ValueError('quaternion algebra must be definite')
- pariqf = qf.__pari__()
+ pariqf = self.quadratic_form().__pari__()
_,v = pariqf.qfminim(None, None, 1)
return sum(ZZ(c)*g for c,g in zip(v, self.basis()))
@@ -3063,9 +3182,11 @@ def multiply_by_conjugate(self, J):
R = self.quaternion_algebra()
return R.ideal(basis, check=False)
- def is_equivalent(self, J, B=10) -> bool:
- """
- Return ``True`` if ``self`` and ``J`` are equivalent as right ideals.
+ def is_equivalent(self, J, B=10, certificate=False, side=None):
+ r"""
+ Checks whether ``self`` and ``J`` are equivalent as ideals.
+ Tests equivalence as right ideals by default. Requires the underlying
+ rational quaternion algebra to be definite.
INPUT:
@@ -3074,44 +3195,168 @@ def is_equivalent(self, J, B=10) -> bool:
- ``B`` -- a bound to compute and compare theta series before
doing the full equivalence test
- OUTPUT: bool
+ - ``certificate`` -- if ``True`` returns an element alpha such that
+ alpha*J = I or J*alpha = I for right and left ideals respectively
+
+ - ``side`` -- If ``'left'`` performs left equivalence test. If ``'right'
+ ``or ``None`` performs right ideal equivalence test
+
+ OUTPUT: bool, or (bool, alpha) if ``certificate`` is ``True``
EXAMPLES::
sage: R = BrandtModule(3,5).right_ideals(); len(R)
2
- sage: R[0].is_equivalent(R[1])
+ sage: OO = R[0].left_order()
+ sage: S = OO.right_ideal([3*a for a in R[0].basis()])
+ sage: R[0].is_equivalent(S)
+ doctest:...: DeprecationWarning: is_equivalent is deprecated,
+ please use is_left_equivalent or is_right_equivalent
+ accordingly instead
+ See https://github.com/sagemath/sage/issues/37100 for details.
+ True
+ """
+ from sage.misc.superseded import deprecation
+ deprecation(37100, 'is_equivalent is deprecated, please use is_left_equivalent'
+ ' or is_right_equivalent accordingly instead')
+ if side == 'left':
+ return self.is_left_equivalent(J, B, certificate)
+ # If None, assume right ideals, for backwards compatibility
+ return self.is_right_equivalent(J, B, certificate)
+
+ def is_left_equivalent(self, J, B=10, certificate=False):
+ r"""
+ Checks whether ``self`` and ``J`` are equivalent as left ideals.
+ Requires the underlying rational quaternion algebra to be definite.
+
+ INPUT:
+
+ - ``J`` -- a fractional quaternion left ideal with same order as ``self``
+
+ - ``B`` -- a bound to compute and compare theta series before
+ doing the full equivalence test
+
+ - ``certificate`` -- if ``True`` returns an element alpha such that J*alpha=I
+
+ OUTPUT: bool, or (bool, alpha) if ``certificate`` is ``True``
+ """
+ if certificate:
+ is_equiv, cert = self.conjugate().is_right_equivalent(J.conjugate(), B, True)
+ if is_equiv:
+ return True, cert.conjugate()
+ return False, None
+ return self.conjugate().is_right_equivalent(J.conjugate(), B, False)
+
+ def is_right_equivalent(self, J, B=10, certificate=False):
+ r"""
+ Checks whether ``self`` and ``J`` are equivalent as right ideals.
+ Requires the underlying rational quaternion algebra to be definite.
+
+ INPUT:
+
+ - ``J`` -- a fractional quaternion right ideal with same order as ``self``
+
+ - ``B`` -- a bound to compute and compare theta series before
+ doing the full equivalence test
+
+ - ``certificate`` -- if ``True`` returns an element alpha such that alpha*J=I
+
+ OUTPUT: bool, or (bool, alpha) if ``certificate`` is ``True``
+
+ EXAMPLES::
+
+ sage: R = BrandtModule(3,5).right_ideals(); len(R)
+ 2
+ sage: R[0].is_right_equivalent(R[1])
False
- sage: R[0].is_equivalent(R[0])
+
+ sage: R[0].is_right_equivalent(R[0])
True
sage: OO = R[0].left_order()
sage: S = OO.right_ideal([3*a for a in R[0].basis()])
- sage: R[0].is_equivalent(S)
+ sage: R[0].is_right_equivalent(S, certificate=True)
+ (True, -1/3)
+ sage: -1/3*S == R[0]
+ True
+
+ sage: B = QuaternionAlgebra(101)
+ sage: i,j,k = B.gens()
+ sage: I = B.maximal_order().unit_ideal()
+ sage: beta = B.random_element() # random
+ sage: J = beta*I
+ sage: bool, alpha = I.is_right_equivalent(J, certificate=True)
+ sage: bool
+ True
+ sage: alpha*J == I
True
"""
- # shorthand: let I be self
- if not isinstance(self, QuaternionFractionalIdeal_rational):
- return False
+ if not isinstance(J, QuaternionFractionalIdeal_rational):
+ raise TypeError('J must be a fractional ideal'
+ ' in a rational quaternion algebra')
if self.right_order() != J.right_order():
- raise ValueError("self and J must be right ideals")
+ raise ValueError('self and J must be right ideals over the same order')
- # Just test theta series first. If the theta series are
- # different, the ideals are definitely not equivalent.
+ if not self.quaternion_algebra().is_definite():
+ raise NotImplementedError('equivalence test of ideals not implemented'
+ ' for indefinite quaternion algebras')
+
+ # Just test theta series first; if the theta series are
+ # different, the ideals are definitely not equivalent
if B > 0 and self.theta_series_vector(B) != J.theta_series_vector(B):
+ if certificate:
+ return False, None
return False
- # The theta series are the same, so perhaps the ideals are
- # equivalent. We use Prop 1.18 of [Pizer, 1980] to decide.
+ # The theta series are the same, so perhaps the ideals are equivalent
+ # We adapt Prop 1.18 of [Piz1980]_ to right ideals to decide:
# 1. Compute I * Jbar
- # see Prop. 1.17 in Pizer. Note that we use IJbar instead of
- # JbarI since we work with right ideals
IJbar = self.multiply_by_conjugate(J)
- # 2. Determine if there is alpha in K such
- # that N(alpha) = N(I)*N(J) as explained by Pizer.
- c = IJbar.theta_series_vector(2)[1]
- return c != 0
+ # 2. Determine if there is alpha in I * Jbar with N(alpha) = N(I)*N(J)
+ # Equivalently, we can simply call the principality test on IJbar,
+ # but we rescale by 1/N(J) to make sure this test directly gives back
+ # the correct alpha if a certificate is requested
+ return (1/J.norm()*IJbar).is_principal(certificate)
+
+ def is_principal(self, certificate=False):
+ r"""
+ Checks whether ``self`` is principal as a full rank quaternion ideal.
+ Requires the underlying quaternion algebra to be definite.
+ Independent of whether ``self`` is a left or a right ideal.
+
+ INPUT:
+
+ - ``certificate`` -- if ``True`` returns a generator alpha s.t. I = alpha*O
+ where O is the right order of I.
+
+ OUTPUT: bool, or (bool, alpha) if ``certificate`` is ``True``
+
+ EXAMPLES::
+
+ sage: B. = QuaternionAlgebra(419)
+ sage: O = B.quaternion_order([1/2 + 3/2*j, 1/6*i + 2/3*j + 1/2*k, 3*j, k])
+ sage: beta = O.random_element() # random
+ sage: I = O*beta
+ sage: bool, alpha = I.is_principal(True)
+ sage: bool
+ True
+ sage: I == O*alpha
+ True
+ """
+ if not self.quaternion_algebra().is_definite():
+ raise NotImplementedError('principality test not implemented in'
+ ' indefinite quaternion algebras')
+
+ c = self.theta_series_vector(2)[1]
+ if not certificate:
+ return c != 0
+ if certificate and c == 0:
+ return False, None
+
+ # From this point on we know that self is principal, so it suffices to
+ # find an element of minimal norm in self; see [Piz1980]_, Corollary 1.20.
+ return True, self.minimal_element()
def __contains__(self, x):
"""
diff --git a/src/sage/categories/finite_dimensional_lie_algebras_with_basis.py b/src/sage/categories/finite_dimensional_lie_algebras_with_basis.py
index e6ed304aa7b..6cb7badd99a 100644
--- a/src/sage/categories/finite_dimensional_lie_algebras_with_basis.py
+++ b/src/sage/categories/finite_dimensional_lie_algebras_with_basis.py
@@ -7,7 +7,7 @@
"""
# ****************************************************************************
-# Copyright (C) 2013-2017 Travis Scrimshaw
+# Copyright (C) 2013-2024 Travis Scrimshaw
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -1745,10 +1745,10 @@ def universal_commutative_algebra(self):
R = P[0].parent()
return R.quotient(P)
- def casimir_element(self, order=2, UEA=None, force_generic=False):
+ def casimir_element(self, order=2, UEA=None, force_generic=False, basis=False):
r"""
- Return the Casimir element in the universal enveloping algebra
- of ``self``.
+ Return a Casimir element of order ``order`` in the universal
+ enveloping algebra of ``self``.
A *Casimir element* of order `k` is a distinguished basis element
for the center of `U(\mathfrak{g})` of homogeneous degree `k`
@@ -1760,11 +1760,13 @@ def casimir_element(self, order=2, UEA=None, force_generic=False):
INPUT:
- ``order`` -- (default: ``2``) the order of the Casimir element
- - ``UEA`` -- (optional) the universal enveloping algebra to
- return the result in
+ - ``UEA`` -- (optional) the universal enveloping algebra
+ implementation to return the result in
- ``force_generic`` -- (default: ``False``) if ``True`` for the
quadratic order, then this uses the default algorithm; otherwise
this is ignored
+ - ``basis`` -- (default: ``False``) if ``True``, this returns a
+ basis of all Casimir elements of order ``order`` as a list
ALGORITHM:
@@ -1832,6 +1834,13 @@ def casimir_element(self, order=2, UEA=None, force_generic=False):
sage: L.casimir_element()
0
+ sage: # needs sage.combinat sage.modules
+ sage: g = LieAlgebra(QQ, cartan_type=['D',2])
+ sage: U = g.pbw_basis()
+ sage: U.casimir_element(2, basis=True)
+ [2*PBW[alpha[2]]*PBW[-alpha[2]] + 1/2*PBW[alphacheck[2]]^2 - PBW[alphacheck[2]],
+ 2*PBW[alpha[1]]*PBW[-alpha[1]] + 1/2*PBW[alphacheck[1]]^2 - PBW[alphacheck[1]]]
+
TESTS::
sage: # needs sage.combinat sage.modules
@@ -1856,7 +1865,7 @@ def casimir_element(self, order=2, UEA=None, force_generic=False):
B = self.basis()
- if order == 2 and not force_generic:
+ if order == 2 and not force_generic and not basis:
# Special case for the quadratic using the Killing form
try:
K = self.killing_form_matrix().inverse()
@@ -1896,11 +1905,10 @@ def casimir_element(self, order=2, UEA=None, force_generic=False):
if ker.dimension() == 0:
return self.zero()
- tens = ker.basis()[0]
del eqns # no need to hold onto the matrix
- def to_prod(index):
- coeff = tens[index]
+ def to_prod(vec, index):
+ coeff = vec[index]
p = [0] * order
base = dim ** (order-1)
for i in range(order):
@@ -1910,7 +1918,14 @@ def to_prod(index):
p.reverse()
return coeff * UEA.prod(UEA(B[keys[i]]) for i in p)
- return UEA.sum(to_prod(index) for index in tens.support())
+ tens = ker.basis()
+
+ if not basis:
+ vec = tens[0]
+ return UEA.sum(to_prod(vec, index) for index in vec.support())
+
+ return [UEA.sum(to_prod(vec, index) for index in vec.support())
+ for vec in tens]
class ElementMethods:
def adjoint_matrix(self, sparse=False): # In #11111 (more or less) by using matrix of a morphism
diff --git a/src/sage/categories/lie_algebras.py b/src/sage/categories/lie_algebras.py
index 81596a7786c..d0958f12d89 100644
--- a/src/sage/categories/lie_algebras.py
+++ b/src/sage/categories/lie_algebras.py
@@ -346,6 +346,25 @@ def _construct_UEA(self):
Multivariate Polynomial Ring in x0, x1, x2 over Rational Field
"""
+ def center_universal_enveloping_algebra(self, UEA=None):
+ """
+ Return the center of the universal enveloping algebra of ``self``.
+
+ EXAMPLES::
+
+ sage: L = LieAlgebra(QQ, 3, 'x', abelian=True)
+ sage: L.center_universal_enveloping_algebra()
+ Center of Universal enveloping algebra of Abelian Lie algebra on 3 generators (x0, x1, x2)
+ over Rational Field in the Poincare-Birkhoff-Witt basis
+ sage: PBW = L.pbw_basis()
+ sage: L.center_universal_enveloping_algebra(PBW)
+ Center of Universal enveloping algebra of Abelian Lie algebra on 3 generators (x0, x1, x2)
+ over Rational Field in the Poincare-Birkhoff-Witt basis
+ """
+ if UEA is not None:
+ return UEA.center()
+ return self.pbw_basis().center()
+
@abstract_method(optional=True)
def module(self):
r"""
diff --git a/src/sage/categories/lie_algebras_with_basis.py b/src/sage/categories/lie_algebras_with_basis.py
index 6eaebfde844..67570b611b3 100644
--- a/src/sage/categories/lie_algebras_with_basis.py
+++ b/src/sage/categories/lie_algebras_with_basis.py
@@ -7,13 +7,13 @@
"""
#*****************************************************************************
-# Copyright (C) 2013-2017 Travis Scrimshaw
+# Copyright (C) 2013-2024 Travis Scrimshaw
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
-# http://www.gnu.org/licenses/
+# https://www.gnu.org/licenses/
#*****************************************************************************
from sage.misc.abstract_method import abstract_method
@@ -239,10 +239,6 @@ def lift(self):
"""
P = self.parent()
UEA = P.universal_enveloping_algebra()
- try:
- gen_dict = UEA.algebra_generators()
- except (TypeError, AttributeError):
- gen_dict = UEA.gens_dict()
s = UEA.zero()
if not self:
return s
@@ -250,9 +246,14 @@ def lift(self):
# does not match the generators index set of the UEA.
if hasattr(P, '_UEA_names_map'):
names_map = P._UEA_names_map
+ gen_dict = UEA.gens_dict()
for t, c in self.monomial_coefficients(copy=False).items():
s += c * gen_dict[names_map[t]]
else:
+ try:
+ gen_dict = UEA.algebra_generators()
+ except (TypeError, AttributeError):
+ gen_dict = UEA.gens_dict()
for t, c in self.monomial_coefficients(copy=False).items():
s += c * gen_dict[t]
return s
diff --git a/src/sage/categories/magmatic_algebras.py b/src/sage/categories/magmatic_algebras.py
index f0537c104d6..763f5aab764 100644
--- a/src/sage/categories/magmatic_algebras.py
+++ b/src/sage/categories/magmatic_algebras.py
@@ -221,8 +221,8 @@ def _product_from_product_on_basis_multiply( self, left, right ):
"""
return self.linear_combination((self.product_on_basis(mon_left, mon_right), coeff_left * coeff_right )
- for (mon_left, coeff_left) in left.monomial_coefficients().items()
- for (mon_right, coeff_right) in right.monomial_coefficients().items() )
+ for (mon_left, coeff_left) in left.monomial_coefficients(copy=False).items()
+ for (mon_right, coeff_right) in right.monomial_coefficients(copy=False).items() )
class FiniteDimensional(CategoryWithAxiom_over_base_ring):
class ParentMethods:
diff --git a/src/sage/combinat/algebraic_combinatorics.py b/src/sage/combinat/algebraic_combinatorics.py
index a024d2fa67d..3dd16bf7799 100644
--- a/src/sage/combinat/algebraic_combinatorics.py
+++ b/src/sage/combinat/algebraic_combinatorics.py
@@ -12,7 +12,8 @@
----------------------------------------
- :ref:`sage.combinat.catalog_partitions`
-- :class:`~sage.combinat.gelfand_tsetlin_patterns.GelfandTsetlinPattern`, :class:`~sage.combinat.gelfand_tsetlin_patterns.GelfandTsetlinPatterns`
+- :class:`~sage.combinat.gelfand_tsetlin_patterns.GelfandTsetlinPattern`,
+ :class:`~sage.combinat.gelfand_tsetlin_patterns.GelfandTsetlinPatterns`
- :class:`~sage.combinat.knutson_tao_puzzles.KnutsonTaoPuzzleSolver`
Groups and Algebras
@@ -39,7 +40,7 @@
- :ref:`sage.combinat.cluster_algebra_quiver.all`
- :class:`~sage.combinat.kazhdan_lusztig.KazhdanLusztigPolynomial`
- :class:`~sage.combinat.symmetric_group_representations.SymmetricGroupRepresentation`
-- :class:`~sage.combinat.specht_module.SpechtModule`
+- :ref:`sage.combinat.specht_module`
- :ref:`sage.combinat.yang_baxter_graph`
- :ref:`sage.combinat.hall_polynomial`
- :ref:`sage.combinat.key_polynomial`
diff --git a/src/sage/combinat/catalog_partitions.py b/src/sage/combinat/catalog_partitions.py
index ca143fa540c..302e8f112d6 100644
--- a/src/sage/combinat/catalog_partitions.py
+++ b/src/sage/combinat/catalog_partitions.py
@@ -8,6 +8,7 @@
- :ref:`sage.combinat.skew_partition`
- :ref:`sage.combinat.partition_tuple`
- :ref:`sage.combinat.superpartition`
+- :ref:`sage.combinat.tableau`
- :ref:`sage.combinat.tableau_tuple`
- :ref:`sage.combinat.skew_tableau`
- :ref:`sage.combinat.ribbon`
diff --git a/src/sage/combinat/composition.py b/src/sage/combinat/composition.py
index 8d01e9ac081..0be0ba09074 100644
--- a/src/sage/combinat/composition.py
+++ b/src/sage/combinat/composition.py
@@ -45,6 +45,9 @@
from sage.combinat.combinatorial_map import combinatorial_map
from sage.misc.persist import register_unpickle_override
+from sage.misc.lazy_import import lazy_import
+lazy_import("sage.combinat.partition", "Partition")
+
class Composition(CombinatorialElement):
r"""
@@ -1186,7 +1189,6 @@ def to_partition(self):
sage: Composition([]).to_partition() # needs sage.combinat
[]
"""
- from sage.combinat.partition import Partition
return Partition(sorted(self, reverse=True))
def to_skew_partition(self, overlap=1):
@@ -1753,8 +1755,10 @@ def _element_constructor_(self, lst) -> Composition:
sage: P = Compositions()
sage: P([3,3,1]) # indirect doctest
[3, 3, 1]
+ sage: P(Partition([5,2,1]))
+ [5, 2, 1]
"""
- if isinstance(lst, Composition):
+ if isinstance(lst, (Composition, Partition)):
lst = list(lst)
elt = self.element_class(self, lst)
if elt not in self:
@@ -1774,7 +1778,7 @@ def __contains__(self, x) -> bool:
sage: [0,0] in Compositions()
True
"""
- if isinstance(x, Composition):
+ if isinstance(x, (Composition, Partition)):
return True
elif isinstance(x, list):
for i in x:
diff --git a/src/sage/combinat/partition.py b/src/sage/combinat/partition.py
index 3ff9bb2f1d6..196cc0b401a 100644
--- a/src/sage/combinat/partition.py
+++ b/src/sage/combinat/partition.py
@@ -5495,9 +5495,8 @@ def specht_module(self, base_ring=None):
EXAMPLES::
sage: SM = Partition([2,2,1]).specht_module(QQ); SM
- Specht module of [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0)] over Rational Field
- sage: s = SymmetricFunctions(QQ).s()
- sage: s(SM.frobenius_image()) # needs sage.modules
+ Specht module of [2, 2, 1] over Rational Field
+ sage: SM.frobenius_image() # needs sage.modules
s[2, 2, 1]
"""
from sage.combinat.specht_module import SpechtModule
@@ -5570,6 +5569,24 @@ def simple_module_dimension(self, base_ring=None):
from sage.combinat.specht_module import simple_module_rank
return simple_module_rank(self, base_ring)
+ def tabloid_module(self, base_ring=None):
+ r"""
+ Return the tabloid module corresponding to ``self``.
+
+ EXAMPLES::
+
+ sage: TM = Partition([2,2,1]).tabloid_module(QQ); TM
+ Tabloid module of [2, 2, 1] over Rational Field
+ sage: TM.frobenius_image()
+ s[2, 2, 1] + s[3, 1, 1] + 2*s[3, 2] + 2*s[4, 1] + s[5]
+ """
+ from sage.combinat.specht_module import TabloidModule
+ from sage.combinat.symmetric_group_algebra import SymmetricGroupAlgebra
+ if base_ring is None:
+ from sage.rings.rational_field import QQ
+ base_ring = QQ
+ R = SymmetricGroupAlgebra(base_ring, sum(self))
+ return TabloidModule(R, self)
##############
# Partitions #
diff --git a/src/sage/combinat/permutation.py b/src/sage/combinat/permutation.py
index 8152ed81a05..4769442d3dd 100644
--- a/src/sage/combinat/permutation.py
+++ b/src/sage/combinat/permutation.py
@@ -1263,7 +1263,7 @@ def __mul__(self, rp):
sage: p213 * SGA.an_element()
3*[1, 2, 3] + [1, 3, 2] + [2, 1, 3] + 2*[3, 1, 2]
sage: p213 * SM.an_element()
- 2*B[0] - 4*B[1]
+ 2*S[[1, 2], [3]] - 4*S[[1, 3], [2]]
"""
if not isinstance(rp, Permutation) and isinstance(rp, Element):
return get_coercion_model().bin_op(self, rp, operator.mul)
@@ -7092,7 +7092,19 @@ def _element_constructor_(self, x, check=True):
[1, 4, 5, 2, 3, 6]
sage: Permutations(6)(x) # known bug
[1, 4, 5, 2, 3, 6]
+
+ Ensure that :issue:`37284` is fixed::
+
+ sage: PG = PermutationGroup([[(1,2,3),(5,6)],[(7,8)]])
+ sage: P8 = Permutations(8)
+ sage: p = PG.an_element()
+ sage: q = P8(p); q
+ [2, 3, 1, 4, 6, 5, 8, 7]
+ sage: q.parent()
+ Standard permutations of 8
"""
+ if isinstance(x, PermutationGroupElement):
+ return self. _from_permutation_group_element(x)
if len(x) < self.n:
x = list(x) + list(range(len(x) + 1, self.n + 1))
return self.element_class(self, x, check=check)
@@ -7342,6 +7354,19 @@ def cardinality(self):
"""
return factorial(self.n)
+ @cached_method
+ def gens(self) -> tuple:
+ r"""
+ Return a set of generators for ``self`` as a group.
+
+ EXAMPLES::
+
+ sage: P4 = Permutations(4)
+ sage: P4.gens()
+ ([2, 1, 3, 4], [1, 3, 2, 4], [1, 2, 4, 3])
+ """
+ return tuple(self.group_generators())
+
def degree(self):
"""
Return the degree of ``self``.
diff --git a/src/sage/combinat/skew_tableau.py b/src/sage/combinat/skew_tableau.py
index 44dece78ab0..a342e7005e8 100644
--- a/src/sage/combinat/skew_tableau.py
+++ b/src/sage/combinat/skew_tableau.py
@@ -6,11 +6,11 @@
- Mike Hansen: Initial version
- Travis Scrimshaw, Arthur Lubovsky (2013-02-11):
Factored out ``CombinatorialClass``
-- Trevor K. Karn (2022-08-03): added `backward_slide`
+- Trevor K. Karn (2022-08-03): added ``backward_slide``
"""
# ****************************************************************************
# Copyright (C) 2007 Mike Hansen ,
-# Copyright (C) 2013 Travis Scrimshaw
+# Copyright (C) 2013 Travis Scrimshaw
# Copyright (C) 2013 Arthur Lubovsky
#
# Distributed under the terms of the GNU General Public License (GPL)
diff --git a/src/sage/combinat/specht_module.py b/src/sage/combinat/specht_module.py
index 62842b11a30..127903829f7 100644
--- a/src/sage/combinat/specht_module.py
+++ b/src/sage/combinat/specht_module.py
@@ -5,6 +5,13 @@
AUTHORS:
- Travis Scrimshaw (2023-1-22): initial version
+- Travis Scrimshaw (2023-11-23): added simple modules based on code
+ from Sacha Goldman
+
+.. TODO::
+
+ Integrate this with the implementations in
+ :mod:`sage.modules.with_basis.representation`.
"""
# ****************************************************************************
@@ -18,15 +25,243 @@
# ****************************************************************************
from sage.misc.cachefunc import cached_method
+from sage.misc.lazy_attribute import lazy_attribute
from sage.combinat.diagram import Diagram
+from sage.combinat.partition import _Partitions
+from sage.combinat.free_module import CombinatorialFreeModule
+from sage.modules.with_basis.representation import Representation_abstract
from sage.sets.family import Family
from sage.matrix.constructor import matrix
from sage.rings.rational_field import QQ
-from sage.modules.with_basis.subquotient import SubmoduleWithBasis
+from sage.modules.with_basis.subquotient import SubmoduleWithBasis, QuotientModuleWithBasis
+from sage.modules.free_module_element import vector
from sage.categories.modules_with_basis import ModulesWithBasis
+class SymmetricGroupRepresentation:
+ """
+ Mixin class for symmetric group (algebra) representations.
+ """
+ def __init__(self, SGA):
+ """
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: SM = Partition([3,1,1]).specht_module(GF(3))
+ sage: TestSuite(SM).run()
+ """
+ self._semigroup = SGA.group()
+ self._semigroup_algebra = SGA
+
+ def side(self):
+ r"""
+ Return the side of the action defining ``self``.
+
+ EXAMPLES::
+
+ sage: SM = Partition([3,1,1]).specht_module(GF(3))
+ sage: SM.side()
+ 'left'
+ """
+ return "left"
+
+ @cached_method
+ def frobenius_image(self):
+ r"""
+ Return the Frobenius image of ``self``.
+
+ The Frobenius map is defined as the map to symmetric functions
+
+ .. MATH::
+
+ F(\chi) = \frac{1}{n!} \sum_{w \in S_n} \chi(w) p_{\rho(w)},
+
+ where `\chi` is the character of the `S_n`-module ``self``,
+ `p_{\lambda}` is the powersum symmetric function basis element
+ indexed by `\lambda`, and `\rho(w)` is the cycle type of `w` as a
+ partition. Specifically, this map takes irreducible representations
+ indexed by `\lambda` to the Schur function `s_{\lambda}`.
+
+ EXAMPLES::
+
+ sage: SM = Partition([2,2,1]).specht_module(QQ)
+ sage: SM.frobenius_image()
+ s[2, 2, 1]
+ sage: SM = Partition([4,1]).specht_module(CyclotomicField(5))
+ sage: SM.frobenius_image()
+ s[4, 1]
+
+ We verify the regular representation::
+
+ sage: from sage.combinat.diagram import Diagram
+ sage: D = Diagram([(0,0), (1,1), (2,2), (3,3), (4,4)])
+ sage: F = D.specht_module(QQ).frobenius_image(); F
+ s[1, 1, 1, 1, 1] + 4*s[2, 1, 1, 1] + 5*s[2, 2, 1]
+ + 6*s[3, 1, 1] + 5*s[3, 2] + 4*s[4, 1] + s[5]
+ sage: s = SymmetricFunctions(QQ).s()
+ sage: F == sum(StandardTableaux(la).cardinality() * s[la]
+ ....: for la in Partitions(5))
+ True
+ sage: all(s[la] == la.specht_module(QQ).frobenius_image()
+ ....: for n in range(1, 5) for la in Partitions(n))
+ True
+
+ sage: D = Diagram([(0,0), (1,1), (1,2), (2,3), (2,4)])
+ sage: SM = D.specht_module(QQ)
+ sage: SM.frobenius_image()
+ s[2, 2, 1] + s[3, 1, 1] + 2*s[3, 2] + 2*s[4, 1] + s[5]
+
+ An example using the tabloid module::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2, 2, 1])
+ sage: TM.frobenius_image()
+ s[2, 2, 1] + s[3, 1, 1] + 2*s[3, 2] + 2*s[4, 1] + s[5]
+ """
+ from sage.combinat.sf.sf import SymmetricFunctions
+ p = SymmetricFunctions(QQ).p()
+ s = SymmetricFunctions(QQ).s()
+ G = self._semigroup
+ CCR = [(elt, elt.cycle_type()) for elt in G.conjugacy_classes_representatives()]
+ return s(p.sum(QQ(self.representation_matrix(elt).trace()) / la.centralizer_size() * p[la]
+ for elt, la in CCR))
+
+ # TODO: Move these methods up to methods of general representations
+
+ def representation_matrix(self, elt):
+ r"""
+ Return the matrix corresponding to the left action of the symmetric
+ group (algebra) element ``elt`` on ``self``.
+
+ EXAMPLES::
+
+ sage: SM = Partition([3,1,1]).specht_module(QQ)
+ sage: SM.representation_matrix(Permutation([2,1,3,5,4]))
+ [-1 0 0 0 0 0]
+ [ 0 0 0 -1 0 0]
+ [ 1 0 0 -1 1 0]
+ [ 0 -1 0 0 0 0]
+ [ 1 -1 1 0 0 0]
+ [ 0 -1 0 1 0 -1]
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([(0,0), (0,1), (0,2), (1,0), (2,0)])
+ sage: SM.representation_matrix(Permutation([2,1,3,5,4]))
+ [-1 0 0 1 -1 0]
+ [ 0 0 1 0 -1 1]
+ [ 0 1 0 -1 0 1]
+ [ 0 0 0 0 -1 0]
+ [ 0 0 0 -1 0 0]
+ [ 0 0 0 0 0 -1]
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM.representation_matrix(SGA([3,1,5,2,4]))
+ [ 0 -1 0 1 0 -1]
+ [ 0 0 0 0 0 -1]
+ [ 0 0 0 -1 0 0]
+ [ 0 0 -1 0 1 -1]
+ [ 1 0 0 -1 1 0]
+ [ 0 0 0 0 1 0]
+ """
+ return matrix(self.base_ring(), [(elt * b).to_vector() for b in self.basis()])
+
+ @cached_method
+ def character(self):
+ r"""
+ Return the character of ``self``.
+
+ EXAMPLES::
-class SpechtModule(SubmoduleWithBasis):
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: SM.character()
+ (5, 1, 1, -1, 1, -1, 0)
+ sage: matrix(SGA.specht_module(la).character() for la in Partitions(5))
+ [ 1 1 1 1 1 1 1]
+ [ 4 2 0 1 -1 0 -1]
+ [ 5 1 1 -1 1 -1 0]
+ [ 6 0 -2 0 0 0 1]
+ [ 5 -1 1 -1 -1 1 0]
+ [ 4 -2 0 1 1 0 -1]
+ [ 1 -1 1 1 -1 -1 1]
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, SymmetricGroup(5))
+ sage: SM = SGA.specht_module([3,2])
+ sage: SM.character()
+ Character of Symmetric group of order 5! as a permutation group
+ sage: SM.character().values()
+ [5, 1, 1, -1, 1, -1, 0]
+ sage: matrix(SGA.specht_module(la).character().values() for la in reversed(Partitions(5)))
+ [ 1 -1 1 1 -1 -1 1]
+ [ 4 -2 0 1 1 0 -1]
+ [ 5 -1 1 -1 -1 1 0]
+ [ 6 0 -2 0 0 0 1]
+ [ 5 1 1 -1 1 -1 0]
+ [ 4 2 0 1 -1 0 -1]
+ [ 1 1 1 1 1 1 1]
+ sage: SGA.group().character_table()
+ [ 1 -1 1 1 -1 -1 1]
+ [ 4 -2 0 1 1 0 -1]
+ [ 5 -1 1 -1 -1 1 0]
+ [ 6 0 -2 0 0 0 1]
+ [ 5 1 1 -1 1 -1 0]
+ [ 4 2 0 1 -1 0 -1]
+ [ 1 1 1 1 1 1 1]
+ """
+ G = self._semigroup
+ chi = [self.representation_matrix(g).trace()
+ for g in G.conjugacy_classes_representatives()]
+ try:
+ return G.character(chi)
+ except AttributeError:
+ return vector(chi, immutable=True)
+
+ @cached_method
+ def brauer_character(self):
+ r"""
+ Return the Brauer character of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(2), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: SM.brauer_character()
+ (5, -1, 0)
+ sage: SM.simple_module().brauer_character()
+ (4, -2, -1)
+ """
+ from sage.rings.number_field.number_field import CyclotomicField
+ from sage.arith.functions import lcm
+ G = self._semigroup
+ p = self.base_ring().characteristic()
+ # We manually compute the order since a Permutation does not implement order()
+ chi = []
+ for g in G.conjugacy_classes_representatives():
+ if p.divides(lcm(g.cycle_type())):
+ # ignore the non-p-regular elements
+ continue
+ evals = self.representation_matrix(g).eigenvalues()
+ K = evals[0].parent()
+ val = 0
+ orders = {la: la.multiplicative_order() for la in evals if la != K.one()}
+ zetas = {o: CyclotomicField(o).gen() for o in orders.values()}
+ prims = {o: K.zeta(o) for o in orders.values()}
+ for la in evals:
+ if la == K.one():
+ val += 1
+ continue
+ o = la.multiplicative_order()
+ zeta = zetas[o]
+ prim = prims[o]
+ for deg in range(o):
+ if prim ** deg == la:
+ val += zeta ** deg
+ break
+ chi.append(val)
+
+ return vector(chi, immutable=True)
+
+
+class SpechtModule(SubmoduleWithBasis, SymmetricGroupRepresentation, Representation_abstract):
r"""
A Specht module.
@@ -84,13 +319,13 @@ class SpechtModule(SubmoduleWithBasis):
sage: S5 = SGA.group()
sage: v = SM.an_element(); v
- 2*B[0] + 2*B[1] + 3*B[2]
+ 2*S[0] + 2*S[1] + 3*S[2]
sage: S5([2,1,5,3,4]) * v
- 3*B[0] + 2*B[1] + 2*B[2]
+ 3*S[0] + 2*S[1] + 2*S[2]
sage: x = SGA.an_element(); x
[1, 2, 3, 4, 5] + 2*[1, 2, 3, 5, 4] + 3*[1, 2, 4, 3, 5] + [5, 1, 2, 3, 4]
sage: x * v
- 15*B[0] + 14*B[1] + 16*B[2] - 7*B[5] + 2*B[6] + 2*B[7]
+ 15*S[0] + 14*S[1] + 16*S[2] - 7*S[5] + 2*S[6] + 2*S[7]
.. SEEALSO::
@@ -120,6 +355,9 @@ def __classcall_private__(cls, SGA, D):
...
ValueError: the domain size (=3) does not match the number of boxes (=2) of the diagram
"""
+ if D in _Partitions:
+ D = _Partitions(D)
+ return TabloidModule(SGA, D).specht_module()
D = _to_diagram(D)
D = Diagram(D)
n = len(D)
@@ -138,6 +376,7 @@ def __init__(self, SGA, D):
sage: SM = SGA.specht_module([(0,0), (1,1), (1,2), (2,1)])
sage: TestSuite(SM).run()
"""
+ SymmetricGroupRepresentation.__init__(self, SGA)
self._diagram = D
Mod = ModulesWithBasis(SGA.category().base_ring())
span_set = specht_module_spanning_set(D, SGA)
@@ -145,7 +384,8 @@ def __init__(self, SGA, D):
basis = SGA.echelon_form(span_set, False, order=support_order)
basis = Family(basis)
SubmoduleWithBasis.__init__(self, basis, support_order, ambient=SGA,
- unitriangular=False, category=Mod.Subobjects())
+ unitriangular=False, category=Mod.Subobjects(),
+ prefix='S')
def _repr_(self):
r"""
@@ -156,6 +396,9 @@ def _repr_(self):
sage: SGA = SymmetricGroupAlgebra(QQ, 4)
sage: SGA.specht_module([(0,0), (1,1), (1,2), (2,1)])
Specht module of [(0, 0), (1, 1), (1, 2), (2, 1)] over Rational Field
+
+ sage: SGA.specht_module([3, 1])
+ Specht module of [3, 1] over Rational Field
"""
return f"Specht module of {self._diagram} over {self.base_ring()}"
@@ -168,13 +411,22 @@ def _latex_(self):
sage: SGA = SymmetricGroupAlgebra(QQ, 4)
sage: SM = SGA.specht_module([(0,0), (1,1), (1,2), (2,1)])
sage: latex(SM)
- S^{{\def\lr#1{\multicolumn{1}{|@{\hspace{.6ex}}c@{\hspace{.6ex}}|}{\raisebox{-.3ex}{$#1$}}}
- \raisebox{-.6ex}{$\begin{array}[b]{*{3}{p{0.6ex}}}\cline{1-1}
- \lr{\phantom{x}}&&\\\cline{1-1}\cline{2-2}\cline{3-3}
- &\lr{\phantom{x}}&\lr{\phantom{x}}\\\cline{2-2}\cline{3-3}\cline{2-2}
- &\lr{\phantom{x}}&\\\cline{2-2}
- \end{array}$}
- }}
+ S^{{\def\lr#1{\multicolumn{1}{|@{\hspace{.6ex}}c@{\hspace{.6ex}}|}{\raisebox{-.3ex}{$#1$}}}
+ \raisebox{-.6ex}{$\begin{array}[b]{*{3}{p{0.6ex}}}\cline{1-1}
+ \lr{\phantom{x}}&&\\\cline{1-1}\cline{2-2}\cline{3-3}
+ &\lr{\phantom{x}}&\lr{\phantom{x}}\\\cline{2-2}\cline{3-3}\cline{2-2}
+ &\lr{\phantom{x}}&\\\cline{2-2}
+ \end{array}$}
+ }}
+
+ sage: SM = SGA.specht_module([3,1])
+ sage: latex(SM)
+ S^{{\def\lr#1{\multicolumn{1}{|@{\hspace{.6ex}}c@{\hspace{.6ex}}|}{\raisebox{-.3ex}{$#1$}}}
+ \raisebox{-.6ex}{$\begin{array}[b]{*{3}c}\cline{1-3}
+ \lr{\phantom{x}}&\lr{\phantom{x}}&\lr{\phantom{x}}\\\cline{1-3}
+ \lr{\phantom{x}}\\\cline{1-1}
+ \end{array}$}
+ }}
"""
from sage.misc.latex import latex
return f"S^{{{latex(self._diagram)}}}"
@@ -192,6 +444,12 @@ def _ascii_art_(self):
. O O
. O .
S
+
+ sage: SM = SGA.specht_module([3,1])
+ sage: ascii_art(SM)
+ ***
+ *
+ S
"""
from sage.typeset.ascii_art import ascii_art
return ascii_art("S", baseline=0) + ascii_art(self._diagram, baseline=-1)
@@ -213,99 +471,299 @@ def _unicode_art_(self):
│ │X│ │
└─┴─┴─┘
S
+
+ sage: SM = SGA.specht_module([3,1])
+ sage: unicode_art(SM)
+ ┌┬┬┐
+ ├┼┴┘
+ └┘
+ S
"""
from sage.typeset.unicode_art import unicode_art
return unicode_art("S", baseline=0) + unicode_art(self._diagram, baseline=-1)
- def representation_matrix(self, elt):
+ class Element(SubmoduleWithBasis.Element):
+ def _acted_upon_(self, x, self_on_left=False):
+ """
+ Return the action of ``x`` on ``self``.
+
+ INPUT:
+
+ - ``x`` -- an element of the base ring or can be converted into
+ the defining symmetric group algebra
+ - ``self_on_left`` -- boolean (default: ``False``); which side
+ ``self`` is on for the action
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: from sage.combinat.diagram import Diagram
+ sage: D = Diagram([(0,0), (1,1), (2,2), (3,3), (4,4)])
+ sage: SM = SGA.specht_module(D)
+ sage: SGA.an_element() * SM.an_element()
+ 15*S[0] + 6*S[1] + 9*S[2] + 6*S[3] + 6*S[4] + 2*S[72] + 2*S[96] + 3*S[97]
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 4)
+ sage: SM = SGA.specht_module([3,1])
+ sage: SGA.an_element() * SM.an_element()
+ 9*S[[1, 2, 3], [4]] + 17*S[[1, 2, 4], [3]] + 14*S[[1, 3, 4], [2]]
+ sage: 4 * SM.an_element()
+ 12*S[[1, 2, 3], [4]] + 8*S[[1, 2, 4], [3]] + 8*S[[1, 3, 4], [2]]
+
+ TESTS::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 4)
+ sage: SM = SGA.specht_module([3,1])
+ sage: SM.an_element() * SGA.an_element()
+ Traceback (most recent call last):
+ ...
+ TypeError: unsupported operand parent(s) for *:
+ 'Specht module of [3, 1] over Rational Field'
+ and 'Symmetric group algebra of order 4 over Rational Field'
+ sage: groups.permutation.Dihedral(3).an_element() * SM.an_element()
+ Traceback (most recent call last):
+ ...
+ TypeError: unsupported operand parent(s) for *:
+ 'Dihedral group of order 6 as a permutation group'
+ and 'Specht module of [3, 1] over Rational Field'
+ """
+ # Check for a scalar first
+ ret = super()._acted_upon_(x, self_on_left)
+ if ret is not None:
+ return ret
+ # Check if it is in the symmetric group algebra
+ P = self.parent()
+ if x in P._semigroup_algebra or x in P._semigroup_algebra.group():
+ if self_on_left: # it is only a left module
+ return None
+ else:
+ return P.retract(P._semigroup_algebra(x) * self.lift())
+ return None
+
+
+class TabloidModule(SymmetricGroupRepresentation, Representation_abstract):
+ r"""
+ The vector space of all tabloids of a fixed shape with the natural
+ symmetric group action.
+
+ A *tabloid* is an :class:`OrderedSetPartition` whose underlying set
+ is `\{1, \ldots, n\}`. The symmetric group acts by permuting the
+ entries of the set. Hence, this is a representation of the symmetric
+ group defined over any field.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: TM = SGA.tabloid_module([2, 2, 1])
+ sage: TM.dimension()
+ 30
+ sage: TM.brauer_character()
+ (30, 6, 2, 0, 0)
+ sage: IM = TM.invariant_module()
+ sage: IM.dimension()
+ 1
+ sage: IM.basis()[0].lift() == sum(TM.basis())
+ True
+ """
+ @staticmethod
+ def __classcall_private__(cls, SGA, shape):
r"""
- Return the matrix corresponding to the left action of the symmetric
- group (algebra) element ``elt`` on ``self``.
+ Normalize input to ensure a unique representation.
- .. SEEALSO::
+ EXAMPLES::
+
+ sage: from sage.combinat.specht_module import TabloidModule
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM1 = TabloidModule(SGA, [2, 2, 1])
+ sage: TM2 = TabloidModule(SGA, Partition([2, 2, 1]))
+ sage: TM1 is TM2
+ True
- :class:`~sage.combinat.symmetric_group_representations.SpechtRepresentation`
+ sage: TabloidModule(SGA, [3, 2, 1])
+ Traceback (most recent call last):
+ ...
+ ValueError: the domain size (=5) does not match the number of boxes (=6) of the diagram
+ """
+ shape = _Partitions(shape)
+ if SGA.group().rank() != sum(shape) - 1:
+ rk = SGA.group().rank() + 1
+ n = sum(shape)
+ raise ValueError(f"the domain size (={rk}) does not match the number of boxes (={n}) of the diagram")
+ return super().__classcall__(cls, SGA, shape)
+
+ def __init__(self, SGA, shape):
+ r"""
+ Initialize ``self``.
EXAMPLES::
- sage: SM = Partition([3,1,1]).specht_module(QQ)
- sage: SM.representation_matrix(Permutation([2,1,3,5,4]))
- [-1 0 0 1 -1 0]
- [ 0 0 1 0 -1 1]
- [ 0 1 0 -1 0 1]
- [ 0 0 0 0 -1 0]
- [ 0 0 0 -1 0 0]
- [ 0 0 0 0 0 -1]
sage: SGA = SymmetricGroupAlgebra(QQ, 5)
- sage: SM.representation_matrix(SGA([3,1,5,2,4]))
- [ 0 -1 0 1 0 -1]
- [ 0 0 0 0 0 -1]
- [ 0 0 0 -1 0 0]
- [ 0 0 -1 0 1 -1]
- [ 1 0 0 -1 1 0]
- [ 0 0 0 0 1 0]
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: TestSuite(TM).run()
"""
- SGA = self._ambient
- return matrix(self.base_ring(), [self.retract(SGA(elt) * b.lift()).to_vector()
- for b in self.basis()])
+ from sage.combinat.set_partition_ordered import OrderedSetPartitions
+ from sage.groups.perm_gps.permgroup_named import SymmetricGroup
+ self._shape = shape
+ n = sum(shape)
+ self._symgp = SymmetricGroup(n)
+ cat = ModulesWithBasis(SGA.base_ring()).FiniteDimensional()
+ tabloids = OrderedSetPartitions(n, shape)
+ CombinatorialFreeModule.__init__(self, SGA.base_ring(), tabloids,
+ category=cat, prefix='T', bracket='')
+ SymmetricGroupRepresentation.__init__(self, SGA)
- @cached_method
- def frobenius_image(self):
+ def _repr_(self):
r"""
- Return the Frobenius image of ``self``.
+ Return a string representation of ``self``.
- The Frobenius map is defined as the map to symmetric functions
+ EXAMPLES::
- .. MATH::
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SGA.tabloid_module([2,2,1])
+ Tabloid module of [2, 2, 1] over Rational Field
+ """
+ return f"Tabloid module of {self._shape} over {self.base_ring()}"
- F(\chi) = \frac{1}{n!} \sum_{w \in S_n} \chi(w) p_{\rho(w)},
+ def _ascii_art_term(self, T):
+ r"""
+ Return an ascii art representation of the term indexed by ``T``.
- where `\chi` is the character of the `S_n`-module ``self``,
- `p_{\lambda}` is the powersum symmetric function basis element
- indexed by `\lambda`, and `\rho(w)` is partition of the cycle type
- of `w`. Specifically, this map takes irreducible representations
- indexed by `\lambda` to the Schur function `s_{\lambda}`.
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: ascii_art(TM.an_element()) # indirect doctest
+ 2*T + 2*T + 3*T
+ {1, 2} {1, 2} {1, 2}
+ {3, 4} {3, 5} {4, 5}
+ {5} {4} {3}
+ """
+ # This is basically copied from CombinatorialFreeModule._ascii_art_term
+ from sage.typeset.ascii_art import AsciiArt, ascii_art
+ pref = AsciiArt([self.prefix()])
+ tab = "\n".join("{" + ", ".join(str(val) for val in sorted(row)) + "}" for row in T)
+ if not tab:
+ tab = '-'
+ r = pref * (AsciiArt([" " * len(pref)]) + ascii_art(tab))
+ r._baseline = r._h - 1
+ return r
+
+ def _unicode_art_term(self, T):
+ r"""
+ Return a unicode art representation of the term indexed by ``T``.
EXAMPLES::
- sage: s = SymmetricFunctions(QQ).s()
- sage: SM = Partition([2,2,1]).specht_module(QQ)
- sage: s(SM.frobenius_image())
- s[2, 2, 1]
- sage: SM = Partition([4,1]).specht_module(CyclotomicField(5))
- sage: s(SM.frobenius_image())
- s[4, 1]
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: unicode_art(TM.an_element()) # indirect doctest
+ 2*T + 2*T + 3*T
+ {1, 2} {1, 2} {1, 2}
+ {3, 4} {3, 5} {4, 5}
+ {5} {4} {3}
+ """
+ from sage.typeset.unicode_art import unicode_art
+ r = unicode_art(repr(self._ascii_art_term(T)))
+ r._baseline = r._h - 1
+ return r
- We verify the regular representation::
+ def _latex_term(self, T):
+ r"""
+ Return a latex representation of the term indexed by ``T``.
- sage: from sage.combinat.diagram import Diagram
- sage: D = Diagram([(0,0), (1,1), (2,2), (3,3), (4,4)])
- sage: F = s(D.specht_module(QQ).frobenius_image()); F
- s[1, 1, 1, 1, 1] + 4*s[2, 1, 1, 1] + 5*s[2, 2, 1]
- + 6*s[3, 1, 1] + 5*s[3, 2] + 4*s[4, 1] + s[5]
- sage: F == sum(StandardTableaux(la).cardinality() * s[la]
- ....: for la in Partitions(5))
- True
- sage: all(s[la] == s(la.specht_module(QQ).frobenius_image())
- ....: for n in range(1, 5) for la in Partitions(n))
- True
+ EXAMPLES::
- sage: D = Diagram([(0,0), (1,1), (1,2), (2,3), (2,4)])
- sage: SM = D.specht_module(QQ)
- sage: s(SM.frobenius_image())
- s[2, 2, 1] + s[3, 1, 1] + 2*s[3, 2] + 2*s[4, 1] + s[5]
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: latex(TM.an_element()) # indirect doctest
+ 2 T_{{\def\lr#1{\multicolumn{1}{@{\hspace{.6ex}}c@{\hspace{.6ex}}}{\raisebox{-.3ex}{$#1$}}}
+ \raisebox{-.6ex}{$\begin{array}[b]{*{2}c}\cline{1-2}
+ \lr{1}&\lr{2}\\\cline{1-2}
+ \lr{3}&\lr{4}\\\cline{1-2}
+ \lr{5}\\\cline{1-1}
+ \end{array}$}
+ }} + 2 T_{{\def\lr#1{\multicolumn{1}{@{\hspace{.6ex}}c@{\hspace{.6ex}}}{\raisebox{-.3ex}{$#1$}}}
+ \raisebox{-.6ex}{$\begin{array}[b]{*{2}c}\cline{1-2}
+ \lr{1}&\lr{2}\\\cline{1-2}
+ \lr{3}&\lr{5}\\\cline{1-2}
+ \lr{4}\\\cline{1-1}
+ \end{array}$}
+ }} + 3 T_{{\def\lr#1{\multicolumn{1}{@{\hspace{.6ex}}c@{\hspace{.6ex}}}{\raisebox{-.3ex}{$#1$}}}
+ \raisebox{-.6ex}{$\begin{array}[b]{*{2}c}\cline{1-2}
+ \lr{1}&\lr{2}\\\cline{1-2}
+ \lr{4}&\lr{5}\\\cline{1-2}
+ \lr{3}\\\cline{1-1}
+ \end{array}$}
+ }}
"""
- from sage.combinat.sf.sf import SymmetricFunctions
- BR = self._ambient.base_ring()
- p = SymmetricFunctions(BR).p()
- G = self._ambient.group()
- CCR = [(elt, elt.cycle_type()) for elt in G.conjugacy_classes_representatives()]
- return p.sum(self.representation_matrix(elt).trace() / la.centralizer_size() * p[la]
- for elt, la in CCR)
+ if not T:
+ tab = "\\emptyset"
+ else:
+ from sage.combinat.output import tex_from_array
+ A = list(map(sorted, T))
+ tab = str(tex_from_array(A))
+ tab = tab.replace("|", "")
+ return f"{self.prefix()}_{{{tab}}}"
- class Element(SubmoduleWithBasis.Element):
- def _acted_upon_(self, x, self_on_left=False):
- """
+ def _symmetric_group_action(self, osp, g):
+ r"""
+ Return the action of the symmetric group element ``g`` on the
+ ordered set partition ``osp``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: osp = TM._indices([[1,4],[3,5],[2]])
+ sage: g = SGA.group().an_element(); g
+ [5, 1, 2, 3, 4]
+ sage: TM._symmetric_group_action(osp, g)
+ [{3, 5}, {2, 4}, {1}]
+ """
+ P = self._indices
+ g = self._symgp(g)
+ return P.element_class(P, [[g(val) for val in row] for row in osp], check=False)
+
+ def specht_module(self):
+ r"""
+ Return the Specht submodule of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: TM.specht_module() is SGA.specht_module([2,2,1])
+ True
+ """
+ return SpechtModuleTableauxBasis(self)
+
+ def bilinear_form(self, u, v):
+ r"""
+ Return the natural bilinear form of ``self`` applied to ``u`` and ``v``.
+
+ The natural bilinear form is given by defining the tabloid basis
+ to be orthonormal.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module([2,2,1])
+ sage: u = TM.an_element(); u
+ 2*T[{1, 2}, {3, 4}, {5}] + 2*T[{1, 2}, {3, 5}, {4}] + 3*T[{1, 2}, {4, 5}, {3}]
+ sage: v = sum(TM.basis())
+ sage: TM.bilinear_form(u, v)
+ 7
+ sage: TM.bilinear_form(u, TM.zero())
+ 0
+ """
+ if len(v) < len(u):
+ u, v = v, u
+ R = self.base_ring()
+ return R.sum(c * v[T] for T, c in u)
+
+ class Element(CombinatorialFreeModule.Element):
+ def _acted_upon_(self, x, self_on_left):
+ r"""
Return the action of ``x`` on ``self``.
INPUT:
@@ -317,25 +775,399 @@ def _acted_upon_(self, x, self_on_left=False):
EXAMPLES::
- sage: SGA = SymmetricGroupAlgebra(QQ, 4)
- sage: SM = SGA.specht_module([3,1])
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.tabloid_module([2,2,1])
sage: SGA.an_element() * SM.an_element()
- 14*B[0] + 18*B[1] + 8*B[2]
+ 2*T[{1, 5}, {2, 3}, {4}] + 2*T[{1, 5}, {2, 4}, {3}] + 3*T[{1, 5}, {3, 4}, {2}]
+ + 12*T[{1, 2}, {3, 4}, {5}] + 15*T[{1, 2}, {3, 5}, {4}] + 15*T[{1, 2}, {4, 5}, {3}]
sage: 4 * SM.an_element()
- 8*B[0] + 8*B[1] + 12*B[2]
+ 8*T[{1, 2}, {3, 4}, {5}] + 8*T[{1, 2}, {3, 5}, {4}] + 12*T[{1, 2}, {4, 5}, {3}]
+ sage: SM.an_element() * SGA.an_element()
+ Traceback (most recent call last):
+ ...
+ TypeError: unsupported operand parent(s) for *:
+ 'Tabloid module of [2, 2, 1] over Rational Field'
+ and 'Symmetric group algebra of order 5 over Rational Field'
"""
- # Check for a scalar first
+ # first check for the base action
ret = super()._acted_upon_(x, self_on_left)
if ret is not None:
return ret
- # Check if it is in the symmetric group algebra
+
+ if self_on_left:
+ return None
P = self.parent()
- if x in P._ambient or x in P._ambient.group():
- if self_on_left: # it is only a left module
- return None
- else:
- return P.retract(P._ambient(x) * self.lift())
- return None
+ if x in P._semigroup_algebra:
+ return P.sum(c * (perm * self) for perm, c in x.monomial_coefficients().items())
+ if x in P._semigroup_algebra.indices():
+ return P.element_class(P, {P._symmetric_group_action(T, x): c
+ for T, c in self._monomial_coefficients.items()})
+
+
+class SpechtModuleTableauxBasis(SpechtModule):
+ r"""
+ A Specht module of a partition in the classical standard
+ tableau basis.
+
+ This is constructed as a `S_n`-submodule of the :class:`TabloidModule`
+ (also referred to as the standard module).
+
+ .. SEEALSO::
+
+ - :class:`SpechtModule` for the generic diagram implementation
+ constructed as a left ideal of the group algebra
+ - :class:`~sage.combinat.symmetric_group_representations.SpechtRepresentation`
+ for an implementation of the representation by matrices.
+ """
+ def __init__(self, ambient):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([2,2,1])
+ sage: TestSuite(SM).run()
+ """
+ self._diagram = ambient._shape
+ SymmetricGroupRepresentation.__init__(self, ambient._semigroup_algebra)
+
+ ambient_basis = ambient.basis()
+ tabloids = ambient_basis.keys()
+ support_order = list(tabloids)
+
+ def elt(T):
+ tab = tabloids.element_class(tabloids, list(T), check=False)
+ return ambient.sum_of_terms((ambient._symmetric_group_action(tab, sigma), sigma.sign())
+ for sigma in T.column_stabilizer())
+
+ basis = Family({T: elt(T)
+ for T in self._diagram.standard_tableaux()})
+ cat = ambient.category().Subobjects()
+ SubmoduleWithBasis.__init__(self, basis, support_order, ambient=ambient,
+ unitriangular=False, category=cat,
+ prefix='S', bracket='')
+
+ @lazy_attribute
+ def lift(self):
+ r"""
+ The lift (embedding) map from ``self`` to the ambient space.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([3, 1, 1])
+ sage: SM.lift
+ Generic morphism:
+ From: Specht module of [3, 1, 1] over Rational Field
+ To: Tabloid module of [3, 1, 1] over Rational Field
+ """
+ return self.module_morphism(self.lift_on_basis, codomain=self.ambient())
+
+ @lazy_attribute
+ def retract(self):
+ r"""
+ The retract map from the ambient space.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: X = SGA.tabloid_module([2,2,1])
+ sage: Y = X.specht_module()
+ sage: Y.retract
+ Generic morphism:
+ From: Tabloid module of [2, 2, 1] over Rational Field
+ To: Specht module of [2, 2, 1] over Rational Field
+ sage: all(Y.retract(u.lift()) == u for u in Y.basis())
+ True
+
+ sage: Y.retract(X.zero())
+ 0
+ sage: Y.retract(sum(X.basis()))
+ Traceback (most recent call last):
+ ...
+ ValueError: ... is not in the image
+ """
+ B = self.basis()
+ COB = matrix([b.lift().to_vector() for b in B]).T
+ P, L, U = COB.LU()
+ # Since U is upper triangular, the nonzero entriesm must be in the
+ # upper square portiion of the matrix
+ n = len(B)
+
+ Uinv = U.matrix_from_rows(range(n)).inverse()
+ # This is a slight abuse as the codomain should be a module with a different
+ # S_n action, but we only use it internally, so there isn't any problems
+ PLinv = (P*L).inverse()
+
+ def retraction(elt):
+ vec = PLinv * elt.to_vector(order=self._support_order)
+ if not vec:
+ return self.zero()
+ # vec is now in the image of self under U, which is
+ if max(vec.support()) >= n:
+ raise ValueError(f"{elt} is not in the image")
+ return self._from_dict(dict(zip(B.keys(), Uinv * vec[:n])))
+
+ return self._ambient.module_morphism(function=retraction, codomain=self)
+
+ def bilinear_form(self, u, v):
+ r"""
+ Return the natural bilinear form of ``self`` applied to ``u`` and ``v``.
+
+ The natural bilinear form is given by the pullback of the natural
+ bilinear form on the tabloid module (where the tabloid basis is an
+ orthonormal basis).
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([2,2,1])
+ sage: u = SM.an_element(); u
+ 3*S[[1, 2], [3, 5], [4]] + 2*S[[1, 3], [2, 5], [4]] + 2*S[[1, 4], [2, 5], [3]]
+ sage: v = sum(SM.basis())
+ sage: SM.bilinear_form(u, v)
+ 140
+ """
+ TM = self._ambient
+ return TM.bilinear_form(u.lift(), v.lift())
+
+ @cached_method
+ def gram_matrix(self):
+ r"""
+ Return the Gram matrix of the natural bilinear form of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([2,2,1])
+ sage: M = SM.gram_matrix(); M
+ [12 4 -4 -4 4]
+ [ 4 12 4 4 4]
+ [-4 4 12 4 4]
+ [-4 4 4 12 4]
+ [ 4 4 4 4 12]
+ sage: M.det() != 0
+ True
+ """
+ B = self.basis()
+ M = matrix([[self.bilinear_form(b, bp) for bp in B] for b in B])
+ M.set_immutable()
+ return M
+
+ def maximal_submodule(self):
+ """
+ Return the maximal submodule of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: U = SM.maximal_submodule()
+ sage: U.dimension()
+ 4
+ """
+ return MaximalSpechtSubmodule(self)
+
+ def simple_module(self):
+ r"""
+ Return the simple (or irreducible) `S_n`-submodule of ``self``.
+
+ .. SEEALSO::
+
+ :class:`~sage.combinat.specht_module.SimpleModule`
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: L = SM.simple_module()
+ sage: L.dimension()
+ 1
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: SM.simple_module() is SM
+ True
+ """
+ if self.base_ring().characteristic() == 0:
+ return self
+ return SimpleModule(self)
+
+
+class MaximalSpechtSubmodule(SubmoduleWithBasis, SymmetricGroupRepresentation):
+ r"""
+ The maximal submodule `U^{\lambda}` of the Specht module `S^{\lambda}`.
+
+ ALGORITHM:
+
+ We construct `U^{\lambda}` as the intersection `S \cap S^{\perp}`,
+ where `S^{\perp}` is the orthogonal complement of the Specht module `S`
+ inside of the tabloid module `T` (with respect to the natural
+ bilinear form on `T`).
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: U = SM.maximal_submodule()
+ sage: u = U.an_element(); u
+ 2*U[0] + 2*U[1]
+ sage: [p * u for p in list(SGA.basis())[:4]]
+ [2*U[0] + 2*U[1], 2*U[2] + 2*U[3], 2*U[0] + 2*U[1], U[0] + 2*U[2]]
+ sage: sum(SGA.basis()) * u
+ 0
+ """
+ def __init__(self, specht_module):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: U = SM.maximal_submodule()
+ sage: TestSuite(U).run()
+
+ sage: SM = SGA.specht_module([2,1,1,1])
+ sage: SM.maximal_submodule()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: only implemented for 3-regular partitions
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: U = SM.maximal_submodule()
+ sage: TestSuite(U).run()
+ sage: U.dimension()
+ 0
+ """
+ SymmetricGroupRepresentation.__init__(self, specht_module._semigroup_algebra)
+
+ p = specht_module.base_ring().characteristic()
+ if p == 0:
+ basis = Family([])
+ else:
+ TM = specht_module._ambient
+ if not TM._shape.is_regular(p):
+ raise NotImplementedError(f"only implemented for {p}-regular partitions")
+ TV = TM._dense_free_module()
+ SV = TV.submodule(specht_module.lift.matrix().columns())
+ basis = (SV & SV.complement()).basis()
+ basis = [specht_module.retract(TM.from_vector(b)) for b in basis]
+ basis = Family(specht_module.echelon_form(basis))
+
+ unitriangular = all(b.leading_support() == 1 for b in basis)
+ support_order = list(specht_module.basis().keys())
+ cat = specht_module.category().Subobjects()
+ SubmoduleWithBasis.__init__(self, basis, support_order, ambient=specht_module,
+ unitriangular=unitriangular, category=cat,
+ prefix='U')
+
+ def _repr_(self):
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,2])
+ sage: SM.maximal_submodule()
+ Maximal submodule of Specht module of [3, 2] over Finite Field of size 3
+ """
+ return f"Maximal submodule of {self._ambient}"
+
+ Element = SpechtModule.Element
+
+
+class SimpleModule(QuotientModuleWithBasis, SymmetricGroupRepresentation):
+ r"""
+ The simgle `S_n`-module associated with a partition `\lambda`.
+
+ The simple module `D^{\lambda}` is the quotient of the Specht module
+ `S^{\lambda}` by its :class:`maximal submodule `
+ `U^{\lambda}`.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,1,1])
+ sage: D = SM.simple_module()
+ sage: v = D.an_element(); v
+ 2*D[[[1, 3, 5], [2], [4]]] + 2*D[[[1, 4, 5], [2], [3]]]
+ sage: SGA.an_element() * v
+ 2*D[[[1, 2, 4], [3], [5]]] + 2*D[[[1, 3, 5], [2], [4]]]
+
+ We give an example on how to construct the decomposition matrix
+ (the Specht modules are a complete set of irreducible projective
+ modules) and the Cartan matrix of a symmetric group algebra::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 4)
+ sage: BM = matrix(SGA.simple_module(la).brauer_character()
+ ....: for la in Partitions(4, regular=3))
+ sage: SBT = matrix(SGA.specht_module(la).brauer_character()
+ ....: for la in Partitions(4))
+ sage: D = SBT * ~BM; D
+ [1 0 0 0]
+ [0 1 0 0]
+ [1 0 1 0]
+ [0 0 0 1]
+ [0 0 1 0]
+ sage: D.transpose() * D
+ [2 0 1 0]
+ [0 1 0 0]
+ [1 0 2 0]
+ [0 0 0 1]
+
+ We verify this against the direct computation (up to reindexing the
+ rows and columns)::
+
+ sage: SGA.cartan_invariants_matrix() # long time
+ [1 0 0 0]
+ [0 1 0 0]
+ [0 0 2 1]
+ [0 0 1 2]
+ """
+ def __init__(self, specht_module):
+ r"""
+ Initialize ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,1,1])
+ sage: D = SM.simple_module()
+ sage: TestSuite(D).run()
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([2,1,1,1])
+ sage: SM.simple_module()
+ Traceback (most recent call last):
+ ...
+ ValueError: the partition must be 3-regular
+ """
+ self._diagram = specht_module._diagram
+ p = specht_module.base_ring().characteristic()
+ if not self._diagram.is_regular(p):
+ raise ValueError(f"the partition must be {p}-regular")
+ SymmetricGroupRepresentation.__init__(self, specht_module._semigroup_algebra)
+ cat = specht_module.category()
+ QuotientModuleWithBasis.__init__(self, specht_module.maximal_submodule(), cat, prefix='D')
+
+ def _repr_(self):
+ r"""
+ Return a string representation of ``self``.
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: SM = SGA.specht_module([3,1,1])
+ sage: SM.simple_module()
+ Simple module of [3, 1, 1] over Finite Field of size 3
+ """
+ return f"Simple module of {self._diagram} over {self.base_ring()}"
+
+ Element = SpechtModule.Element
def _to_diagram(D):
@@ -358,7 +1190,6 @@ def _to_diagram(D):
"""
from sage.combinat.integer_vector import IntegerVectors
from sage.combinat.skew_partition import SkewPartitions
- from sage.combinat.partition import _Partitions
if isinstance(D, Diagram):
return D
if D in _Partitions:
@@ -527,7 +1358,8 @@ def bilinear_form(p1, p2):
p1, p2 = p2, p1
return sum(c1 * p2.get(T1, 0) for T1, c1 in p1.items() if c1)
- gram_matrix = [[bilinear_form(polytabloid(T1), polytabloid(T2)) for T1 in ST] for T2 in ST]
+ PT = {T: polytabloid(T) for T in ST}
+ gram_matrix = [[bilinear_form(PT[T1], PT[T2]) for T1 in ST] for T2 in ST]
return matrix(base_ring, gram_matrix)
diff --git a/src/sage/combinat/symmetric_group_algebra.py b/src/sage/combinat/symmetric_group_algebra.py
index 4672a53570d..b0214063e3b 100644
--- a/src/sage/combinat/symmetric_group_algebra.py
+++ b/src/sage/combinat/symmetric_group_algebra.py
@@ -1572,20 +1572,40 @@ def specht_module(self, D):
sage: SGA = SymmetricGroupAlgebra(QQ, 5)
sage: SM = SGA.specht_module(Partition([3,1,1]))
sage: SM
- Specht module of [(0, 0), (0, 1), (0, 2), (1, 0), (2, 0)] over Rational Field
- sage: s = SymmetricFunctions(QQ).s()
- sage: s(SM.frobenius_image())
+ Specht module of [3, 1, 1] over Rational Field
+ sage: SM.frobenius_image()
s[3, 1, 1]
sage: SM = SGA.specht_module([(1,1),(1,3),(2,2),(3,1),(3,2)])
sage: SM
Specht module of [(1, 1), (1, 3), (2, 2), (3, 1), (3, 2)] over Rational Field
- sage: s(SM.frobenius_image())
+ sage: SM.frobenius_image()
s[2, 2, 1] + s[3, 1, 1] + s[3, 2]
"""
from sage.combinat.specht_module import SpechtModule
return SpechtModule(self, D)
+ def tabloid_module(self, D):
+ r"""
+ Return the module of tabloids with the natural action of ``self``.
+
+ .. SEEALSO::
+
+ :class:`~sage.combinat.specht_module.TabloidModule`
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(QQ, 5)
+ sage: TM = SGA.tabloid_module(Partition([3,1,1]))
+ sage: TM
+ Tabloid module of [3, 1, 1] over Rational Field
+ sage: s = SymmetricFunctions(QQ).s()
+ sage: s(TM.frobenius_image())
+ s[3, 1, 1] + s[3, 2] + 2*s[4, 1] + s[5]
+ """
+ from sage.combinat.specht_module import TabloidModule
+ return TabloidModule(self, D)
+
def specht_module_dimension(self, D):
r"""
Return the dimension of the Specht module of ``self`` indexed by ``D``.
@@ -1603,6 +1623,29 @@ def specht_module_dimension(self, D):
span_set = specht_module_spanning_set(D, self)
return matrix(self.base_ring(), [v.to_vector() for v in span_set]).rank()
+ def simple_module(self, la):
+ r"""
+ Return the simple module of ``self`` indexed by the partition ``la``.
+
+ Over a field of characteristic `0`, this simply returns the Specht
+ module.
+
+ .. SEEALSO::
+
+ :class:`sage.combinat.specht_module.SimpleModule`
+
+ EXAMPLES::
+
+ sage: SGA = SymmetricGroupAlgebra(GF(3), 5)
+ sage: D = SGA.simple_module(Partition([3,1,1]))
+ sage: D
+ Simple module of [3, 1, 1] over Finite Field of size 3
+ sage: D.brauer_character()
+ (6, 0, -2, 0, 1)
+ """
+ from sage.combinat.specht_module import SpechtModule
+ return SpechtModule(self, la).simple_module()
+
def simple_module_dimension(self, la):
r"""
Return the dimension of the simple module of ``self`` indexed by the
@@ -2591,10 +2634,10 @@ def e(tableau, star=0):
one = QQ.one()
P = Permutation
- rd = dict((P(h), one) for h in rs)
+ rd = {P(h): one for h in rs}
sym = QSn._from_dict(rd)
- cd = dict((P(v), v.sign() * one) for v in cs)
+ cd = {P(v): QQ(v.sign()) for v in cs}
antisym = QSn._from_dict(cd)
res = QSn.right_action_product(antisym, sym)
@@ -2604,7 +2647,7 @@ def e(tableau, star=0):
# being [1] rather than [] (which seems to have its origins in
# permutation group code).
# TODO: Fix this.
- if len(tableau) == 0:
+ if not tableau:
res = QSn.one()
e_cache[t] = res
diff --git a/src/sage/crypto/lattice.py b/src/sage/crypto/lattice.py
index ce6c63f66f3..513730ff89f 100644
--- a/src/sage/crypto/lattice.py
+++ b/src/sage/crypto/lattice.py
@@ -112,10 +112,10 @@ def gen_lattice(type='modular', n=4, m=8, q=11, seed=None,
[ 0 11 0 0 0 0 0 0]
[ 0 0 11 0 0 0 0 0]
[ 0 0 0 11 0 0 0 0]
- [-2 -3 -3 4 1 0 0 0]
- [ 4 -2 -3 -3 0 1 0 0]
- [-3 4 -2 -3 0 0 1 0]
- [-3 -3 4 -2 0 0 0 1]
+ [-3 -3 -2 4 1 0 0 0]
+ [ 4 -3 -3 -2 0 1 0 0]
+ [-2 4 -3 -3 0 0 1 0]
+ [-3 -2 4 -3 0 0 0 1]
Ideal bases also work with polynomials::
@@ -125,10 +125,10 @@ def gen_lattice(type='modular', n=4, m=8, q=11, seed=None,
[ 0 11 0 0 0 0 0 0]
[ 0 0 11 0 0 0 0 0]
[ 0 0 0 11 0 0 0 0]
- [ 1 4 -3 3 1 0 0 0]
- [ 3 1 4 -3 0 1 0 0]
- [-3 3 1 4 0 0 1 0]
- [ 4 -3 3 1 0 0 0 1]
+ [-3 4 1 4 1 0 0 0]
+ [ 4 -3 4 1 0 1 0 0]
+ [ 1 4 -3 4 0 0 1 0]
+ [ 4 1 4 -3 0 0 0 1]
Cyclotomic bases with n=2^k are SWIFFT bases::
@@ -137,10 +137,10 @@ def gen_lattice(type='modular', n=4, m=8, q=11, seed=None,
[ 0 11 0 0 0 0 0 0]
[ 0 0 11 0 0 0 0 0]
[ 0 0 0 11 0 0 0 0]
- [-2 -3 -3 4 1 0 0 0]
- [-4 -2 -3 -3 0 1 0 0]
- [ 3 -4 -2 -3 0 0 1 0]
- [ 3 3 -4 -2 0 0 0 1]
+ [-3 -3 -2 4 1 0 0 0]
+ [-4 -3 -3 -2 0 1 0 0]
+ [ 2 -4 -3 -3 0 0 1 0]
+ [ 3 2 -4 -3 0 0 0 1]
Dual modular bases are related to Regev's famous public-key
encryption [Reg2005]_::
diff --git a/src/sage/crypto/lwe.py b/src/sage/crypto/lwe.py
index 25bb2a3fb47..40281916b19 100644
--- a/src/sage/crypto/lwe.py
+++ b/src/sage/crypto/lwe.py
@@ -670,7 +670,7 @@ def __init__(self, ringlwe):
sage: lwe = RingLWEConverter(RingLWE(16, 257, D, secret_dist='uniform'))
sage: set_random_seed(1337)
sage: lwe()
- ((32, 216, 3, 125, 58, 197, 171, 43), ...)
+ ((171, 197, 58, 125, 3, 216, 32, 130), ...)
"""
self.ringlwe = ringlwe
self._i = 0
@@ -686,7 +686,7 @@ def __call__(self):
sage: lwe = RingLWEConverter(RingLWE(16, 257, D, secret_dist='uniform'))
sage: set_random_seed(1337)
sage: lwe()
- ((32, 216, 3, 125, 58, 197, 171, 43), ...)
+ ((171, 197, 58, 125, 3, 216, 32, 130), ...)
"""
R_q = self.ringlwe.R_q
diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py
index 79b2fbc90ca..138e5241024 100644
--- a/src/sage/doctest/control.py
+++ b/src/sage/doctest/control.py
@@ -416,7 +416,7 @@ def __init__(self, options, args):
if options.gc:
options.timeout *= 2
if options.nthreads == 0:
- options.nthreads = int(os.getenv('SAGE_NUM_THREADS_PARALLEL',1))
+ options.nthreads = int(os.getenv('SAGE_NUM_THREADS_PARALLEL', 1))
if options.failed and not (args or options.new):
# If the user doesn't specify any files then we rerun all failed files.
options.all = True
@@ -450,15 +450,11 @@ def __init__(self, options, args):
options.hide.discard('all')
from sage.features.all import all_features
feature_names = {f.name for f in all_features() if not f.is_standard()}
- from sage.doctest.external import external_software
- feature_names.difference_update(external_software)
options.hide = options.hide.union(feature_names)
if 'optional' in options.hide:
options.hide.discard('optional')
from sage.features.all import all_features
feature_names = {f.name for f in all_features() if f.is_optional()}
- from sage.doctest.external import external_software
- feature_names.difference_update(external_software)
options.hide = options.hide.union(feature_names)
options.disabled_optional = set()
@@ -1008,7 +1004,7 @@ def expand():
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
for dir in list(dirs):
- if dir[0] == "." or skipdir(os.path.join(root,dir)):
+ if dir[0] == "." or skipdir(os.path.join(root, dir)):
dirs.remove(dir)
for file in files:
if not skipfile(os.path.join(root, file),
@@ -1345,9 +1341,9 @@ def run_val_gdb(self, testing=False):
flags = os.getenv("SAGE_MEMCHECK_FLAGS")
if flags is None:
flags = "--leak-resolution=high --leak-check=full --num-callers=25 "
- flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE,"valgrind", "pyalloc.supp"))
- flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE,"valgrind", "sage.supp"))
- flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE,"valgrind", "sage-additional.supp"))
+ flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE, "valgrind", "pyalloc.supp"))
+ flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE, "valgrind", "sage.supp"))
+ flags += '''--suppressions="%s" ''' % (os.path.join(SAGE_EXTCODE, "valgrind", "sage-additional.supp"))
elif opt.massif:
toolname = "massif"
flags = os.getenv("SAGE_MASSIF_FLAGS", "--depth=6 ")
@@ -1362,7 +1358,7 @@ def run_val_gdb(self, testing=False):
if opt.omega:
toolname = "omega"
if "%s" in flags:
- flags %= toolname + ".%p" # replace %s with toolname
+ flags %= toolname + ".%p" # replace %s with toolname
cmd += flags + sage_cmd
sys.stdout.flush()
@@ -1489,6 +1485,25 @@ def run(self):
cumulative wall time: ... seconds
Features detected...
0
+
+ Test *Features that have been hidden* message::
+
+ sage: DC.run() # optional - meataxe
+ Running doctests with ID ...
+ Using --optional=sage
+ Features to be detected: ...
+ Doctesting 1 file.
+ sage -t ....py
+ [4 tests, ... s]
+ ----------------------------------------------------------------------
+ All tests passed!
+ ----------------------------------------------------------------------
+ Total time for all tests: ... seconds
+ cpu time: ... seconds
+ cumulative wall time: ... seconds
+ Features detected...
+ Features that have been hidden: ...meataxe...
+ 0
"""
opt = self.options
L = (opt.gdb, opt.lldb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega)
@@ -1516,10 +1531,10 @@ def run(self):
pass
try:
ref = subprocess.check_output(["git",
- "--git-dir=" + SAGE_ROOT_GIT,
- "describe",
- "--always",
- "--dirty"])
+ "--git-dir=" + SAGE_ROOT_GIT,
+ "describe",
+ "--always",
+ "--dirty"])
ref = ref.decode('utf-8')
self.log("Git ref: " + ref, end="")
except subprocess.CalledProcessError:
@@ -1537,12 +1552,11 @@ def run(self):
pass
else:
f = available_software._features[i]
- if f.is_present():
- f.hide()
- self.options.hidden_features.add(f)
- for g in f.joined_features():
- if g.name in self.options.optional:
- self.options.optional.discard(g.name)
+ f.hide()
+ self.options.hidden_features.add(f)
+ for g in f.joined_features():
+ if g.name in self.options.optional:
+ self.options.optional.discard(g.name)
for o in self.options.disabled_optional:
try:
@@ -1553,8 +1567,6 @@ def run(self):
available_software._seen[i] = -1
self.log("Features to be detected: " + ','.join(available_software.detectable()))
- if self.options.hidden_features:
- self.log("Hidden features: " + ','.join([f.name for f in self.options.hidden_features]))
if self.options.probe:
self.log("Features to be probed: " + ('all' if self.options.probe is True
else ','.join(self.options.probe)))
@@ -1564,11 +1576,11 @@ def run(self):
self.sort_sources()
self.run_doctests()
- for f in self.options.hidden_features:
- f.unhide()
-
self.log("Features detected for doctesting: "
+ ','.join(available_software.seen()))
+ if self.options.hidden_features:
+ features_hidden = [f.name for f in self.options.hidden_features if f.unhide()]
+ self.log("Features that have been hidden: " + ','.join(features_hidden))
self.cleanup()
return self.reporter.error_status
diff --git a/src/sage/env.py b/src/sage/env.py
index da9bad48da9..a6a99ca9303 100644
--- a/src/sage/env.py
+++ b/src/sage/env.py
@@ -220,6 +220,7 @@ def var(key: str, *fallbacks: Optional[str], force: bool = False) -> Optional[st
MAXIMA_FAS = var("MAXIMA_FAS")
KENZO_FAS = var("KENZO_FAS")
SAGE_NAUTY_BINS_PREFIX = var("SAGE_NAUTY_BINS_PREFIX", "")
+SAGE_ECMBIN = var("SAGE_ECMBIN")
RUBIKS_BINS_PREFIX = var("RUBIKS_BINS_PREFIX", "")
FOURTITWO_HILBERT = var("FOURTITWO_HILBERT")
FOURTITWO_MARKOV = var("FOURTITWO_MARKOV")
diff --git a/src/sage/features/__init__.py b/src/sage/features/__init__.py
index ea8fd6bdb05..6af9413e55a 100644
--- a/src/sage/features/__init__.py
+++ b/src/sage/features/__init__.py
@@ -28,7 +28,7 @@
Here we test whether the grape GAP package is available::
sage: from sage.features.gap import GapPackage
- sage: GapPackage("grape", spkg="gap_packages").is_present() # optional - gap_packages
+ sage: GapPackage("grape", spkg="gap_packages").is_present() # optional - gap_package_grape
FeatureTestResult('gap_package_grape', True)
Note that a :class:`FeatureTestResult` acts like a bool in most contexts::
@@ -158,6 +158,12 @@ def __init__(self, name, spkg=None, url=None, description=None, type='optional')
self._hidden = False
self._type = type
+ # For multiprocessing of doctests, the data self._num_hidings should be
+ # shared among subprocesses. Thus we use the Value class from the
+ # multiprocessing module (cf. self._seen of class AvailableSoftware)
+ from multiprocessing import Value
+ self._num_hidings = Value('i', 0)
+
try:
from sage.misc.package import spkg_type
except ImportError: # may have been surgically removed in a downstream distribution
@@ -182,7 +188,7 @@ def is_present(self):
EXAMPLES::
sage: from sage.features.gap import GapPackage
- sage: GapPackage("grape", spkg="gap_packages").is_present() # optional - gap_packages
+ sage: GapPackage("grape", spkg="gap_packages").is_present() # optional - gap_package_grape
FeatureTestResult('gap_package_grape', True)
sage: GapPackage("NOT_A_PACKAGE", spkg="gap_packages").is_present()
FeatureTestResult('gap_package_NOT_A_PACKAGE', False)
@@ -205,8 +211,6 @@ def is_present(self):
sage: TestFeature("other").is_present()
FeatureTestResult('other', True)
"""
- if self._hidden:
- return FeatureTestResult(self, False, reason="Feature `{name}` is hidden.".format(name=self.name))
# We do not use @cached_method here because we wish to use
# Feature early in the build system of sagelib.
if self._cache_is_present is None:
@@ -214,6 +218,14 @@ def is_present(self):
if not isinstance(res, FeatureTestResult):
res = FeatureTestResult(self, res)
self._cache_is_present = res
+
+ if self._hidden:
+ if self._num_hidings.value > 0:
+ self._num_hidings.value += 1
+ elif self._cache_is_present:
+ self._num_hidings.value = 1
+ return FeatureTestResult(self, False, reason="Feature `{name}` is hidden.".format(name=self.name))
+
return self._cache_is_present
def _is_present(self):
@@ -381,7 +393,8 @@ def hide(self):
Feature `benzene` is hidden.
Use method `unhide` to make it available again.
- sage: Benzene().unhide()
+ sage: Benzene().unhide() # optional - benzene, needs sage.graphs
+ 1
sage: len(list(graphs.fusenes(2))) # optional - benzene, needs sage.graphs
1
"""
@@ -389,32 +402,25 @@ def hide(self):
def unhide(self):
r"""
- Revert what :meth:`hide` does.
-
- EXAMPLES:
+ Revert what :meth:`hide` did.
- PolyCyclic is an optional GAP package. The following test
- fails if it is hidden, regardless of whether it is installed
- or not::
+ OUTPUT: The number of events a present feature has been hidden.
- sage: from sage.features.gap import GapPackage
- sage: Polycyclic = GapPackage("polycyclic", spkg="gap_packages")
- sage: Polycyclic.hide()
- sage: libgap(AbelianGroup(3, [0,3,4], names="abc")) # needs sage.libs.gap # optional - gap_packages_polycyclic
- Traceback (most recent call last):
- ...
- FeatureNotPresentError: gap_package_polycyclic is not available.
- Feature `gap_package_polycyclic` is hidden.
- Use method `unhide` to make it available again.
-
- After unhiding the feature, the test should pass again if PolyCyclic
- is installed and loaded::
+ EXAMPLES:
- sage: Polycyclic.unhide()
- sage: libgap(AbelianGroup(3, [0,3,4], names="abc")) # needs sage.libs.gap # optional - gap_packages_polycyclic
- Pcp-group with orders [ 0, 3, 4 ]
+ sage: from sage.features.sagemath import sage__plot
+ sage: sage__plot().hide()
+ sage: sage__plot().is_present()
+ FeatureTestResult('sage.plot', False)
+ sage: sage__plot().unhide() # needs sage.plot
+ 1
+ sage: sage__plot().is_present() # needs sage.plot
+ FeatureTestResult('sage.plot', True)
"""
+ num_hidings = self._num_hidings.value
+ self._num_hidings.value = 0
self._hidden = False
+ return int(num_hidings)
class FeatureNotPresentError(RuntimeError):
@@ -802,7 +808,7 @@ class StaticFile(FileFeature):
To install no_such_file...you can try to run...sage -i some_spkg...
Further installation instructions might be available at http://rand.om.
"""
- def __init__(self, name, filename, *, search_path=None, **kwds):
+ def __init__(self, name, filename, *, search_path=None, type='optional', **kwds):
r"""
TESTS::
@@ -817,7 +823,7 @@ def __init__(self, name, filename, *, search_path=None, **kwds):
'/bin/sh'
"""
- Feature.__init__(self, name, **kwds)
+ Feature.__init__(self, name, type=type, **kwds)
self.filename = filename
if search_path is None:
self.search_path = [SAGE_SHARE]
diff --git a/src/sage/features/databases.py b/src/sage/features/databases.py
index cbb5de36ca0..844ed54de17 100644
--- a/src/sage/features/databases.py
+++ b/src/sage/features/databases.py
@@ -52,12 +52,12 @@ class DatabaseCremona(StaticFile):
EXAMPLES::
sage: from sage.features.databases import DatabaseCremona
- sage: DatabaseCremona('cremona_mini').is_present()
+ sage: DatabaseCremona('cremona_mini', type='standard').is_present()
FeatureTestResult('database_cremona_mini_ellcurve', True)
sage: DatabaseCremona().is_present() # optional - database_cremona_ellcurve
FeatureTestResult('database_cremona_ellcurve', True)
"""
- def __init__(self, name="cremona"):
+ def __init__(self, name="cremona", spkg="database_cremona_ellcurve", type='optional'):
r"""
TESTS::
@@ -290,7 +290,7 @@ def __init__(self, name='polytopes_db'):
def all_features():
return [PythonModule('conway_polynomials', spkg='conway_polynomials', type='standard'),
DatabaseCremona(),
- DatabaseCremona('cremona_mini'),
+ DatabaseCremona('cremona_mini', type='standard'),
DatabaseEllcurves(),
DatabaseGraphs(),
DatabaseJones(),
diff --git a/src/sage/features/ecm.py b/src/sage/features/ecm.py
new file mode 100644
index 00000000000..79a1e77918f
--- /dev/null
+++ b/src/sage/features/ecm.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+r"""
+Feature for testing the presence of ``ecm`` or ``gmp-ecm``
+"""
+# ****************************************************************************
+# Copyright (C) 2032 Dima Pasechnik
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+# https://www.gnu.org/licenses/
+# ****************************************************************************
+
+from . import Executable
+from sage.env import SAGE_ECMBIN
+
+
+class Ecm(Executable):
+ r"""
+ A :class:`~sage.features.Feature` describing the presence of :ref:`GMP-ECM `.
+
+ EXAMPLES::
+
+ sage: from sage.features.ecm import Ecm
+ sage: Ecm().is_present()
+ FeatureTestResult('ecm', True)
+ """
+ def __init__(self):
+ r"""
+ TESTS::
+
+ sage: from sage.features.ecm import Ecm
+ sage: isinstance(Ecm(), Ecm)
+ True
+ """
+ Executable.__init__(self, name="ecm", executable=SAGE_ECMBIN,
+ spkg="ecm", type="standard")
+
+
+def all_features():
+ return [Ecm()]
diff --git a/src/sage/features/gap.py b/src/sage/features/gap.py
index df5545e9c07..314ba1cc514 100644
--- a/src/sage/features/gap.py
+++ b/src/sage/features/gap.py
@@ -53,7 +53,7 @@ def _is_present(self):
EXAMPLES::
sage: from sage.features.gap import GapPackage
- sage: GapPackage("grape", spkg="gap_packages")._is_present() # optional - gap_packages
+ sage: GapPackage("grape", spkg="gap_packages")._is_present() # optional - gap_package_grape
FeatureTestResult('gap_package_grape', True)
"""
try:
diff --git a/src/sage/features/join_feature.py b/src/sage/features/join_feature.py
index 6360eec1576..24c6583c123 100644
--- a/src/sage/features/join_feature.py
+++ b/src/sage/features/join_feature.py
@@ -152,7 +152,9 @@ def hide(self):
def unhide(self):
r"""
- Revert what :meth:`hide` does.
+ Revert what :meth:`hide` did.
+
+ OUTPUT: The number of events a present feature has been hidden.
EXAMPLES::
@@ -165,11 +167,14 @@ def unhide(self):
FeatureTestResult('sage.groups.perm_gps.permgroup', False)
sage: f.unhide()
+ 4
sage: f.is_present() # optional sage.groups
FeatureTestResult('sage.groups', True)
sage: f._features[0].is_present() # optional sage.groups
FeatureTestResult('sage.groups.perm_gps.permgroup', True)
"""
+ num_hidings = 0
for f in self._features:
- f.unhide()
- super().unhide()
+ num_hidings += f.unhide()
+ num_hidings += super().unhide()
+ return num_hidings
diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py
index f7eac6cd39c..729b001a202 100644
--- a/src/sage/graphs/graph.py
+++ b/src/sage/graphs/graph.py
@@ -10134,7 +10134,7 @@ def bipartite_double(self, extended=False):
from sage.graphs.tutte_polynomial import tutte_polynomial
from sage.graphs.lovasz_theta import lovasz_theta
from sage.graphs.partial_cube import is_partial_cube
- from sage.graphs.orientations import strong_orientations_iterator, random_orientation
+ from sage.graphs.orientations import strong_orientations_iterator, random_orientation, acyclic_orientations
from sage.graphs.connectivity import bridges, cleave, spqr_tree
from sage.graphs.connectivity import is_triconnected
from sage.graphs.comparability import is_comparability
@@ -10180,6 +10180,7 @@ def bipartite_double(self, extended=False):
"lovasz_theta" : "Leftovers",
"strong_orientations_iterator" : "Connectivity, orientations, trees",
"random_orientation" : "Connectivity, orientations, trees",
+ "acyclic_orientations" : "Connectivity, orientations, trees",
"bridges" : "Connectivity, orientations, trees",
"cleave" : "Connectivity, orientations, trees",
"spqr_tree" : "Connectivity, orientations, trees",
diff --git a/src/sage/graphs/orientations.py b/src/sage/graphs/orientations.py
index 7b7fa8681fc..1ecb283ba2a 100644
--- a/src/sage/graphs/orientations.py
+++ b/src/sage/graphs/orientations.py
@@ -41,6 +41,263 @@
from sage.graphs.digraph import DiGraph
+def acyclic_orientations(G):
+ r"""
+ Return an iterator over all acyclic orientations of an undirected graph `G`.
+
+ ALGORITHM:
+
+ The algorithm is based on [Sq1998]_.
+ It presents an efficient algorithm for listing the acyclic orientations of a
+ graph. The algorithm is shown to require O(n) time per acyclic orientation
+ generated, making it the most efficient known algorithm for generating acyclic
+ orientations.
+
+ The function uses a recursive approach to generate acyclic orientations of the
+ graph. It reorders the vertices and edges of the graph, creating a new graph
+ with updated labels. Then, it iteratively generates acyclic orientations by
+ considering subsets of edges and checking whether they form upsets in a
+ corresponding poset.
+
+ INPUT:
+
+ - ``G`` -- an undirected graph.
+
+ OUTPUT:
+
+ - An iterator over all acyclic orientations of the input graph.
+
+ .. NOTE::
+
+ The function assumes that the input graph is undirected and the edges are unlabelled.
+
+ EXAMPLES:
+
+ To count number acyclic orientations for a graph::
+
+ sage: g = Graph([(0, 3), (0, 4), (3, 4), (1, 3), (1, 2), (2, 3), (2, 4)])
+ sage: it = g.acyclic_orientations()
+ sage: len(list(it))
+ 54
+
+ Test for arbitary vertex labels::
+
+ sage: g_str = Graph([('abc', 'def'), ('ghi', 'def'), ('xyz', 'abc'), ('xyz', 'uvw'), ('uvw', 'abc'), ('uvw', 'ghi')])
+ sage: it = g_str.acyclic_orientations()
+ sage: len(list(it))
+ 42
+
+ TESTS:
+
+ To count the number of acyclic orientations for a graph with 0 vertices::
+
+ sage: list(Graph().acyclic_orientations())
+ []
+
+ To count the number of acyclic orientations for a graph with 1 vertex::
+
+ sage: list(Graph(1).acyclic_orientations())
+ []
+
+ To count the number of acyclic orientations for a graph with 2 vertices::
+
+ sage: list(Graph(2).acyclic_orientations())
+ []
+
+ Acyclic orientations of a complete graph::
+
+ sage: g = graphs.CompleteGraph(5)
+ sage: it = g.acyclic_orientations()
+ sage: len(list(it))
+ 120
+
+ Graph with one edge::
+
+ sage: list(Graph([(0, 1)]).acyclic_orientations())
+ [Digraph on 2 vertices, Digraph on 2 vertices]
+
+ Graph with two edges::
+
+ sage: len(list(Graph([(0, 1), (1, 2)]).acyclic_orientations()))
+ 4
+
+ Cycle graph::
+
+ sage: len(list(Graph([(0, 1), (1, 2), (2, 0)]).acyclic_orientations()))
+ 6
+
+ """
+ if not G.size():
+ # A graph without edge cannot be oriented
+ return
+
+ from sage.rings.infinity import Infinity
+ from sage.combinat.subset import Subsets
+
+ def reorder_vertices(G):
+ n = G.order()
+ ko = n
+ k = n
+ G_copy = G.copy()
+ vertex_labels = {v: None for v in G_copy.vertices()}
+
+ while G_copy.size() > 0:
+ min_val = float('inf')
+ uv = None
+ for u, v, _ in G_copy.edges():
+ du = G_copy.degree(u)
+ dv = G_copy.degree(v)
+ val = (du + dv) / (du * dv)
+ if val < min_val:
+ min_val = val
+ uv = (u, v)
+
+ if uv:
+ u, v = uv
+ vertex_labels[u] = ko
+ vertex_labels[v] = ko - 1
+ G_copy.delete_vertex(u)
+ G_copy.delete_vertex(v)
+ ko -= 2
+
+ if G_copy.size() == 0:
+ break
+
+ for vertex, label in vertex_labels.items():
+ if label is None:
+ vertex_labels[vertex] = ko
+ ko -= 1
+
+ return vertex_labels
+
+ def order_edges(G, vertex_labels):
+ n = len(vertex_labels)
+ m = 1
+ edge_labels = {}
+
+ for j in range(2, n + 1):
+ for i in range(1, j):
+ if G.has_edge(i, j):
+ edge_labels[(i, j)] = m
+ m += 1
+
+ return edge_labels
+
+ def is_upset_of_poset(Poset, subset, keys):
+ for (u, v) in subset:
+ for (w, x) in keys:
+ if (Poset[(u, v), (w, x)] == 1 and (w, x) not in subset):
+ return False
+ return True
+
+ def generate_orientations(globO, starting_of_Ek, m, k, keys):
+ # Creating a poset
+ Poset = {}
+ for i in range(starting_of_Ek, m - 1):
+ for j in range(starting_of_Ek, m - 1):
+ u, v = keys[i]
+ w, x = keys[j]
+ Poset[(u, v), (w, x)] = 0
+
+ # Create a new graph to determine reachable vertices
+ new_G = DiGraph()
+
+ # Process vertices up to starting_of_Ek
+ new_G.add_edges([(v, u) if globO[(u, v)] == 1 else (u, v) for u, v in keys[:starting_of_Ek]])
+
+ # Process vertices starting from starting_of_Ek
+ new_G.add_vertices([u for u, _ in keys[starting_of_Ek:]] + [v for _, v in keys[starting_of_Ek:]])
+
+ if (globO[(k-1, k)] == 1):
+ new_G.add_edge(k, k - 1)
+ else:
+ new_G.add_edge(k-1, k)
+
+ for i in range(starting_of_Ek, m - 1):
+ for j in range(starting_of_Ek, m - 1):
+ u, v = keys[i]
+ w, x = keys[j]
+ # w should be reachable from u and v should be reachable from x
+ if w in new_G.depth_first_search(u) and v in new_G.depth_first_search(x):
+ Poset[(u, v), (w, x)] = 1
+
+ # For each subset of the base set of E_k, check if it is an upset or not
+ upsets = []
+ for subset in Subsets(keys[starting_of_Ek:m-1]):
+ if (is_upset_of_poset(Poset, subset, keys[starting_of_Ek:m-1])):
+ upsets.append(list(subset))
+
+ for upset in upsets:
+ for i in range(starting_of_Ek, m - 1):
+ u, v = keys[i]
+ if (u, v) in upset:
+ globO[(u, v)] = 1
+ else:
+ globO[(u, v)] = 0
+
+ yield globO.copy()
+
+ def helper(G, globO, m, k):
+ keys = list(globO.keys())
+ keys = keys[0:m]
+
+ if m <= 0:
+ yield {}
+ return
+
+ starting_of_Ek = 0
+ for (u, v) in keys:
+ if u >= k - 1 or v >= k - 1:
+ break
+ else:
+ starting_of_Ek += 1
+
+ # s is the size of E_k
+ s = m - 1 - starting_of_Ek
+
+ # Recursively generate acyclic orientations
+ orientations_G_small = helper(G, globO, starting_of_Ek, k - 2)
+
+ # For each orientation of G_k-2, yield acyclic orientations
+ for alpha in orientations_G_small:
+ for (u, v) in alpha:
+ globO[(u, v)] = alpha[(u, v)]
+
+ # Orienting H_k as 1
+ globO[(k-1, k)] = 1
+ yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
+
+ # Orienting H_k as 0
+ globO[(k-1, k)] = 0
+ yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
+
+ # Reorder vertices based on the logic in reorder_vertices function
+ vertex_labels = reorder_vertices(G)
+
+ # Create a new graph with updated vertex labels using SageMath, Assuming the graph edges are unlabelled
+ new_G = G.relabel(perm=vertex_labels, inplace=False)
+
+ G = new_G
+
+ # Order the edges based on the logic in order_edges function
+ edge_labels = order_edges(G, vertex_labels)
+
+ # Create globO array
+ globO = {uv: 0 for uv in edge_labels}
+
+ m = len(edge_labels)
+ k = len(vertex_labels)
+ orientations = helper(G, globO, m, k)
+
+ # Create a mapping between original and new vertex labels
+ reverse_vertex_labels = {label: vertex for vertex, label in vertex_labels.items()}
+
+ # Iterate over acyclic orientations and create relabeled graphs
+ for orientation in orientations:
+ relabeled_graph = DiGraph([(reverse_vertex_labels[u], reverse_vertex_labels[v], label) for (u, v), label in orientation.items()])
+ yield relabeled_graph
+
+
def strong_orientations_iterator(G):
r"""
Return an iterator over all strong orientations of a graph `G`.
diff --git a/src/sage/groups/generic.py b/src/sage/groups/generic.py
index 206e8561e5a..84d3cd89436 100644
--- a/src/sage/groups/generic.py
+++ b/src/sage/groups/generic.py
@@ -119,7 +119,6 @@
from sage.arith.misc import integer_ceil, integer_floor, xlcm
from sage.arith.srange import xsrange
-from sage.misc.functional import log
from sage.misc.misc_c import prod
import sage.rings.integer_ring as integer_ring
import sage.rings.integer
@@ -171,9 +170,11 @@ def multiple(a, n, operation='*', identity=None, inverse=None, op=None):
sage: E = EllipticCurve('389a1')
sage: P = E(-1,1)
sage: multiple(P, 10, '+')
- (645656132358737542773209599489/22817025904944891235367494656 : 525532176124281192881231818644174845702936831/3446581505217248068297884384990762467229696 : 1)
+ (645656132358737542773209599489/22817025904944891235367494656 :
+ 525532176124281192881231818644174845702936831/3446581505217248068297884384990762467229696 : 1)
sage: multiple(P, -10, '+')
- (645656132358737542773209599489/22817025904944891235367494656 : -528978757629498440949529703029165608170166527/3446581505217248068297884384990762467229696 : 1)
+ (645656132358737542773209599489/22817025904944891235367494656 :
+ -528978757629498440949529703029165608170166527/3446581505217248068297884384990762467229696 : 1)
"""
from operator import inv, mul, neg, add
@@ -182,7 +183,7 @@ def multiple(a, n, operation='*', identity=None, inverse=None, op=None):
inverse = inv
op = mul
elif operation in addition_names:
- identity = a.parent()(0)
+ identity = a.parent().zero()
inverse = neg
op = add
else:
@@ -329,18 +330,19 @@ def __init__(self, P, n, P0=None, indexed=False, operation='+', op=None):
self.op = mul
elif operation in addition_names:
if P0 is None:
- P0 = P.parent()(0)
+ P0 = P.parent().zero()
self.op = add
else:
- self.op = op
if P0 is None:
raise ValueError("P0 must be supplied when operation is neither addition nor multiplication")
if op is None:
raise ValueError("op() must both be supplied when operation is neither addition nor multiplication")
+ self.op = op
self.P = copy(P)
self.Q = copy(P0)
- assert self.P is not None and self.Q is not None
+ if self.P is None or self.Q is None:
+ raise ValueError("P and Q must not be None")
self.i = 0
self.bound = n
self.indexed = indexed
@@ -442,7 +444,7 @@ def bsgs(a, b, bounds, operation='*', identity=None, inverse=None, op=None):
This will return a multiple of the order of P::
- sage: bsgs(P, P.parent()(0), Hasse_bounds(F.order()), operation='+') # needs sage.rings.finite_rings sage.schemes
+ sage: bsgs(P, P.parent().zero(), Hasse_bounds(F.order()), operation='+') # needs sage.rings.finite_rings sage.schemes
69327408
AUTHOR:
@@ -458,7 +460,8 @@ def bsgs(a, b, bounds, operation='*', identity=None, inverse=None, op=None):
inverse = inv
op = mul
elif operation in addition_names:
- identity = a.parent()(0)
+ # Should this be replaced with .zero()? With an extra AttributeError handler?
+ identity = a.parent().zero()
inverse = neg
op = add
else:
@@ -469,7 +472,7 @@ def bsgs(a, b, bounds, operation='*', identity=None, inverse=None, op=None):
if lb < 0 or ub < lb:
raise ValueError("bsgs() requires 0<=lb<=ub")
- if a.is_zero() and not b.is_zero():
+ if a == identity and b != identity:
raise ValueError("no solution in bsgs()")
ran = 1 + ub - lb # the length of the interval
@@ -479,7 +482,7 @@ def bsgs(a, b, bounds, operation='*', identity=None, inverse=None, op=None):
if ran < 30: # use simple search for small ranges
d = c
-# for i,d in multiples(a,ran,c,indexed=True,operation=operation):
+ # for i,d in multiples(a,ran,c,indexed=True,operation=operation):
for i0 in range(ran):
i = lb + i0
if identity == d: # identity == b^(-1)*a^i, so return i
@@ -1008,7 +1011,7 @@ def discrete_log_lambda(a, base, bounds, operation='*', identity=None, inverse=N
This will return a multiple of the order of P::
- sage: discrete_log_lambda(P.parent()(0), P, Hasse_bounds(F.order()), # needs sage.rings.finite_rings sage.schemes
+ sage: discrete_log_lambda(P.parent().zero(), P, Hasse_bounds(F.order()), # needs sage.rings.finite_rings sage.schemes
....: operation='+')
69327408
@@ -1187,11 +1190,13 @@ def linear_relation(P, Q, operation='+', identity=None, inverse=None, op=None):
def order_from_multiple(P, m, plist=None, factorization=None, check=True,
- operation='+'):
+ operation='+', identity=None, inverse=None, op=None):
r"""
Generic function to find order of a group element given a multiple
of its order.
+ See :meth:`bsgs` for full explanation of the inputs.
+
INPUT:
- ``P`` -- a Sage object which is a group element;
@@ -1201,9 +1206,12 @@ def order_from_multiple(P, m, plist=None, factorization=None, check=True,
really is a multiple of the order;
- ``factorization`` -- the factorization of ``m``, or ``None`` in which
case this function will need to factor ``m``;
- - ``plist`` -- a list of the prime factors of ``m``, or ``None`` - kept for compatibility only,
+ - ``plist`` -- a list of the prime factors of ``m``, or ``None``. Kept for compatibility only,
prefer the use of ``factorization``;
- - ``operation`` -- string: ``'+'`` (default) or ``'*'``.
+ - ``operation`` -- string: ``'+'`` (default), ``'*'`` or ``None``;
+ - ``identity`` -- the identity element of the group;
+ - ``inverse()`` -- function of 1 argument ``x``, returning inverse of ``x``;
+ - ``op()`` - function of 2 arguments ``x``, ``y`` returning ``x*y`` in the group.
.. note::
@@ -1255,16 +1263,25 @@ def order_from_multiple(P, m, plist=None, factorization=None, check=True,
if operation in multiplication_names:
identity = P.parent().one()
elif operation in addition_names:
- identity = P.parent()(0)
+ identity = P.parent().zero()
else:
- raise ValueError("unknown group operation")
+ if identity is None or inverse is None or op is None:
+ raise ValueError("identity, inverse and operation must all be specified")
+
+ def _multiple(A, B):
+ return multiple(A,
+ B,
+ operation=operation,
+ identity=identity,
+ inverse=inverse,
+ op=op)
if P == identity:
return Z.one()
M = Z(m)
- if check:
- assert multiple(P, M, operation=operation) == identity
+ if check and _multiple(P, M) != identity:
+ raise ValueError(f"The order of P(={P}) does not divide {M}")
if factorization:
F = factorization
@@ -1293,7 +1310,7 @@ def _order_from_multiple_helper(Q, L, S):
p, e = L[0]
e0 = 0
while (Q != identity) and (e0 < e - 1):
- Q = multiple(Q, p, operation=operation)
+ Q = _multiple(Q, p)
e0 += 1
if Q != identity:
e0 += 1
@@ -1312,12 +1329,8 @@ def _order_from_multiple_helper(Q, L, S):
L2 = L[k:]
# recursive calls
o1 = _order_from_multiple_helper(
- multiple(Q, prod([p**e for p, e in L2]), operation),
- L1,
- sum_left)
- o2 = _order_from_multiple_helper(multiple(Q, o1, operation),
- L2,
- S - sum_left)
+ _multiple(Q, prod([p**e for p, e in L2])), L1, sum_left)
+ o2 = _order_from_multiple_helper(_multiple(Q, o1), L2, S - sum_left)
return o1 * o2
return _order_from_multiple_helper(P, F, sage.functions.log.log(float(M)))
@@ -1389,7 +1402,7 @@ def order_from_bounds(P, bounds, d=None, operation='+',
identity = P.parent().one()
elif operation in addition_names:
op = add
- identity = P.parent()(0)
+ identity = P.parent().zero()
else:
if op is None:
raise ValueError("operation and identity must be specified")
@@ -1410,6 +1423,7 @@ def order_from_bounds(P, bounds, d=None, operation='+',
return order_from_multiple(P, m, operation=operation, check=False)
+
def has_order(P, n, operation='+'):
r"""
Generic function to test if a group element `P` has order
@@ -1510,8 +1524,8 @@ def _rec(Q, fn):
fl = fn[::2]
fr = fn[1::2]
- l = prod(p**k for p,k in fl)
- r = prod(p**k for p,k in fr)
+ l = prod(p**k for p, k in fl)
+ r = prod(p**k for p, k in fr)
L, R = mult(Q, r), mult(Q, l)
return _rec(L, fl) and _rec(R, fr)
@@ -1581,14 +1595,14 @@ def merge_points(P1, P2, operation='+',
identity = g1.parent().one()
elif operation in addition_names:
op = add
- identity = g1.parent()(0)
+ identity = g1.parent().zero()
else:
if op is None:
raise ValueError("operation and identity must be specified")
if check:
- assert multiple(g1, n1, operation=operation) == identity
- assert multiple(g2, n2, operation=operation) == identity
+ if multiple(g1, n1, operation=operation) != identity or multiple(g2, n2, operation=operation) != identity:
+ raise ValueError("the orders provided do not divide the orders of the points provided")
# trivial cases
if n1.divides(n2):
diff --git a/src/sage/interfaces/ecm.py b/src/sage/interfaces/ecm.py
index a2c882643c4..ae1379861f2 100644
--- a/src/sage/interfaces/ecm.py
+++ b/src/sage/interfaces/ecm.py
@@ -55,6 +55,7 @@
from sage.structure.sage_object import SageObject
from sage.rings.integer_ring import ZZ
+from sage.env import SAGE_ECMBIN
class ECM(SageObject):
@@ -182,7 +183,7 @@ def __init__(self, B1=10, B2=None, **kwds):
self._cmd = self._make_cmd(B1, B2, kwds)
def _make_cmd(self, B1, B2, kwds):
- ecm = ['ecm']
+ ecm = [SAGE_ECMBIN]
options = []
for x, v in kwds.items():
if v is False:
diff --git a/src/sage/misc/randstate.pyx b/src/sage/misc/randstate.pyx
index b918b153883..756722d0b6a 100644
--- a/src/sage/misc/randstate.pyx
+++ b/src/sage/misc/randstate.pyx
@@ -56,22 +56,22 @@ results of these random number generators reproducible. ::
sage: set_random_seed(0)
sage: print(rtest())
- (303, -0.266166246380421, 1/6, (1,2), [ 0, 1, 1, 0, 0 ], 265625921, 79302, 0.2450652680687958)
+ (303, -0.266166246380421, 1/2*x^2 - 1/95*x - 1/2, (1,3), [ 1, 0, 0, 1, 1 ], 265625921, 5842, 0.9661911734708414)
sage: set_random_seed(1)
sage: print(rtest())
- (978, 0.0557699430711638, -1/8*x^2 - 1/2*x + 1/2, (1,2,3), [ 1, 0, 0, 0, 1 ], 807447831, 23865, 0.6170498912488264)
+ (978, 0.0557699430711638, -3*x^2 - 1/12, (1,2), [ 0, 0, 1, 1, 0 ], 807447831, 29982, 0.8335077654199736)
sage: set_random_seed(2)
sage: print(rtest())
- (207, -0.0141049486533456, 0, (1,3)(4,5), [ 1, 0, 1, 1, 1 ], 1642898426, 16190, 0.9343331114872127)
+ (207, -0.0141049486533456, 4*x^2 + 1/2, (1,2)(4,5), [ 1, 0, 0, 1, 1 ], 1642898426, 41662, 0.19982565117278328)
sage: set_random_seed(0)
sage: print(rtest())
- (303, -0.266166246380421, 1/6, (1,2), [ 0, 1, 1, 0, 0 ], 265625921, 79302, 0.2450652680687958)
+ (303, -0.266166246380421, 1/2*x^2 - 1/95*x - 1/2, (1,3), [ 1, 0, 0, 1, 1 ], 265625921, 5842, 0.9661911734708414)
sage: set_random_seed(1)
sage: print(rtest())
- (978, 0.0557699430711638, -1/8*x^2 - 1/2*x + 1/2, (1,2,3), [ 1, 0, 0, 0, 1 ], 807447831, 23865, 0.6170498912488264)
+ (978, 0.0557699430711638, -3*x^2 - 1/12, (1,2), [ 0, 0, 1, 1, 0 ], 807447831, 29982, 0.8335077654199736)
sage: set_random_seed(2)
sage: print(rtest())
- (207, -0.0141049486533456, 0, (1,3)(4,5), [ 1, 0, 1, 1, 1 ], 1642898426, 16190, 0.9343331114872127)
+ (207, -0.0141049486533456, 4*x^2 + 1/2, (1,2)(4,5), [ 1, 0, 0, 1, 1 ], 1642898426, 41662, 0.19982565117278328)
Once we've set the random number seed, we can check what seed was used.
(This is not the current random number state; it does not change when
@@ -81,7 +81,7 @@ random numbers are generated.) ::
sage: initial_seed()
12345
sage: print(rtest())
- (720, -0.612180244315804, 0, (1,3), [ 1, 0, 1, 1, 0 ], 1911581957, 65175, 0.8043027951758298)
+ (720, -0.612180244315804, x^2 - x, (1,2,3), [ 1, 0, 0, 0, 1 ], 1911581957, 27093, 0.9205331599518184)
sage: initial_seed()
12345
@@ -216,9 +216,9 @@ that you get without intervening ``with seed``. ::
sage: set_random_seed(0)
sage: r1 = rtest(); print(r1)
- (303, -0.266166246380421, 1/6, (1,2), [ 0, 1, 1, 0, 0 ], 265625921, 79302, 0.2450652680687958)
+ (303, -0.266166246380421, 1/2*x^2 - 1/95*x - 1/2, (1,3), [ 1, 0, 0, 1, 1 ], 265625921, 5842, 0.9661911734708414)
sage: r2 = rtest(); print(r2)
- (443, 0.185001351421963, -2, (1,3), [ 0, 0, 1, 1, 0 ], 53231108, 8171, 0.28363811590618193)
+ (105, 0.642309615982449, -x^2 - x - 6, (1,3)(4,5), [ 1, 0, 0, 0, 1 ], 53231108, 77132, 0.001767155077382232)
We get slightly different results with an intervening ``with seed``. ::
@@ -226,9 +226,9 @@ We get slightly different results with an intervening ``with seed``. ::
sage: r1 == rtest()
True
sage: with seed(1): rtest()
- (978, 0.0557699430711638, -1/8*x^2 - 1/2*x + 1/2, (1,2,3), [ 1, 0, 0, 0, 1 ], 807447831, 23865, 0.6170498912488264)
+ (978, 0.0557699430711638, -3*x^2 - 1/12, (1,2), [ 0, 0, 1, 1, 0 ], 807447831, 29982, 0.8335077654199736)
sage: r2m = rtest(); r2m
- (443, 0.185001351421963, -2, (1,3), [ 0, 0, 1, 1, 0 ], 53231108, 51295, 0.28363811590618193)
+ (105, 0.642309615982449, -x^2 - x - 6, (1,3)(4,5), [ 1, 0, 0, 0, 1 ], 53231108, 40267, 0.001767155077382232)
sage: r2m == r2
False
@@ -245,8 +245,8 @@ case, as we see in this example::
sage: with seed(1):
....: print(rtest())
....: print(rtest())
- (978, 0.0557699430711638, -1/8*x^2 - 1/2*x + 1/2, (1,2,3), [ 1, 0, 0, 0, 1 ], 807447831, 23865, 0.6170498912488264)
- (181, 0.607995392046754, -x + 1/2, (2,3)(4,5), [ 1, 0, 0, 1, 1 ], 1010791326, 9693, 0.5691716786307407)
+ (978, 0.0557699430711638, -3*x^2 - 1/12, (1,2), [ 0, 0, 1, 1, 0 ], 807447831, 29982, 0.8335077654199736)
+ (138, -0.0404945051288503, 2*x - 24, (1,2,3), [ 1, 1, 0, 1, 1 ], 1010791326, 91360, 0.0033332230808060803)
sage: r2m == rtest()
True
@@ -258,7 +258,7 @@ NTL random numbers were generated inside the ``with seed``.
True
sage: with seed(1):
....: rtest()
- (978, 0.0557699430711638, -1/8*x^2 - 1/2*x + 1/2, (1,2,3), [ 1, 0, 0, 0, 1 ], 807447831, 23865, 0.6170498912488264)
+ (978, 0.0557699430711638, -3*x^2 - 1/12, (1,2), [ 0, 0, 1, 1, 0 ], 807447831, 29982, 0.8335077654199736)
sage: r2m == rtest()
True
diff --git a/src/sage/modular/quasimodform/element.py b/src/sage/modular/quasimodform/element.py
index 622152a9711..dd45fca7713 100644
--- a/src/sage/modular/quasimodform/element.py
+++ b/src/sage/modular/quasimodform/element.py
@@ -31,16 +31,20 @@
class QuasiModularFormsElement(ModuleElement):
r"""
- A quasimodular forms ring element. Such an element is describbed by SageMath
- as a polynomial
+ A quasimodular forms ring element. Such an element is described by
+ SageMath as a polynomial
.. MATH::
- f_0 + f_1 E_2 + f_2 E_2^2 + \cdots + f_m E_2^m
+ F = f_0 + f_1 E_2 + f_2 E_2^2 + \cdots + f_m E_2^m
where each `f_i` a graded modular form element
(see :class:`~sage.modular.modform.element.GradedModularFormElement`)
+ For an integer `k`, we say that `F` is homogeneous of weight `k` if
+ it lies in an homogeneous component of degree `k` of the graded ring
+ of quasimodular forms.
+
EXAMPLES::
sage: QM = QuasiModularForms(1)
@@ -295,6 +299,45 @@ def __bool__(self):
"""
return bool(self._polynomial)
+ def depth(self):
+ r"""
+ Return the depth of this quasimodular form.
+
+ Note that the quasimodular form must be homogeneous of weight
+ `k`. Recall that the *depth* is the integer `p` such that
+
+ .. MATH::
+
+ f = f_0 + f_1 E_2 + \cdots + f_p E_2^p,
+
+ where `f_i` is a modular form of weight `k - 2i` and `f_p` is
+ nonzero.
+
+ EXAMPLES::
+
+ sage: QM = QuasiModularForms(1)
+ sage: E2, E4, E6 = QM.gens()
+ sage: E2.depth()
+ 1
+ sage: F = E4^2 + E6*E2 + E4*E2^2 + E2^4
+ sage: F.depth()
+ 4
+ sage: QM(7/11).depth()
+ 0
+
+ TESTS::
+
+ sage: QM = QuasiModularForms(1)
+ sage: (QM.0 + QM.1).depth()
+ Traceback (most recent call last):
+ ...
+ ValueError: the given graded quasiform is not an homogeneous element
+ """
+ if not self.is_homogeneous():
+ raise ValueError("the given graded quasiform is not an "
+ "homogeneous element")
+ return self._polynomial.degree()
+
def is_zero(self):
r"""
Return whether the given quasimodular form is zero.
diff --git a/src/sage/modular/quasimodform/ring.py b/src/sage/modular/quasimodform/ring.py
index ee939a182df..53fcea616bd 100644
--- a/src/sage/modular/quasimodform/ring.py
+++ b/src/sage/modular/quasimodform/ring.py
@@ -779,3 +779,49 @@ def from_polynomial(self, polynomial):
raise ValueError("the number of variables (%s) of the given polynomial cannot exceed the number of generators (%s) of the quasimodular forms ring" % (nb_var, self.ngens()))
gens_dict = {poly_parent.gen(i):self.gen(i) for i in range(0, nb_var)}
return self(polynomial.subs(gens_dict))
+
+ def basis_of_weight(self, weight):
+ r"""
+ Return a basis of elements generating the subspace of the given
+ weight.
+
+ INPUT:
+
+ - ``weight`` (integer) -- the weight of the subspace
+
+ OUTPUT:
+
+ A list of quasimodular forms of the given weight.
+
+ EXAMPLES::
+
+ sage: QM = QuasiModularForms(1)
+ sage: QM.basis_of_weight(12)
+ [q - 24*q^2 + 252*q^3 - 1472*q^4 + 4830*q^5 + O(q^6),
+ 1 + 65520/691*q + 134250480/691*q^2 + 11606736960/691*q^3 + 274945048560/691*q^4 + 3199218815520/691*q^5 + O(q^6),
+ 1 - 288*q - 129168*q^2 - 1927296*q^3 + 65152656*q^4 + 1535768640*q^5 + O(q^6),
+ 1 + 432*q + 39312*q^2 - 1711296*q^3 - 14159664*q^4 + 317412000*q^5 + O(q^6),
+ 1 - 576*q + 21168*q^2 + 308736*q^3 - 15034608*q^4 - 39208320*q^5 + O(q^6),
+ 1 + 144*q - 17712*q^2 + 524736*q^3 - 2279088*q^4 - 79760160*q^5 + O(q^6),
+ 1 - 144*q + 8208*q^2 - 225216*q^3 + 2634192*q^4 + 1488672*q^5 + O(q^6)]
+ sage: QM = QuasiModularForms(Gamma1(3))
+ sage: QM.basis_of_weight(3)
+ [1 + 54*q^2 + 72*q^3 + 432*q^5 + O(q^6),
+ q + 3*q^2 + 9*q^3 + 13*q^4 + 24*q^5 + O(q^6)]
+ sage: QM.basis_of_weight(5)
+ [1 - 90*q^2 - 240*q^3 - 3744*q^5 + O(q^6),
+ q + 15*q^2 + 81*q^3 + 241*q^4 + 624*q^5 + O(q^6),
+ 1 - 24*q - 18*q^2 - 1320*q^3 - 5784*q^4 - 10080*q^5 + O(q^6),
+ q - 21*q^2 - 135*q^3 - 515*q^4 - 1392*q^5 + O(q^6)]
+ """
+ basis = []
+ E2 = self.weight_2_eisenstein_series()
+ M = self.__modular_forms_subring
+ E2_pow = self.one()
+ for j in range(weight//2):
+ basis += [f*E2_pow for f
+ in M.modular_forms_of_weight(weight - 2*j).basis()]
+ E2_pow *= E2
+ if not weight%2:
+ basis.append(E2_pow)
+ return basis
diff --git a/src/sage/modular/quatalg/brandt.py b/src/sage/modular/quatalg/brandt.py
index 985cfe72b5f..4c46a481095 100644
--- a/src/sage/modular/quatalg/brandt.py
+++ b/src/sage/modular/quatalg/brandt.py
@@ -93,7 +93,7 @@
if there exists an element `\alpha \in I \overline{J}` such
`N(\alpha)=N(I)N(J)`.
-``is_equivalent(I,J)`` returns true if `I` and `J` are equivalent. This
+``is_right_equivalent(I,J)`` returns true if `I` and `J` are equivalent. This
method first compares the theta series of `I` and `J`. If they are the
same, it computes the theta series of the lattice `I\overline(J)`. It
returns true if the `n^{th}` coefficient of this series is nonzero
@@ -1193,7 +1193,7 @@ def _compute_hecke_matrix_directly(self, n, B=None, sparse=False):
T[r, v[0]] += 1
else:
for i in v:
- if C[i].is_equivalent(J, 0):
+ if C[i].is_right_equivalent(J, 0):
T[r, i] += 1
break
return T
@@ -1325,7 +1325,7 @@ def right_ideals(self, B=None):
sage: B = BrandtModule(1009)
sage: Is = B.right_ideals()
sage: n = len(Is)
- sage: prod(not Is[i].is_equivalent(Is[j]) for i in range(n) for j in range(i))
+ sage: prod(not Is[i].is_right_equivalent(Is[j]) for i in range(n) for j in range(i))
1
"""
p = self._smallest_good_prime()
@@ -1353,7 +1353,7 @@ def right_ideals(self, B=None):
J_theta = tuple(J.theta_series_vector(B))
if J_theta in ideals_theta:
for K in ideals_theta[J_theta]:
- if J.is_equivalent(K, 0):
+ if J.is_right_equivalent(K, 0):
is_new = False
break
if is_new:
diff --git a/src/sage/modules/filtered_vector_space.py b/src/sage/modules/filtered_vector_space.py
index d6a1d6237a6..68bbff12c8d 100644
--- a/src/sage/modules/filtered_vector_space.py
+++ b/src/sage/modules/filtered_vector_space.py
@@ -806,7 +806,7 @@ def _repr_field_name(self):
return 'RR'
from sage.categories.finite_fields import FiniteFields
if self.base_ring() in FiniteFields():
- return 'GF({0})'.format(len(self.base_ring()))
+ return 'GF({})'.format(len(self.base_ring()))
else:
raise NotImplementedError()
diff --git a/src/sage/modules/fp_graded/free_element.py b/src/sage/modules/fp_graded/free_element.py
index 232a7956d0e..8dc11e7764a 100755
--- a/src/sage/modules/fp_graded/free_element.py
+++ b/src/sage/modules/fp_graded/free_element.py
@@ -193,7 +193,7 @@ def _lmul_(self, a):
Sq(1,1,1)*x0 + Sq(1,1,1)*y0 + Sq(5,1)*z3,
Sq(3,2)*z3]
"""
- return self.parent()((a * c for c in self.dense_coefficient_list()))
+ return self.parent()(a * c for c in self.dense_coefficient_list())
@cached_method
def vector_presentation(self):
diff --git a/src/sage/modules/fp_graded/morphism.py b/src/sage/modules/fp_graded/morphism.py
index 47f74fd49e5..92c2753bddf 100755
--- a/src/sage/modules/fp_graded/morphism.py
+++ b/src/sage/modules/fp_graded/morphism.py
@@ -1747,7 +1747,7 @@ def _resolve_kernel(self, top_dim=None, verbose=False):
if not kernel_n:
continue
- generator_degrees = tuple((x.degree() for x in F_.generators()))
+ generator_degrees = tuple(x.degree() for x in F_.generators())
if j.is_zero():
# The map j is not onto in degree `n` of the kernel.
@@ -1875,7 +1875,7 @@ def _resolve_image(self, top_dim=None, verbose=False):
if image_n.dimension() == 0:
continue
- generator_degrees = tuple((x.degree() for x in F_.generators()))
+ generator_degrees = tuple(x.degree() for x in F_.generators())
if j.is_zero():
# The map j is not onto in degree `n` of the image.
new_generator_degrees = image_n.rank() * (n,)
diff --git a/src/sage/modules/free_module.py b/src/sage/modules/free_module.py
index fd3c2735838..867f42bf93b 100644
--- a/src/sage/modules/free_module.py
+++ b/src/sage/modules/free_module.py
@@ -276,7 +276,7 @@ def create_object(self, version, key):
"of left/right and both sided modules, so be careful.\n"
"It's also not guaranteed that all multiplications are\n"
"done from the right side.")
- # raise TypeError("The base_ring must be a commutative ring.")
+ # raise TypeError("the base_ring must be a commutative ring")
if not sparse and isinstance(base_ring, sage.rings.abc.RealDoubleField):
return RealDoubleVectorSpace_class(rank)
@@ -294,7 +294,7 @@ def create_object(self, version, key):
return FreeModule_ambient_pid(base_ring, rank, sparse=sparse)
if (isinstance(base_ring, sage.rings.abc.Order)
- and base_ring.is_maximal() and base_ring.class_number() == 1):
+ and base_ring.is_maximal() and base_ring.class_number() == 1):
return FreeModule_ambient_pid(base_ring, rank, sparse=sparse)
if isinstance(base_ring, IntegralDomain) or base_ring in IntegralDomains():
@@ -305,6 +305,7 @@ def create_object(self, version, key):
FreeModuleFactory_with_standard_basis = FreeModuleFactory("FreeModule")
+
def FreeModule(base_ring, rank_or_basis_keys=None, sparse=False, inner_product_matrix=None, *,
with_basis='standard', rank=None, basis_keys=None, **args):
r"""
@@ -538,7 +539,7 @@ def FreeModule(base_ring, rank_or_basis_keys=None, sparse=False, inner_product_m
elif with_basis == 'standard':
if rank is not None:
return FreeModuleFactory_with_standard_basis(base_ring, rank, sparse,
- inner_product_matrix, **args)
+ inner_product_matrix, **args)
else:
if inner_product_matrix is not None:
raise NotImplementedError
@@ -547,6 +548,7 @@ def FreeModule(base_ring, rank_or_basis_keys=None, sparse=False, inner_product_m
else:
raise NotImplementedError
+
def VectorSpace(K, dimension_or_basis_keys=None, sparse=False, inner_product_matrix=None, *,
with_basis='standard', dimension=None, basis_keys=None, **args):
"""
@@ -583,6 +585,7 @@ def VectorSpace(K, dimension_or_basis_keys=None, sparse=False, inner_product_mat
with_basis=with_basis, rank=dimension, basis_keys=basis_keys,
**args)
+
def span(gens, base_ring=None, check=True, already_echelonized=False):
r"""
Return the span of the vectors in ``gens`` using scalars from ``base_ring``.
@@ -773,6 +776,7 @@ def span(gens, base_ring=None, check=True, already_echelonized=False):
return M.span(gens=gens, base_ring=base_ring, check=check,
already_echelonized=already_echelonized)
+
def basis_seq(V, vecs):
"""
This converts a list vecs of vectors in V to a Sequence of
@@ -2002,8 +2006,9 @@ def construction(self):
(VectorFunctor, Multivariate Polynomial Ring in x0, x1, x2 over Rational Field)
"""
from sage.categories.pushout import VectorFunctor
- if hasattr(self,'_inner_product_matrix'):
- return VectorFunctor(self.rank(), self.is_sparse(),self.inner_product_matrix()), self.base_ring()
+ if hasattr(self, '_inner_product_matrix'):
+ return VectorFunctor(self.rank(), self.is_sparse(),
+ self.inner_product_matrix()), self.base_ring()
return VectorFunctor(self.rank(), self.is_sparse()), self.base_ring()
# FIXME: what's the level of generality of FreeModuleHomspace?
@@ -2169,10 +2174,9 @@ def _element_constructor_(self, x, coerce=True, copy=True, check=True):
sage: N((0,0,0,1), check=False) in N
True
"""
- if (isinstance(x, (int, sage.rings.integer.Integer)) and
- x == 0):
+ if (isinstance(x, (int, sage.rings.integer.Integer)) and x == 0):
return self.zero_vector()
- elif isinstance(x, free_module_element.FreeModuleElement):
+ if isinstance(x, free_module_element.FreeModuleElement):
if x.parent() is self:
if copy:
return x.__copy__()
@@ -2326,7 +2330,7 @@ def is_submodule(self, other):
pass
from sage.modules.quotient_module import FreeModule_ambient_field_quotient
if isinstance(other, FreeModule_ambient_field_quotient):
- #if the relations agree we continue with the covers.
+ # if the relations agree we continue with the covers.
if isinstance(self, FreeModule_ambient_field_quotient):
if other.relations() != self.relations():
return False
@@ -2438,23 +2442,24 @@ def __iter__(self):
# Aleksei Udovenko, adapted by Lorenz Panny to order by 1-norm
# primarily and by max-norm secondarily.
def aux(length, norm, max_):
- if not 0 <= norm <= length*max_:
+ if not 0 <= norm <= length * max_:
return # there are no such vectors
if norm == max_ == 0:
- yield (0,)*length
+ yield (0,) * length
return
for pos in range(length):
- for lnorm in range(norm-max_+1):
+ for lnorm in range(norm - max_ + 1):
for lmax in range(max_):
for left in aux(pos, lnorm, lmax):
- for rmax in range(max_+1):
- for right in aux(length-1-pos, norm-max_-lnorm, rmax):
+ for rmax in range(max_ + 1):
+ for right in aux(length - 1 - pos,
+ norm - max_ - lnorm, rmax):
for mid in (+max_, -max_):
yield left + (mid,) + right
n = len(G)
for norm in itertools.count(0):
- mm = (norm + n-1) // n
- for max_ in range(mm, norm+1):
+ mm = (norm + n - 1) // n
+ for max_ in range(mm, norm + 1):
for vec in aux(n, norm, max_):
yield self.linear_combination_of_basis(vec)
assert False # should loop forever
@@ -2620,8 +2625,9 @@ def basis_matrix(self, ring=None):
A = self.__basis_matrix
except AttributeError:
MAT = sage.matrix.matrix_space.MatrixSpace(self.coordinate_ring(),
- len(self.basis()), self.degree(),
- sparse=self.is_sparse())
+ len(self.basis()),
+ self.degree(),
+ sparse=self.is_sparse())
if self.is_ambient():
A = MAT.identity_matrix()
else:
@@ -3018,10 +3024,10 @@ def gram_matrix(self):
else:
if self._gram_matrix is None:
B = self.basis_matrix()
- self._gram_matrix = B*B.transpose()
+ self._gram_matrix = B * B.transpose()
return self._gram_matrix
- def has_user_basis(self):
+ def has_user_basis(self) -> bool:
"""
Return ``True`` if the basis of this free module is
specified by the user, as opposed to being the default echelon
@@ -3505,7 +3511,7 @@ def scale(self, other):
return self.zero_submodule()
if other == 1 or other == -1:
return self
- return self.span([v*other for v in self.basis()])
+ return self.span([v * other for v in self.basis()])
def __radd__(self, other):
"""
@@ -4121,12 +4127,12 @@ def span_of_basis(self, basis, base_ring=None, check=True, already_echelonized=F
M = self.change_ring(base_ring)
except TypeError:
raise ValueError("Argument base_ring (= %s) is not compatible " % base_ring +
- "with the base ring (= %s)." % self.base_ring())
+ "with the base ring (= %s)." % self.base_ring())
try:
return M.span_of_basis(basis)
except TypeError:
raise ValueError("Argument gens (= %s) is not compatible " % basis +
- "with base_ring (= %s)." % base_ring)
+ "with base_ring (= %s)." % base_ring)
def submodule_with_basis(self, basis, check=True, already_echelonized=False):
r"""
@@ -4755,7 +4761,7 @@ def subspaces(self, dim):
b = self.basis_matrix()
from sage.matrix.echelon_matrix import reduced_echelon_matrix_iterator
for m in reduced_echelon_matrix_iterator(self.base_ring(), dim, self.dimension(), self.is_sparse(), copy=False):
- yield self.subspace((m*b).rows())
+ yield self.subspace((m * b).rows())
def subspace_with_basis(self, gens, check=True, already_echelonized=False):
"""
@@ -5354,7 +5360,9 @@ def __init__(self, base_ring, rank, sparse=False, coordinate_ring=None, category
True
"""
FreeModule_generic.__init__(self, base_ring, rank=rank,
- degree=rank, sparse=sparse, coordinate_ring=coordinate_ring, category=category)
+ degree=rank, sparse=sparse,
+ coordinate_ring=coordinate_ring,
+ category=category)
def __hash__(self):
"""
@@ -5399,14 +5407,14 @@ def _coerce_map_from_(self, M):
return None
if isinstance(M, FreeModule_ambient):
if (self.base_ring().has_coerce_map_from(M.base_ring()) and
- self.rank() == M.rank()):
+ self.rank() == M.rank()):
# We could return M.hom(self.basis(), self), but the
# complexity of this is quadratic in space and time,
# since it constructs a matrix.
return True
elif isinstance(M, Submodule_free_ambient):
if (self.base_ring().has_coerce_map_from(M.base_ring()) and
- self.rank() == M.degree()):
+ self.rank() == M.degree()):
return True
return super()._coerce_map_from_(M)
@@ -5544,7 +5552,7 @@ def _echelon_matrix_richcmp(self, other, op):
if isinstance(other, FreeModule_ambient):
if (isinstance(other, FreeModule_ambient_field_quotient) or
isinstance(self, FreeModule_ambient_field_quotient)):
- return richcmp(self,other,op)
+ return richcmp(self, other, op)
lx = self.rank()
rx = other.rank()
@@ -5559,10 +5567,10 @@ def _echelon_matrix_richcmp(self, other, op):
if self._inner_product_is_dot_product() and other._inner_product_is_dot_product():
return rich_to_bool(op, 0)
else:
- #this only affects free_quadratic_modules
+ # this only affects free_quadratic_modules
lx = self.inner_product_matrix()
rx = other.inner_product_matrix()
- return richcmp(lx,rx,op)
+ return richcmp(lx, rx, op)
try:
if lx.is_subring(rx):
@@ -6234,9 +6242,11 @@ def __init__(self, base_ring, rank, sparse=False, coordinate_ring=None, category
"""
FreeModule_ambient_domain.__init__(self, base_ring=base_ring,
- rank=rank, sparse=sparse, coordinate_ring=coordinate_ring, category=category)
+ rank=rank, sparse=sparse,
+ coordinate_ring=coordinate_ring,
+ category=category)
- def _repr_(self):
+ def _repr_(self) -> str:
"""
The printing representation of self.
@@ -6402,18 +6412,18 @@ def _element_constructor_(self, e, *args, **kwds):
class RealDoubleVectorSpace_class(FreeModule_ambient_field):
- def __init__(self,n):
- FreeModule_ambient_field.__init__(self,sage.rings.real_double.RDF,n)
+ def __init__(self, n):
+ FreeModule_ambient_field.__init__(self, sage.rings.real_double.RDF, n)
- def coordinates(self,v):
+ def coordinates(self, v):
return v
class ComplexDoubleVectorSpace_class(FreeModule_ambient_field):
- def __init__(self,n):
- FreeModule_ambient_field.__init__(self,sage.rings.complex_double.CDF,n)
+ def __init__(self, n):
+ FreeModule_ambient_field.__init__(self, sage.rings.complex_double.CDF, n)
- def coordinates(self,v):
+ def coordinates(self, v):
return v
@@ -6472,8 +6482,9 @@ class FreeModule_submodule_with_basis_pid(FreeModule_generic_pid):
[ 4 5 6]
"""
def __init__(self, ambient, basis, check=True,
- echelonize=False, echelonized_basis=None, already_echelonized=False,
- category=None):
+ echelonize=False, echelonized_basis=None,
+ already_echelonized=False,
+ category=None):
r"""
See :class:`FreeModule_submodule_with_basis_pid` for documentation.
@@ -6731,11 +6742,11 @@ def _echelonized_basis(self, ambient, basis):
MAT = sage.matrix.matrix_space.MatrixSpace(
ambient.base_ring(), len(basis), ambient.degree(), sparse=ambient.is_sparse())
if d != 1:
- basis = [x*d for x in basis]
+ basis = [x * d for x in basis]
A = MAT(basis)
E = A.echelon_form()
if d != 1:
- E = E.matrix_over_field()*(~d) # divide out denominator
+ E = E.matrix_over_field() * (~d) # divide out denominator
r = E.rank()
if r < E.nrows():
E = E.matrix_from_rows(range(r))
@@ -6766,7 +6777,7 @@ def _denominator(self, B):
d = B[0].denominator()
from sage.arith.functions import lcm
for x in B[1:]:
- d = lcm(d,x.denominator())
+ d = lcm(d, x.denominator())
return d
def _repr_(self):
@@ -7094,10 +7105,11 @@ def user_to_echelon_matrix(self):
if self.base_ring().is_field():
self.__user_to_echelon_matrix = self._user_to_rref_matrix()
else:
- rows = sum([self.echelon_coordinates(b,check=False) for b in self.basis()], [])
+ rows = sum([self.echelon_coordinates(b, check=False)
+ for b in self.basis()], [])
M = sage.matrix.matrix_space.MatrixSpace(self.base_ring().fraction_field(),
- self.dimension(),
- sparse=self.is_sparse())
+ self.dimension(),
+ sparse=self.is_sparse())
self.__user_to_echelon_matrix = M(rows)
return self.__user_to_echelon_matrix
@@ -7603,8 +7615,9 @@ def __init__(self, ambient, gens, check=True, already_echelonized=False,
[0 3 6]
"""
FreeModule_submodule_with_basis_pid.__init__(self, ambient, basis=gens,
- echelonize=True, already_echelonized=already_echelonized,
- category=category)
+ echelonize=True,
+ already_echelonized=already_echelonized,
+ category=category)
def _repr_(self):
"""
@@ -7743,8 +7756,9 @@ class FreeModule_submodule_with_basis_field(FreeModule_generic_field, FreeModule
sage: TestSuite(W).run()
"""
def __init__(self, ambient, basis, check=True,
- echelonize=False, echelonized_basis=None, already_echelonized=False,
- category=None):
+ echelonize=False, echelonized_basis=None,
+ already_echelonized=False,
+ category=None):
"""
Create a vector space with given basis.
@@ -7831,12 +7845,12 @@ def _repr_(self):
"""
if self.is_sparse():
return "Sparse vector space of degree %s and dimension %s over %s\n" % (
- self.degree(), self.dimension(), self.base_field()) + \
- "User basis matrix:\n%r" % self.basis_matrix()
- else:
- return "Vector space of degree %s and dimension %s over %s\n" % (
- self.degree(), self.dimension(), self.base_field()) + \
- "User basis matrix:\n%r" % self.basis_matrix()
+ self.degree(), self.dimension(), self.base_field()) + \
+ "User basis matrix:\n%r" % self.basis_matrix()
+
+ return "Vector space of degree %s and dimension %s over %s\n" % (
+ self.degree(), self.dimension(), self.base_field()) + \
+ "User basis matrix:\n%r" % self.basis_matrix()
def _denominator(self, B):
"""
@@ -7958,11 +7972,13 @@ def __init__(self, ambient, gens, check=True, already_echelonized=False, categor
"""
if is_FreeModule(gens):
gens = gens.gens()
- FreeModule_submodule_with_basis_field.__init__(self, ambient, basis=gens, check=check,
- echelonize=not already_echelonized, already_echelonized=already_echelonized,
- category=category)
+ FreeModule_submodule_with_basis_field.__init__(self, ambient,
+ basis=gens, check=check,
+ echelonize=not already_echelonized,
+ already_echelonized=already_echelonized,
+ category=category)
- def _repr_(self):
+ def _repr_(self) -> str:
"""
The default printing representation of self.
@@ -8256,7 +8272,7 @@ def element_class(R, is_sparse):
@richcmp_method
-class EchelonMatrixKey():
+class EchelonMatrixKey:
r"""
A total ordering on free modules for sorting.
diff --git a/src/sage/modules/free_module_element.pyx b/src/sage/modules/free_module_element.pyx
index ff43d239493..2f47c924d38 100644
--- a/src/sage/modules/free_module_element.pyx
+++ b/src/sage/modules/free_module_element.pyx
@@ -4148,6 +4148,87 @@ cdef class FreeModuleElement(Vector): # abstract base class
nintegrate=nintegral
+ def concatenate(self, other, *, ring=None):
+ r"""
+ Return the result of concatenating this vector with a sequence
+ of elements given by another iterable.
+
+ If the optional keyword argument ``ring`` is passed, this method
+ will return a vector over the specified ring (or fail). If no
+ base ring is given, the base ring is determined automatically by
+ the :func:`vector` constructor.
+
+ EXAMPLES::
+
+ sage: v = vector([1, 2, 3])
+ sage: w = vector([4, 5])
+ sage: v.concatenate(w)
+ (1, 2, 3, 4, 5)
+ sage: v.parent()
+ Ambient free module of rank 3 over the principal ideal domain Integer Ring
+ sage: w.parent()
+ Ambient free module of rank 2 over the principal ideal domain Integer Ring
+ sage: v.concatenate(w).parent()
+ Ambient free module of rank 5 over the principal ideal domain Integer Ring
+
+ Forcing a base ring is possible using the ``ring`` argument::
+
+ sage: v.concatenate(w, ring=QQ)
+ (1, 2, 3, 4, 5)
+ sage: v.concatenate(w, ring=QQ).parent()
+ Vector space of dimension 5 over Rational Field
+
+ ::
+
+ sage: v.concatenate(w, ring=Zmod(3))
+ (1, 2, 0, 1, 2)
+
+ The method accepts arbitrary iterables of elements which can
+ be coerced to a common base ring::
+
+ sage: v.concatenate(range(4,8))
+ (1, 2, 3, 4, 5, 6, 7)
+ sage: v.concatenate(range(4,8)).parent()
+ Ambient free module of rank 7 over the principal ideal domain Integer Ring
+
+ ::
+
+ sage: w2 = [4, QQbar(-5).sqrt()]
+ sage: v.concatenate(w2)
+ (1, 2, 3, 4, 2.236...*I)
+ sage: v.concatenate(w2).parent()
+ Vector space of dimension 5 over Algebraic Field
+ sage: w2 = vector(w2)
+ sage: v.concatenate(w2)
+ (1, 2, 3, 4, 2.236...*I)
+ sage: v.concatenate(w2).parent()
+ Vector space of dimension 5 over Algebraic Field
+
+ ::
+
+ sage: w2 = polygen(QQ)^4 + 5
+ sage: v.concatenate(w2)
+ (1, 2, 3, 5, 0, 0, 0, 1)
+ sage: v.concatenate(w2).parent()
+ Vector space of dimension 8 over Rational Field
+ sage: v.concatenate(w2, ring=ZZ)
+ (1, 2, 3, 5, 0, 0, 0, 1)
+ sage: v.concatenate(w2, ring=ZZ).parent()
+ Ambient free module of rank 8 over the principal ideal domain Integer Ring
+
+ ::
+
+ sage: v.concatenate(GF(9).gens())
+ (1, 2, 0, z2)
+ sage: v.concatenate(GF(9).gens()).parent()
+ Vector space of dimension 4 over Finite Field in z2 of size 3^2
+ """
+ from itertools import chain
+ coeffs = chain(self, other)
+ if ring is not None:
+ return vector(ring, coeffs)
+ return vector(coeffs)
+
#############################################
# Generic dense element
#############################################
diff --git a/src/sage/modules/matrix_morphism.py b/src/sage/modules/matrix_morphism.py
index 4624465f8dc..9cd429ef0bc 100644
--- a/src/sage/modules/matrix_morphism.py
+++ b/src/sage/modules/matrix_morphism.py
@@ -71,6 +71,7 @@ def is_MatrixMorphism(x):
"""
return isinstance(x, MatrixMorphism_abstract)
+
class MatrixMorphism_abstract(sage.categories.morphism.Morphism):
def __init__(self, parent, side='left'):
"""
@@ -188,7 +189,7 @@ def _call_(self, x):
if parent(x) is not self.domain():
x = self.domain()(x)
except TypeError:
- raise TypeError("%s must be coercible into %s" % (x,self.domain()))
+ raise TypeError("%s must be coercible into %s" % (x, self.domain()))
if self.domain().is_ambient():
x = x.element()
else:
@@ -860,12 +861,12 @@ def decomposition(self, *args, **kwds):
]
"""
if not self.is_endomorphism():
- raise ArithmeticError("Matrix morphism must be an endomorphism.")
+ raise ArithmeticError("matrix morphism must be an endomorphism")
D = self.domain()
if self.side() == "left":
- E = self.matrix().decomposition(*args,**kwds)
+ E = self.matrix().decomposition(*args, **kwds)
else:
- E = self.matrix().transpose().decomposition(*args,**kwds)
+ E = self.matrix().transpose().decomposition(*args, **kwds)
if D.is_ambient():
return Sequence([D.submodule(V, check=False) for V, _ in E],
cr=True, check=False)
@@ -899,7 +900,7 @@ def det(self):
2
"""
if not self.is_endomorphism():
- raise ArithmeticError("Matrix morphism must be an endomorphism.")
+ raise ArithmeticError("matrix morphism must be an endomorphism")
return self.matrix().determinant()
def fcp(self, var='x'):
@@ -1635,7 +1636,7 @@ def __init__(self, parent, A, copy_matrix=True, side='left'):
if A.nrows() != parent.domain().rank():
raise ArithmeticError("number of rows of matrix (={}) must equal rank of domain (={})".format(A.nrows(), parent.domain().rank()))
if A.ncols() != parent.codomain().rank():
- raise ArithmeticError("number of columns of matrix (={}) must equal rank of codomain (={})".format(A.ncols(), parent.codomain().rank()))
+ raise ArithmeticError("number of columns of matrix (={}) must equal rank of codomain (={})".format(A.ncols(), parent.codomain().rank()))
if side == "right":
if A.nrows() != parent.codomain().rank():
raise ArithmeticError("number of rows of matrix (={}) must equal rank of codomain (={})".format(A.nrows(), parent.domain().rank()))
@@ -1697,7 +1698,7 @@ def matrix(self, side=None):
ValueError: side must be 'left' or 'right', not junk
"""
if side not in ['left', 'right', None]:
- raise ValueError("side must be 'left' or 'right', not {0}".format(side))
+ raise ValueError("side must be 'left' or 'right', not {}".format(side))
if side == self.side() or side is None:
return self._matrix
return self._matrix.transpose()
@@ -1794,7 +1795,7 @@ def _repr_(self):
sage: phi._repr_()
'Free module morphism defined by the matrix\n[3 0]\n[0 2]\nDomain: Ambient free module of rank 2 over the principal ideal domain Integer Ring\nCodomain: Ambient free module of rank 2 over the principal ideal domain Integer Ring'
"""
- rep = "Morphism defined by the matrix\n{0}".format(self.matrix())
+ rep = "Morphism defined by the matrix\n{}".format(self.matrix())
if self._side == 'right':
rep += " acting by multiplication on the left"
return rep
diff --git a/src/sage/modules/vector_space_morphism.py b/src/sage/modules/vector_space_morphism.py
index 1102d15968e..6585da0b047 100644
--- a/src/sage/modules/vector_space_morphism.py
+++ b/src/sage/modules/vector_space_morphism.py
@@ -705,9 +705,9 @@ def linear_transformation(arg0, arg1=None, arg2=None, side='left'):
Vector_callable_symbolic_dense = ()
if side not in ['left', 'right']:
- raise ValueError("side must be 'left' or 'right', not {0}".format(side))
+ raise ValueError("side must be 'left' or 'right', not {}".format(side))
if not (is_Matrix(arg0) or is_VectorSpace(arg0)):
- raise TypeError('first argument must be a matrix or a vector space, not {0}'.format(arg0))
+ raise TypeError('first argument must be a matrix or a vector space, not {}'.format(arg0))
if is_Matrix(arg0):
R = arg0.base_ring()
if not R.is_field():
@@ -763,7 +763,7 @@ def linear_transformation(arg0, arg1=None, arg2=None, side='left'):
msg = 'symbolic function must be linear in all the inputs:\n' + e.args[0]
raise ValueError(msg)
# have matrix with respect to standard bases, now consider user bases
- images = [v*arg2 for v in D.basis()]
+ images = [v * arg2 for v in D.basis()]
try:
arg2 = matrix([C.coordinates(C(a)) for a in images])
except (ArithmeticError, TypeError) as e:
@@ -779,7 +779,8 @@ def linear_transformation(arg0, arg1=None, arg2=None, side='left'):
# __init__ will check matrix sizes versus domain/codomain dimensions
return H(arg2)
-def is_VectorSpaceMorphism(x):
+
+def is_VectorSpaceMorphism(x) -> bool:
r"""
Returns ``True`` if ``x`` is a vector space morphism (a linear transformation).
@@ -859,7 +860,7 @@ def __init__(self, homspace, A, side="left"):
"""
if not vector_space_homspace.is_VectorSpaceHomspace(homspace):
- raise TypeError('homspace must be a vector space hom space, not {0}'.format(homspace))
+ raise TypeError('homspace must be a vector space hom space, not {}'.format(homspace))
if isinstance(A, matrix_morphism.MatrixMorphism):
A = A.matrix()
if not is_Matrix(A):
diff --git a/src/sage/modules/with_basis/subquotient.py b/src/sage/modules/with_basis/subquotient.py
index f36317a769d..d331e6243ad 100644
--- a/src/sage/modules/with_basis/subquotient.py
+++ b/src/sage/modules/with_basis/subquotient.py
@@ -82,7 +82,7 @@ def __classcall_private__(cls, submodule, category=None):
category = default_category.or_subcategory(category, join=True)
return super().__classcall__(cls, submodule, category)
- def __init__(self, submodule, category):
+ def __init__(self, submodule, category, *args, **opts):
r"""
Initialize this quotient of a module with basis by a submodule.
@@ -107,7 +107,7 @@ def __init__(self, submodule, category):
indices = embedding.cokernel_basis_indices()
CombinatorialFreeModule.__init__(self,
submodule.base_ring(), indices,
- category=category)
+ category=category, *args, **opts)
def ambient(self):
r"""
@@ -394,21 +394,19 @@ def is_submodule(self, other):
sage: H.is_submodule(G)
False
- TESTS::
+ Different ambient spaces::
sage: X = CombinatorialFreeModule(QQ, range(4)); x = X.basis()
sage: F = X.submodule([x[0]-x[1], x[1]-x[2], x[2]-x[3]])
sage: Y = CombinatorialFreeModule(QQ, range(6)); y = Y.basis()
sage: G = Y.submodule([y[0]-y[1], y[1]-y[2], y[2]-y[3]])
sage: F.is_submodule(G)
- Traceback (most recent call last):
- ...
- ValueError: other (=...) should be a submodule of the same ambient space
+ False
"""
if other is self._ambient:
return True
if not (isinstance(self, SubmoduleWithBasis) and self.ambient() is other.ambient()):
- raise ValueError("other (=%s) should be a submodule of the same ambient space" % other)
+ return False # different ambient spaces
if self not in ModulesWithBasis.FiniteDimensional:
raise NotImplementedError("only implemented for finite dimensional submodules")
if self.dimension() > other.dimension(): # quick dimension check
diff --git a/src/sage/monoids/indexed_free_monoid.py b/src/sage/monoids/indexed_free_monoid.py
index 58910533a9a..09b35530098 100644
--- a/src/sage/monoids/indexed_free_monoid.py
+++ b/src/sage/monoids/indexed_free_monoid.py
@@ -355,6 +355,31 @@ def to_word_list(self):
"""
return [k for k,e in self._sorted_items() for dummy in range(e)]
+ def is_one(self) -> bool:
+ """
+ Return if ``self`` is the identity element.
+
+ EXAMPLES::
+
+ sage: F = FreeMonoid(index_set=ZZ)
+ sage: a,b,c,d,e = [F.gen(i) for i in range(5)]
+ sage: (b*a*c^3*a).is_one()
+ False
+ sage: F.one().is_one()
+ True
+
+ ::
+
+ sage: F = FreeAbelianMonoid(index_set=ZZ)
+ sage: a,b,c,d,e = [F.gen(i) for i in range(5)]
+ sage: (b*c^3*a).is_one()
+ False
+ sage: F.one().is_one()
+ True
+ """
+ return not self._monomial
+
+
class IndexedFreeMonoidElement(IndexedMonoidElement):
"""
An element of an indexed free abelian monoid.
@@ -591,6 +616,29 @@ def __floordiv__(self, elt):
d[k] = diff
return self.__class__(self.parent(), d)
+ def divides(self, m) -> bool:
+ r"""
+ Return whether ``self`` divides ``m``.
+
+ EXAMPLES::
+
+ sage: F = FreeAbelianMonoid(index_set=ZZ)
+ sage: a,b,c,d,e = [F.gen(i) for i in range(5)]
+ sage: elt = a*b*c^3*d^2
+ sage: a.divides(elt)
+ True
+ sage: c.divides(elt)
+ True
+ sage: (a*b*d^2).divides(elt)
+ True
+ sage: (a^4).divides(elt)
+ False
+ sage: e.divides(elt)
+ False
+ """
+ other = m._monomial
+ return all(k in other and v <= other[k] for k, v in self._monomial.items())
+
def __len__(self):
"""
Return the length of ``self``.
@@ -605,8 +653,7 @@ def __len__(self):
sage: len(elt)
7
"""
- m = self._monomial
- return sum(m[gen] for gen in m)
+ return sum(self._monomial.values())
length = __len__
diff --git a/src/sage/rings/finite_rings/element_base.pyx b/src/sage/rings/finite_rings/element_base.pyx
index 1d0dd2b563a..42ca25b930e 100755
--- a/src/sage/rings/finite_rings/element_base.pyx
+++ b/src/sage/rings/finite_rings/element_base.pyx
@@ -143,7 +143,7 @@ cdef class FiniteRingElement(CommutativeRingElement):
raise ValueError("unknown algorithm")
def to_bytes(self, byteorder="big"):
- """
+ r"""
Return an array of bytes representing an integer.
Internally relies on the python ``int.to_bytes()`` method.
diff --git a/src/sage/rings/finite_rings/finite_field_base.pyx b/src/sage/rings/finite_rings/finite_field_base.pyx
index 00b30bb5a44..7419c158d8c 100644
--- a/src/sage/rings/finite_rings/finite_field_base.pyx
+++ b/src/sage/rings/finite_rings/finite_field_base.pyx
@@ -2118,7 +2118,7 @@ cdef class FiniteField(Field):
for col in B.columns()]
def from_bytes(self, input_bytes, byteorder="big"):
- """
+ r"""
Return the integer represented by the given array of bytes.
Internally relies on the python ``int.from_bytes()`` method.
diff --git a/src/sage/rings/integer_ring.pyx b/src/sage/rings/integer_ring.pyx
index bbbd989a65d..9199debbfb6 100644
--- a/src/sage/rings/integer_ring.pyx
+++ b/src/sage/rings/integer_ring.pyx
@@ -1575,7 +1575,7 @@ cdef class IntegerRing_class(PrincipalIdealDomain):
return pAdicValuation(self, p)
def from_bytes(self, input_bytes, byteorder="big", is_signed=False):
- """
+ r"""
Return the integer represented by the given array of bytes.
Internally relies on the python ``int.from_bytes()`` method.
diff --git a/src/sage/rings/polynomial/plural.pxd b/src/sage/rings/polynomial/plural.pxd
index 06b48c737f3..5e3618bd90e 100644
--- a/src/sage/rings/polynomial/plural.pxd
+++ b/src/sage/rings/polynomial/plural.pxd
@@ -37,6 +37,8 @@ cdef class NCPolynomial_plural(RingElement):
cpdef _repr_short_(self) noexcept
cdef long _hash_c(self) noexcept
cpdef is_constant(self) noexcept
+ cpdef dict dict(self) noexcept
+ cpdef dict monomial_coefficients(self, bint copy=*) noexcept
# cpdef _homogenize(self, int var)
cdef NCPolynomial_plural new_NCP(NCPolynomialRing_plural parent, poly *juice) noexcept
diff --git a/src/sage/rings/polynomial/plural.pyx b/src/sage/rings/polynomial/plural.pyx
index 4fb7104cce6..30e6aa89dc3 100644
--- a/src/sage/rings/polynomial/plural.pyx
+++ b/src/sage/rings/polynomial/plural.pyx
@@ -856,6 +856,20 @@ cdef class NCPolynomialRing_plural(Ring):
return new_NCP(self,_p)
+ def algebra_generators(self):
+ r"""
+ Return the algebra generators of ``self``.
+
+ EXAMPLES::
+
+ sage: A. = FreeAlgebra(QQ, 3)
+ sage: P = A.g_algebra(relations={y*x:-x*y}, order='lex')
+ sage: P.algebra_generators()
+ Finite family {'x': x, 'y': y, 'z': z}
+ """
+ from sage.sets.family import Family
+ return Family(self.gens_dict())
+
def ideal(self, *gens, **kwds):
"""
Create an ideal in this polynomial ring.
@@ -2178,7 +2192,7 @@ cdef class NCPolynomial_plural(RingElement):
return (self._parent)._base._zero_element
- def dict(self):
+ cpdef dict dict(self) noexcept:
"""
Return a dictionary representing ``self``. This dictionary is in
the same format as the generic MPolynomial: The dictionary
@@ -2204,7 +2218,8 @@ cdef class NCPolynomial_plural(RingElement):
rChangeCurrRing(r)
base = (self._parent)._base
p = self._poly
- pd = dict()
+ cdef dict d
+ cdef dict pd = dict()
while p:
d = dict()
for v from 1 <= v <= r.N:
@@ -2217,6 +2232,30 @@ cdef class NCPolynomial_plural(RingElement):
p = pNext(p)
return pd
+ cpdef dict monomial_coefficients(self, bint copy=True) noexcept:
+ """
+ Return a dictionary representation of ``self`` with the keys
+ the exponent vectors and the values the corresponding coefficients.
+
+ INPUT:
+
+ * "copy" -- ignored
+
+ EXAMPLES::
+
+ sage: A. = FreeAlgebra(GF(389), 3)
+ sage: R = A.g_algebra(relations={y*x:-x*y + z}, order='lex')
+ sage: R.inject_variables()
+ Defining x, z, y
+ sage: f = (2*x*y^3*z^2 + (7)*x^2 + (3))
+ sage: d = f.monomial_coefficients(False); d
+ {(0, 0, 0): 3, (1, 2, 3): 2, (2, 0, 0): 7}
+ sage: d.clear()
+ sage: f.monomial_coefficients()
+ {(0, 0, 0): 3, (1, 2, 3): 2, (2, 0, 0): 7}
+ """
+ return self.dict()
+
def _im_gens_(self, codomain, im_gens, base_map=None):
"""
Return the image of ``self`` in codomain under the map that sends
diff --git a/src/sage/rings/polynomial/polynomial_ring.py b/src/sage/rings/polynomial/polynomial_ring.py
index b9c64884a57..83251eda18e 100644
--- a/src/sage/rings/polynomial/polynomial_ring.py
+++ b/src/sage/rings/polynomial/polynomial_ring.py
@@ -281,7 +281,6 @@ def __init__(self, base_ring, name=None, sparse=False, implementation=None,
sage: GF(7)['x']['y'].is_finite()
False
-
"""
# We trust that, if category is given, it is useful and does not need to be joined
# with the default category
@@ -1320,7 +1319,7 @@ def monomial(self, exponent):
sage: R.monomial(m.degree()) == m
True
"""
- return self({exponent:self.base_ring().one()})
+ return self({exponent: self.base_ring().one()})
def krull_dimension(self):
"""
@@ -1362,23 +1361,25 @@ def ngens(self):
"""
return 1
- def random_element(self, degree=(-1,2), *args, **kwds):
+ def random_element(self, degree=(-1, 2), monic=False, *args, **kwds):
r"""
- Return a random polynomial of given degree or with given degree bounds.
+ Return a random polynomial of given degree (bounds).
INPUT:
- - ``degree`` - optional integer for fixing the degree
- or a tuple of minimum and maximum degrees. By default set to
- ``(-1,2)``.
+ - ``degree`` -- (default: ``(-1, 2)``) integer for fixing the degree or
+ a tuple of minimum and maximum degrees
+
+ - ``monic`` -- boolean (optional); indicate whether the sampled
+ polynomial should be monic
- - ``*args, **kwds`` - Passed on to the ``random_element`` method for
- the base ring
+ - ``*args, **kwds`` -- additional keyword parameters passed on to the
+ ``random_element`` method for the base ring
EXAMPLES::
sage: R. = ZZ[]
- sage: f = R.random_element(10, 5, 10)
+ sage: f = R.random_element(10, x=5, y=10)
sage: f.degree()
10
sage: f.parent() is R
@@ -1388,14 +1389,16 @@ def random_element(self, degree=(-1,2), *args, **kwds):
sage: R.random_element(6).degree()
6
- If a tuple of two integers is given for the ``degree`` argument, a degree
- is first uniformly chosen, then a polynomial of that degree is given::
+ If a tuple of two integers is given for the ``degree`` argument, a
+ polynomial is chosen among all polynomials with degree between them. If
+ the base ring can be sampled uniformly, then this method also samples
+ uniformly::
- sage: R.random_element(degree=(0, 8)).degree() in range(0, 9)
+ sage: R.random_element(degree=(0, 4)).degree() in range(0, 5)
True
- sage: found = [False]*9
+ sage: found = [False]*5
sage: while not all(found):
- ....: found[R.random_element(degree=(0, 8)).degree()] = True
+ ....: found[R.random_element(degree=(0, 4)).degree()] = True
Note that the zero polynomial has degree `-1`, so if you want to
consider it set the minimum degree to `-1`::
@@ -1403,6 +1406,14 @@ def random_element(self, degree=(-1,2), *args, **kwds):
sage: while R.random_element(degree=(-1,2), x=-1, y=1) != R.zero():
....: pass
+ Monic polynomials are chosen among all monic polynomials with degree
+ between the given ``degree`` argument::
+
+ sage: all(R.random_element(degree=(-1, 1), monic=True).is_monic() for _ in range(10^3))
+ True
+ sage: all(R.random_element(degree=(0, 1), monic=True).is_monic() for _ in range(10^3))
+ True
+
TESTS::
sage: R.random_element(degree=[5])
@@ -1419,14 +1430,42 @@ def random_element(self, degree=(-1,2), *args, **kwds):
sage: R = PolynomialRing(GF(2), 'z')
sage: for _ in range(100):
- ....: d = randint(-1,20)
+ ....: d = randint(-1, 20)
....: P = R.random_element(degree=d)
- ....: assert P.degree() == d, "problem with {} which has not degree {}".format(P,d)
+ ....: assert P.degree() == d
+
+ In :issue:`37118`, ranges including integers below `-1` no longer raise
+ an error::
+
+ sage: R.random_element(degree=(-2, 3)) # random
+ z^3 + z^2 + 1
+
+ ::
+
+ sage: 0 in [R.random_element(degree=(-1, 2), monic=True) for _ in range(500)]
+ False
+
+ Testing error handling::
+
+ sage: R.random_element(degree=-5)
+ Traceback (most recent call last):
+ ...
+ ValueError: degree (=-5) must be at least -1
- sage: R.random_element(degree=-2)
+ sage: R.random_element(degree=(-3, -2))
Traceback (most recent call last):
...
- ValueError: degree should be an integer greater or equal than -1
+ ValueError: maximum degree (=-2) must be at least -1
+
+ Testing uniformity::
+
+ sage: from collections import Counter
+ sage: R = GF(3)["x"]
+ sage: samples = [R.random_element(degree=(-1, 2)) for _ in range(27000)] # long time
+ sage: assert all(750 <= f <= 1250 for f in Counter(samples).values()) # long time
+
+ sage: samples = [R.random_element(degree=(-1, 2), monic=True) for _ in range(13000)] # long time
+ sage: assert all(750 <= f <= 1250 for f in Counter(samples).values()) # long time
"""
R = self.base_ring()
@@ -1435,11 +1474,15 @@ def random_element(self, degree=(-1,2), *args, **kwds):
raise ValueError("degree argument must be an integer or a tuple of 2 integers (min_degree, max_degree)")
if degree[0] > degree[1]:
raise ValueError("minimum degree must be less or equal than maximum degree")
+ if degree[1] < -1:
+ raise ValueError(f"maximum degree (={degree[1]}) must be at least -1")
else:
- degree = (degree,degree)
+ if degree < -1:
+ raise ValueError(f"degree (={degree}) must be at least -1")
+ degree = (degree, degree)
if degree[0] <= -2:
- raise ValueError("degree should be an integer greater or equal than -1")
+ degree = (-1, degree[1])
# If the coefficient range only contains 0, then
# * if the degree range includes -1, return the zero polynomial,
@@ -1450,24 +1493,44 @@ def random_element(self, degree=(-1,2), *args, **kwds):
else:
raise ValueError("No polynomial of degree >= 0 has all coefficients zero")
- # Pick a random degree
- d = randint(degree[0], degree[1])
-
- # If degree is -1, return the 0 polynomial
- if d == -1:
+ if degree == (-1, -1):
return self.zero()
- # If degree is 0, return a random constant term
- if d == 0:
- return self(R._random_nonzero_element(*args, **kwds))
+ # If `monic` is set, zero should be ignored
+ if degree[0] == -1 and monic:
+ if degree[1] == -1:
+ raise ValueError("the maximum degree of monic polynomials needs to be at least 0")
+ if degree[1] == 0:
+ return self.one()
+ degree = (0, degree[1])
# Pick random coefficients
- p = self([R.random_element(*args, **kwds) for _ in range(d)])
+ end = degree[1]
+ if degree[0] == -1:
+ return self([R.random_element(*args, **kwds) for _ in range(end + 1)])
+
+ nonzero = False
+ coefs = [None] * (end + 1)
+
+ while not nonzero:
+ # Pick leading coefficients, if `monic` is set it's handle here.
+ if monic:
+ for i in range(degree[1] - degree[0] + 1):
+ coefs[end - i] = R.random_element(*args, **kwds)
+ if not nonzero and not coefs[end - i].is_zero():
+ coefs[end - i] = R.one()
+ nonzero = True
+ else:
+ # Fast path
+ for i in range(degree[1] - degree[0] + 1):
+ coefs[end - i] = R.random_element(*args, **kwds)
+ nonzero |= not coefs[end - i].is_zero()
- # Add non-zero leading coefficient
- p += R._random_nonzero_element(*args, **kwds) * self.gen() ** d
+ # Now we pick the remaining coefficients.
+ for i in range(degree[1] - degree[0] + 1, degree[1] + 1):
+ coefs[end - i] = R.random_element(*args, **kwds)
- return p
+ return self(coefs)
def _monics_degree(self, of_degree):
"""
@@ -1562,8 +1625,8 @@ def karatsuba_threshold(self):
def set_karatsuba_threshold(self, Karatsuba_threshold):
"""
- Changes the default threshold for this ring in the method :meth:`_mul_karatsuba`
- to fall back to the schoolbook algorithm.
+ Changes the default threshold for this ring in the method
+ :meth:`_mul_karatsuba` to fall back to the schoolbook algorithm.
.. warning::
@@ -1587,11 +1650,11 @@ def polynomials( self, of_degree=None, max_degree=None ):
INPUT: Pass exactly one of:
- - ``max_degree`` - an int; the iterator will generate
+ - ``max_degree`` -- an int; the iterator will generate
all polynomials which have degree less than or equal to
``max_degree``
- - ``of_degree`` - an int; the iterator will generate
+ - ``of_degree`` -- an int; the iterator will generate
all polynomials which have degree ``of_degree``
OUTPUT: an iterator
@@ -1653,11 +1716,11 @@ def monics( self, of_degree=None, max_degree=None ):
INPUT: Pass exactly one of:
- - ``max_degree`` - an int; the iterator will generate
+ - ``max_degree`` -- an int; the iterator will generate
all monic polynomials which have degree less than or equal to
``max_degree``
- - ``of_degree`` - an int; the iterator will generate
+ - ``of_degree`` -- an int; the iterator will generate
all monic polynomials which have degree ``of_degree``
@@ -1734,7 +1797,7 @@ def quotient_by_principal_ideal(self, f, names=None, **kwds):
INPUT:
- - ``f`` - either a polynomial in ``self``, or a principal
+ - ``f`` -- either a polynomial in ``self``, or a principal
ideal of ``self``.
- further named arguments that are passed to the quotient constructor.
@@ -1884,7 +1947,8 @@ def __init__(self, base_ring, name="x", sparse=False, implementation=None,
@cached_method(key=lambda self, d, q, sign, lead: (d, q, sign, tuple([x if isinstance(x, (tuple, list)) else (x, 0) for x in lead]) if isinstance(lead, (tuple, list)) else ((lead, 0))))
def weil_polynomials(self, d, q, sign=1, lead=1):
r"""
- Return all integer polynomials whose complex roots all have a specified absolute value.
+ Return all integer polynomials whose complex roots all have a specified
+ absolute value.
Such polynomials `f` satisfy a functional equation
@@ -1892,29 +1956,34 @@ def weil_polynomials(self, d, q, sign=1, lead=1):
T^d f(q/T) = s q^{d/2} f(T)
- where `d` is the degree of `f`, `s` is a sign and `q^{1/2}` is the absolute value
- of the roots of `f`.
+ where `d` is the degree of `f`, `s` is a sign and `q^{1/2}` is the
+ absolute value of the roots of `f`.
INPUT:
- ``d`` -- integer, the degree of the polynomials
- - ``q`` -- integer, the square of the complex absolute value of the roots
+ - ``q`` -- integer, the square of the complex absolute value of the
+ roots
- - ``sign`` -- integer (default `1`), the sign `s` of the functional equation
+ - ``sign`` -- integer (default `1`), the sign `s` of the functional
+ equation
- - ``lead`` -- integer, list of integers or list of pairs of integers (default `1`),
- constraints on the leading few coefficients of the generated polynomials.
- If pairs `(a, b)` of integers are given, they are treated as a constraint
- of the form `\equiv a \pmod{b}`; the moduli must be in decreasing order by
- divisibility, and the modulus of the leading coefficient must be 0.
+ - ``lead`` -- integer, list of integers or list of pairs of integers
+ (default `1`), constraints on the leading few coefficients of the
+ generated polynomials. If pairs `(a, b)` of integers are given, they
+ are treated as a constraint of the form `\equiv a \pmod{b}`; the
+ moduli must be in decreasing order by divisibility, and the modulus
+ of the leading coefficient must be 0.
.. SEEALSO::
- More documentation and additional options are available using the iterator
+ More documentation and additional options are available using the
+ iterator
:class:`sage.rings.polynomial.weil.weil_polynomials.WeilPolynomials`
- directly. In addition, polynomials have a method :meth:`is_weil_polynomial` to
- test whether or not the given polynomial is a Weil polynomial.
+ directly. In addition, polynomials have a method
+ :meth:`is_weil_polynomial` to test whether or not the given
+ polynomial is a Weil polynomial.
EXAMPLES::
@@ -1949,7 +2018,8 @@ def weil_polynomials(self, d, q, sign=1, lead=1):
TESTS:
- We check that products of Weil polynomials are also listed as Weil polynomials::
+ We check that products of Weil polynomials are also listed as Weil
+ polynomials::
sage: all((f * g) in R.weil_polynomials(6, q) for q in [3, 4] # needs sage.libs.flint
....: for f in R.weil_polynomials(2, q) for g in R.weil_polynomials(4, q))
@@ -1964,13 +2034,15 @@ def weil_polynomials(self, d, q, sign=1, lead=1):
....: for j in range(1, (3+i)//2 + 1))
....: for i in range(4)]) for f in simples]
- Check that every polynomial in this list has 3 real roots between `-2 \sqrt{3}` and `2 \sqrt{3}`::
+ Check that every polynomial in this list has 3 real roots between `-2
+ \sqrt{3}` and `2 \sqrt{3}`::
sage: roots = [f.roots(RR, multiplicities=False) for f in reals] # needs sage.libs.flint
sage: all(len(L) == 3 and all(x^2 <= 12 for x in L) for L in roots) # needs sage.libs.flint
True
- Finally, check that the original polynomials are reconstructed as CM polynomials::
+ Finally, check that the original polynomials are reconstructed as CM
+ polynomials::
sage: all(f == T^3*r(T + 3/T) for (f, r) in zip(simples, reals)) # needs sage.libs.flint
True
@@ -2126,7 +2198,8 @@ def _element_class():
def _ideal_class_(self, n=0):
"""
- Returns the class representing ideals in univariate polynomial rings over fields.
+ Returns the class representing ideals in univariate polynomial rings
+ over fields.
EXAMPLES::
diff --git a/src/sage/rings/tests.py b/src/sage/rings/tests.py
index 011cfc99070..2e20a08be10 100644
--- a/src/sage/rings/tests.py
+++ b/src/sage/rings/tests.py
@@ -428,16 +428,16 @@ def test_karatsuba_multiplication(base_ring, maxdeg1, maxdeg2,
sage: from sage.rings.tests import test_karatsuba_multiplication
sage: test_karatsuba_multiplication(ZZ, 6, 5, verbose=True, seed=42)
test_karatsuba_multiplication: ring=Univariate Polynomial Ring in x over Integer Ring, threshold=2
- (2*x^6 - x^5 - x^4 - 3*x^3 + 4*x^2 + 4*x + 1)*(4*x^4 + x^3 - 2*x^2 - 20*x + 3)
- (16*x^2)*(-41*x + 1)
- (x^6 + 2*x^5 + 8*x^4 - x^3 + x^2 + x)*(-x^2 - 4*x + 3)
- (-x^3 - x - 8)*(-1)
- (x - 1)*(-x^5 + 3*x^4 - x^3 + 2*x + 1)
- (x^3 + x^2 + x + 1)*(4*x^3 + 76*x^2 - x - 1)
- (x^6 - 5*x^4 - x^3 + 6*x^2 + 1)*(5*x^2 - x + 4)
- (3*x - 2)*(x - 1)
- (21)*(14*x^5 - x^2 + 4*x + 1)
- (12*x^5 - 12*x^2 + 2*x + 1)*(26*x^4 + x^3 + 1)
+ (x^6 + 4*x^5 + 4*x^4 - 3*x^3 - x^2 - x)*(2*x^4 + 3*x^3 - 20*x^2 - 2*x + 1)
+ (4*x^5 + 16*x^2 + x - 41)*(x^2 + x - 1)
+ (8*x^2 + 2*x + 1)*(3)
+ (-4*x - 1)*(-8*x^2 - x)
+ (-x^6 - x^3 - x^2 + x + 1)*(2*x^3 - x + 3)
+ (-x^2 + x + 1)*(x^4 + x^3 - x^2 - x + 76)
+ (4*x^3 + x^2 + 6)*(-x^2 - 5*x)
+ (x + 4)*(-x + 5)
+ (-2*x)*(3*x^2 - x)
+ (x^6 + 21*x^5 + x^4 + 4*x^3 - x^2)*(14*x^4 + x^3 + 2*x^2 - 12*x)
Test Karatsuba multiplication of polynomials of small degree over some common rings::
@@ -474,9 +474,9 @@ def test_karatsuba_multiplication(base_ring, maxdeg1, maxdeg2,
R = PolynomialRing(base_ring, 'x')
if verbose:
print("test_karatsuba_multiplication: ring={}, threshold={}".format(R, threshold))
- for i in range(numtests):
- f = R.random_element(randint(0, maxdeg1), *base_ring_random_elt_args)
- g = R.random_element(randint(0, maxdeg2), *base_ring_random_elt_args)
+ for _ in range(numtests):
+ f = R.random_element(randint(0, maxdeg1), False, *base_ring_random_elt_args)
+ g = R.random_element(randint(0, maxdeg2), False, *base_ring_random_elt_args)
if verbose:
print(" ({})*({})".format(f, g))
if ref_mul(f, g) - f._mul_karatsuba(g, threshold) != 0:
diff --git a/src/sage/schemes/curves/point.py b/src/sage/schemes/curves/point.py
index dcc78724497..373dc6ce19f 100644
--- a/src/sage/schemes/curves/point.py
+++ b/src/sage/schemes/curves/point.py
@@ -451,7 +451,7 @@ class IntegralAffineCurvePoint_finite_field(IntegralAffineCurvePoint):
class IntegralAffinePlaneCurvePoint(IntegralAffineCurvePoint, AffinePlaneCurvePoint_field):
"""
- Point of an integral affine plane curve over a finite field.
+ Point of an integral affine plane curve.
"""
pass
diff --git a/src/sage/schemes/generic/glue.py b/src/sage/schemes/generic/glue.py
index 76bd9a1ab9e..e70aafa0507 100644
--- a/src/sage/schemes/generic/glue.py
+++ b/src/sage/schemes/generic/glue.py
@@ -16,19 +16,35 @@ class GluedScheme(scheme.Scheme):
INPUT:
- - ``f`` - open immersion from a scheme U to a scheme
- X
+ - ``f`` -- open immersion from a scheme `U` to a scheme
+ `X`
- - ``g`` - open immersion from U to a scheme Y
+ - ``g`` -- open immersion from `U` to a scheme `Y`
- OUTPUT: The scheme obtained by gluing X and Y along the open set
- U.
+ OUTPUT: The scheme obtained by gluing `X` and `Y` along the open set
+ `U`.
- .. note::
+ .. NOTE::
Checking that `f` and `g` are open
immersions is not implemented.
+
+ EXAMPLES::
+
+ sage: R. = QQ[]
+ sage: S. = R.quotient(x * y - 1)
+ sage: Rx = QQ["x"]
+ sage: Ry = QQ["y"]
+ sage: phi_x = Rx.hom([xbar])
+ sage: phi_y = Ry.hom([ybar])
+ sage: Sx = Schemes()(phi_x)
+ sage: Sy = Schemes()(phi_y)
+ sage: Sx.glue_along_domains(Sy)
+ Scheme obtained by gluing X and Y along U, where
+ X: Spectrum of Univariate Polynomial Ring in x over Rational Field
+ Y: Spectrum of Univariate Polynomial Ring in y over Rational Field
+ U: Spectrum of Quotient of Multivariate Polynomial Ring in x, y over Rational Field by the ideal (x*y - 1)
"""
def __init__(self, f, g, check=True):
if check:
@@ -42,6 +58,23 @@ def __init__(self, f, g, check=True):
self.__g = g
def gluing_maps(self):
+ r"""
+ Return the gluing maps of this glued scheme, i.e. the maps `f` and `g`.
+
+ EXAMPLES::
+
+ sage: R. = QQ[]
+ sage: S. = R.quotient(x * y - 1)
+ sage: Rx = QQ["x"]
+ sage: Ry = QQ["y"]
+ sage: phi_x = Rx.hom([xbar])
+ sage: phi_y = Ry.hom([ybar])
+ sage: Sx = Schemes()(phi_x)
+ sage: Sy = Schemes()(phi_y)
+ sage: Sxy = Sx.glue_along_domains(Sy)
+ sage: Sxy.gluing_maps() == (Sx, Sy)
+ True
+ """
return self.__f, self.__g
def _repr_(self):
diff --git a/src/sage/schemes/generic/homset.py b/src/sage/schemes/generic/homset.py
index a9a0f0735df..6fca9c1e257 100644
--- a/src/sage/schemes/generic/homset.py
+++ b/src/sage/schemes/generic/homset.py
@@ -2,13 +2,13 @@
Set of homomorphisms between two schemes
For schemes `X` and `Y`, this module implements the set of morphisms
-`Hom(X,Y)`. This is done by :class:`SchemeHomset_generic`.
+`\mathrm{Hom}(X,Y)`. This is done by :class:`SchemeHomset_generic`.
-As a special case, the Hom-sets can also represent the points of a
-scheme. Recall that the `K`-rational points of a scheme `X` over `k`
-can be identified with the set of morphisms `Spec(K) \to X`. In Sage
-the rational points are implemented by such scheme morphisms. This is
-done by :class:`SchemeHomset_points` and its subclasses.
+As a special case, the Hom-sets can also represent the points of a scheme.
+Recall that the `K`-rational points of a scheme `X` over `k` can be identified
+with the set of morphisms `\mathrm{Spec}(K) \to X`. In Sage the rational points
+are implemented by such scheme morphisms. This is done by
+:class:`SchemeHomset_points` and its subclasses.
.. note::
@@ -407,12 +407,12 @@ def _element_constructor_(self, x, check=True):
# *******************************************************************
class SchemeHomset_points(SchemeHomset_generic):
- """
+ r"""
Set of rational points of the scheme.
- Recall that the `K`-rational points of a scheme `X` over `k` can
- be identified with the set of morphisms `Spec(K) \to X`. In Sage,
- the rational points are implemented by such scheme morphisms.
+ Recall that the `K`-rational points of a scheme `X` over `k` can be
+ identified with the set of morphisms `\mathrm{Spec}(K) \to X`. In Sage, the
+ rational points are implemented by such scheme morphisms.
If a scheme has a finite number of points, then the homset is
supposed to implement the Python iterator interface. See
@@ -656,16 +656,16 @@ def _element_constructor_(self, *v, **kwds):
"""
if len(v) == 1:
v = v[0]
- return self.codomain()._point(self, v, **kwds)
+ return self.extended_codomain()._point(self, v, **kwds)
def extended_codomain(self):
- """
+ r"""
Return the codomain with extended base, if necessary.
OUTPUT:
The codomain scheme, with its base ring extended to the
- codomain. That is, the codomain is of the form `Spec(R)` and
+ codomain. That is, the codomain is of the form `\mathrm{Spec}(R)` and
the base ring of the domain is extended to `R`.
EXAMPLES::
@@ -693,6 +693,12 @@ def extended_codomain(self):
self._extended_codomain = X
return X
+ def zero(self):
+ """
+ Return the identity of the codomain with extended base, if necessary.
+ """
+ return self.extended_codomain().zero()
+
def _repr_(self):
"""
Return a string representation of ``self``.
@@ -710,8 +716,8 @@ def _repr_(self):
return 'Set of rational points of '+str(self.extended_codomain())
def value_ring(self):
- """
- Return `R` for a point Hom-set `X(Spec(R))`.
+ r"""
+ Return `R` for a point Hom-set `X(\mathrm{Spec}(R))`.
OUTPUT:
diff --git a/src/sage/schemes/generic/morphism.py b/src/sage/schemes/generic/morphism.py
index 31dc4010f5b..7d91214e32a 100644
--- a/src/sage/schemes/generic/morphism.py
+++ b/src/sage/schemes/generic/morphism.py
@@ -28,12 +28,12 @@
new Hom-set class does not use ``MyScheme._morphism`` then you
do not have to provide it.
-Note that points on schemes are morphisms `Spec(K)\to X`, too. But we
-typically use a different notation, so they are implemented in a
-different derived class. For this, you should implement a method
+Note that points on schemes are morphisms `\mathrm{Spec}(K)\to X`, too. But we
+typically use a different notation, so they are implemented in a different
+derived class. For this, you should implement a method
-* ``MyScheme._point(*args, **kwds)`` returning a point, that is,
- a morphism `Spec(K)\to X`. Your point class should derive from
+* ``MyScheme._point(*args, **kwds)`` returning a point, that is, a morphism
+ `\mathrm{Spec}(K)\to X`. Your point class should derive from
:class:`SchemeMorphism_point`.
Optionally, you can also provide a special Hom-set for the points, for
@@ -1790,11 +1790,11 @@ def __init__(self, X):
############################################################################
class SchemeMorphism_point(SchemeMorphism):
- """
+ r"""
Base class for rational points on schemes.
Recall that the `K`-rational points of a scheme `X` over `k` can
- be identified with the set of morphisms `Spec(K) \to X`. In Sage,
+ be identified with the set of morphisms `\mathrm{Spec}(K) \to X`. In Sage,
the rational points are implemented by such scheme morphisms.
EXAMPLES::
diff --git a/src/sage/schemes/generic/point.py b/src/sage/schemes/generic/point.py
index 5f8f3d024a6..7ef85645f56 100644
--- a/src/sage/schemes/generic/point.py
+++ b/src/sage/schemes/generic/point.py
@@ -227,28 +227,3 @@ def _richcmp_(self, other, op):
False
"""
return richcmp(self.__P, other.__P, op)
-
-########################################################
-# Points on a scheme defined by a morphism
-########################################################
-
-def is_SchemeRationalPoint(x):
- return isinstance(x, SchemeRationalPoint)
-
-class SchemeRationalPoint(SchemePoint):
- def __init__(self, f):
- """
- INPUT:
-
-
- - ``f`` - a morphism of schemes
- """
- SchemePoint.__init__(self, f.codomain(), parent=f.parent())
- self.__f = f
-
- def _repr_(self):
- return "Point on %s defined by the morphism %s" % (self.scheme(),
- self.morphism())
-
- def morphism(self):
- return self.__f
diff --git a/src/sage/schemes/generic/scheme.py b/src/sage/schemes/generic/scheme.py
index 1c66ce9bb33..2c0e3100c86 100644
--- a/src/sage/schemes/generic/scheme.py
+++ b/src/sage/schemes/generic/scheme.py
@@ -77,7 +77,7 @@ class Scheme(Parent):
sage: ProjectiveSpace(4, QQ).category()
Category of schemes over Rational Field
- There is a special and unique `Spec(\ZZ)` that is the default base
+ There is a special and unique `\mathrm{Spec}(\ZZ)` that is the default base
scheme::
sage: Spec(ZZ).base_scheme() is Spec(QQ).base_scheme()
@@ -267,7 +267,7 @@ def __call__(self, *args):
@cached_method
def point_homset(self, S=None):
- """
+ r"""
Return the set of S-valued points of this scheme.
INPUT:
@@ -276,7 +276,7 @@ def point_homset(self, S=None):
OUTPUT:
- The set of morphisms `Spec(S)\to X`.
+ The set of morphisms `\mathrm{Spec}(S) \to X`.
EXAMPLES::
diff --git a/src/sage/schemes/projective/projective_morphism.py b/src/sage/schemes/projective/projective_morphism.py
index cefebbe5ff4..d741c936439 100644
--- a/src/sage/schemes/projective/projective_morphism.py
+++ b/src/sage/schemes/projective/projective_morphism.py
@@ -372,7 +372,7 @@ def __call__(self, x, check=True):
sage: F. = GF(4)
sage: P = T(F)(1, a)
sage: h(P) # needs sage.libs.singular
- (a : a)
+ (1 : 1)
sage: h(P).domain()
Spectrum of Finite Field in a of size 2^2
sage: h.change_ring(F)(P)
diff --git a/src/sage/sets/recursively_enumerated_set.pyx b/src/sage/sets/recursively_enumerated_set.pyx
index bf2a9e9c363..e25dde0485c 100644
--- a/src/sage/sets/recursively_enumerated_set.pyx
+++ b/src/sage/sets/recursively_enumerated_set.pyx
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
r"""
-Recursively enumerated set
+Recursively Enumerated Sets
A set `S` is called recursively enumerable if there is an algorithm that
enumerates the members of `S`. We consider here the recursively enumerated
@@ -9,11 +9,11 @@ sets that are described by some ``seeds`` and a successor function
graded, forest) or not. The elements of a set having a symmetric, graded or
forest structure can be enumerated uniquely without keeping all of them in
memory. Many kinds of iterators are provided in this module: depth first
-search, breadth first search or elements of given depth.
+search, breadth first search and elements of given depth.
See :wikipedia:`Recursively_enumerable_set`.
-See documentation of :func:`RecursivelyEnumeratedSet` below for the
+See the documentation of :func:`RecursivelyEnumeratedSet` below for the
description of the inputs.
AUTHORS:
@@ -22,12 +22,11 @@ AUTHORS:
EXAMPLES:
-No hypothesis on the structure
-------------------------------
+.. RUBRIC:: No hypothesis on the structure
What we mean by "no hypothesis" is that the set is not known
to be a forest, symmetric, or graded. However, it may have other
-structure, like not containing an oriented cycle, that does not
+structure, such as not containing an oriented cycle, that does not
help with the enumeration.
In this example, the seed is 0 and the successor function is either ``+2``
@@ -50,8 +49,7 @@ Depth first search::
sage: [next(it) for _ in range(10)]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
-Symmetric structure
--------------------
+.. RUBRIC:: Symmetric structure
The origin ``(0, 0)`` as seed and the upper, lower, left and right lattice
point as successor function. This function is symmetric since `p` is a
@@ -86,8 +84,7 @@ Levels (elements of given depth)::
sage: sorted(C.graded_component(2))
[(-2, 0), (-1, -1), (-1, 1), (0, -2), (0, 2), (1, -1), (1, 1), (2, 0)]
-Graded structure
-----------------
+.. RUBRIC:: Graded structure
Identity permutation as seed and ``permutohedron_succ`` as successor
function::
@@ -137,11 +134,10 @@ Graded components (set of elements of the same depth)::
sage: sorted(R.graded_component(10))
[[5, 4, 3, 2, 1]]
-Forest structure
-----------------
+.. RUBRIC:: Forest structure (Example 1)
The set of words over the alphabet `\{a,b\}` can be generated from the
-empty word by appending letter `a` or `b` as a successor function. This set
+empty word by appending the letter `a` or `b` as a successor function. This set
has a forest structure::
sage: seeds = ['']
@@ -162,9 +158,6 @@ Breadth first search iterator::
sage: [next(it) for _ in range(6)]
['', 'a', 'b', 'aa', 'ab', 'ba']
-Example: Forest structure
--------------------------
-
This example was provided by Florent Hivert.
How to define a set using those classes?
@@ -175,41 +168,23 @@ classes being very similar):
.. MATH::
- \begin{picture}(-300,0)(600,0)
- % Root
- \put(0,0){\circle*{7}}
- \put(0,10){\makebox(0,10){``\ ''}}
- % First Children
- \put(-150,-60){\makebox(0,10){``a''}}
- \put(0,-60){\makebox(0,10){``b''}}
- \put(150,-60){\makebox(0,10){``c''}}
- \multiput(-150,-70)(150,0){3}{\circle*{7}}
- % Second children
- \put(-200,-130){\makebox(0,10){``aa''}}
- \put(-150,-130){\makebox(0,10){``ab''}}
- \put(-100,-130){\makebox(0,10){``ac''}}
- \put(-50,-130){\makebox(0,10){``ba''}}
- \put(0,-130){\makebox(0,10){``bb''}}
- \put(50,-130){\makebox(0,10){``bc''}}
- \put(100,-130){\makebox(0,10){``ca''}}
- \put(150,-130){\makebox(0,10){``cb''}}
- \put(200,-130){\makebox(0,10){``cc''}}
- \multiput(-200,-140)(50,0){9}{\circle*{7}}
- % Legend
- \put(100,-5){\makebox(0,10)[l]{1) An initial element}}
- \put(-250,-5){\makebox(0,10)[l]{2) A function of an element enumerating}}
- \put(-235,-20){\makebox(0,10)[l]{its children (if any)}}
- % Arrows
- \thicklines
- \put(0,-10){\vector(0,-1){30}}
- \put(-15,-5){\vector(-2,-1){110}}
- \put(15,-5){\vector(2,-1){110}}
- \multiput(-150,-80)(150,0){3}{\vector(0,-1){30}}
- \multiput(-160,-80)(150,0){3}{\vector(-1,-1){30}}
- \multiput(-140,-80)(150,0){3}{\vector(1,-1){30}}
- \put(90,0){\vector(-1,0){70}}
- \put(-215,-30){\vector(1,-1){40}}
- \end{picture}
+ \begin{array}{ccc}
+ & \emptyset \\
+ \hfil\swarrow & \downarrow & \searrow\hfil\\
+ a & b & c \\
+ \begin{array}{ccc}
+ \swarrow & \downarrow & \searrow \\
+ aa & ab & ac \\
+ \end{array} &
+ \begin{array}{ccc}
+ \swarrow & \downarrow & \searrow \\
+ ba & bb & bc \\
+ \end{array} &
+ \begin{array}{ccc}
+ \swarrow & \downarrow & \searrow \\
+ ca & cb & cc \\
+ \end{array}
+ \end{array}
For the previous example, the two necessary pieces of information are:
@@ -219,7 +194,7 @@ For the previous example, the two necessary pieces of information are:
lambda x: [x + letter for letter in ['a', 'b', 'c']
-This would actually describe an **infinite** set, as such rules describes
+This would actually describe an **infinite** set, as such rules describe
"all words" on 3 letters. Hence, it is a good idea to replace the function by::
lambda x: [x + letter for letter in ['a', 'b', 'c']] if len(x) < 2 else []
@@ -249,14 +224,13 @@ or::
sage: S.list()
['', 'a', 'aa', 'ab', 'ac', 'b', 'ba', 'bb', 'bc', 'c', 'ca', 'cb', 'cc']
-Example: Forest structure 2
----------------------------
+.. RUBRIC:: Forest structure (Example 2)
This example was provided by Florent Hivert.
Here is a little more involved example. We want to iterate through all
permutations of a given set `S`. One solution is to take elements of `S` one
-by one an insert them at every positions. So a node of the generating tree
+by one and insert them at every position. So a node of the generating tree
contains two pieces of information:
- the list ``lst`` of already inserted element;
@@ -320,7 +294,7 @@ def RecursivelyEnumeratedSet(seeds, successors, structure=None,
A set `S` is called recursively enumerable if there is an algorithm that
enumerates the members of `S`. We consider here the recursively
- enumerated set that are described by some ``seeds`` and a successor
+ enumerated sets that are described by some ``seeds`` and a successor
function ``successors``.
Let `U` be a set and ``successors`` `:U \to 2^U` be a successor function
@@ -406,7 +380,7 @@ def RecursivelyEnumeratedSet(seeds, successors, structure=None,
.. WARNING::
- If you do not set the good structure, you might obtain bad results,
+ If you do not set a good structure, you might obtain bad results,
like elements generated twice::
sage: f = lambda a: [a-1,a+1]
@@ -781,8 +755,8 @@ cdef class RecursivelyEnumeratedSet_generic(Parent):
r"""
Iterate over the elements of ``self`` of given depth.
- An element of depth `n` can be obtained applying `n` times the
- successor function to a seed.
+ An element of depth `n` can be obtained by applying the
+ successor function `n` times to a seed.
INPUT:
@@ -847,7 +821,7 @@ cdef class RecursivelyEnumeratedSet_generic(Parent):
r"""
Iterate on the elements of ``self`` (breadth first).
- This code remembers every elements generated and uses python
+ This code remembers every element generated and uses python
queues. It is 3 times slower than the other one.
See :wikipedia:`Breadth-first_search`.
@@ -876,7 +850,7 @@ cdef class RecursivelyEnumeratedSet_generic(Parent):
r"""
Iterate on the elements of ``self`` (in no particular order).
- This code remembers every elements generated.
+ This code remembers every element generated.
TESTS:
@@ -905,7 +879,7 @@ cdef class RecursivelyEnumeratedSet_generic(Parent):
r"""
Iterate on the elements of ``self`` (depth first).
- This code remembers every elements generated.
+ This code remembers every element generated.
The elements are traversed right-to-left, so the last element returned
by the successor function is visited first.
@@ -1552,7 +1526,7 @@ def search_forest_iterator(roots, children, algorithm='depth'):
[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1],
[1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
- This allows for iterating trough trees of infinite depth::
+ This allows for iterating through trees of infinite depth::
sage: it = search_forest_iterator([[]], lambda l: [l+[0], l+[1]], algorithm='breadth')
sage: [ next(it) for i in range(16) ]
@@ -1574,9 +1548,9 @@ def search_forest_iterator(roots, children, algorithm='depth'):
[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
"""
# Little trick: the same implementation handles both depth and
- # breadth first search. Setting position to -1 makes a depth search
+ # breadth first search. Setting position to -1 results in a depth search
# (you ask the children for the last node you met). Setting
- # position on 0 makes a breadth search (enumerate all the
+ # position on 0 results in a breadth search (enumerate all the
# descendants of a node before going on to the next father)
if algorithm == 'depth':
position = -1
@@ -1920,8 +1894,8 @@ class RecursivelyEnumeratedSet_forest(Parent):
def _elements_of_depth_iterator_rec(self, depth=0):
r"""
Return an iterator over the elements of ``self`` of given depth.
- An element of depth `n` can be obtained applying `n` times the
- children function from a root. This function is not affected
+ An element of depth `n` can be obtained by applying the
+ children function `n` times from a root. This function is not affected
by post processing.
EXAMPLES::
@@ -1951,8 +1925,8 @@ class RecursivelyEnumeratedSet_forest(Parent):
def elements_of_depth_iterator(self, depth=0):
r"""
Return an iterator over the elements of ``self`` of given depth.
- An element of depth `n` can be obtained applying `n` times the
- children function from a root.
+ An element of depth `n` can be obtained by applying the
+ children function `n` times from a root.
EXAMPLES::
@@ -2010,7 +1984,7 @@ class RecursivelyEnumeratedSet_forest(Parent):
depth first search and breadth first search failed. The
following example enumerates all ordered pairs of nonnegative
integers, starting from an infinite set of roots, where each
- roots has an infinite number of children::
+ root has an infinite number of children::
sage: from sage.sets.recursively_enumerated_set import RecursivelyEnumeratedSet_forest
sage: S = RecursivelyEnumeratedSet_forest(Family(NN, lambda x : (x, 0)),
diff --git a/src/sage/stats/all.py b/src/sage/stats/all.py
index 69fb8f01abd..3b4c8c4ff52 100644
--- a/src/sage/stats/all.py
+++ b/src/sage/stats/all.py
@@ -1,3 +1,4 @@
+import sage.stats.distributions.catalog as distributions
from .r import ttest
from .basic_stats import (mean, mode, std, variance, median, moving_average)
diff --git a/src/sage/stats/distributions/all.py b/src/sage/stats/distributions/all.py
index e69de29bb2d..d37a8563ec6 100644
--- a/src/sage/stats/distributions/all.py
+++ b/src/sage/stats/distributions/all.py
@@ -0,0 +1,6 @@
+# We lazy_import the following modules since they import numpy which
+# slows down sage startup
+from sage.misc.lazy_import import lazy_import
+lazy_import("sage.stats.distributions.discrete_gaussian_integer", ["DiscreteGaussianDistributionIntegerSampler"])
+lazy_import("sage.stats.distributions.discrete_gaussian_lattice", ["DiscreteGaussianDistributionLatticeSampler"])
+lazy_import("sage.stats.distributions.discrete_gaussian_polynomial", ["DiscreteGaussianDistributionPolynomialSampler"])
diff --git a/src/sage/stats/distributions/catalog.py b/src/sage/stats/distributions/catalog.py
new file mode 100644
index 00000000000..5f252494e13
--- /dev/null
+++ b/src/sage/stats/distributions/catalog.py
@@ -0,0 +1,34 @@
+r"""
+Index of distributions
+
+This catalogue includes the samplers for statistical distributions listed below.
+
+Let ```` indicate pressing the :kbd:`Tab` key. So begin by typing
+``algebras.`` to the see the currently implemented named algebras.
+
+- :class:`distributions.discrete_gaussian_integer.DiscreteGaussianDistributionIntegerSampler
+ `
+- :class:`distributions.discrete_gaussian_lattice.DiscreteGaussianDistributionLatticeSampler
+ `
+- :class:`distributions.discrete_gaussian_polynomial.DiscreteGaussianDistributionPolynomialSampler
+ `
+
+To import these names into the global namespace, use::
+
+ sage: from sage.stats.distributions.catalog import *
+
+"""
+#*****************************************************************************
+# Copyright (C) 2024 Gareth Ma
+#
+# Distributed under the terms of the GNU General Public License (GPL),
+# version 2 or later (at your preference).
+#
+# http://www.gnu.org/licenses/
+#*****************************************************************************
+
+from sage.misc.lazy_import import lazy_import
+lazy_import("sage.stats.distributions.discrete_gaussian_integer", ["DiscreteGaussianDistributionIntegerSampler"])
+lazy_import("sage.stats.distributions.discrete_gaussian_lattice", ["DiscreteGaussianDistributionLatticeSampler"])
+lazy_import("sage.stats.distributions.discrete_gaussian_polynomial", ["DiscreteGaussianDistributionPolynomialSampler"])
+del lazy_import
diff --git a/src/sage/stats/distributions/discrete_gaussian_lattice.py b/src/sage/stats/distributions/discrete_gaussian_lattice.py
index 08790bf92be..6667b2fb5cf 100644
--- a/src/sage/stats/distributions/discrete_gaussian_lattice.py
+++ b/src/sage/stats/distributions/discrete_gaussian_lattice.py
@@ -3,28 +3,32 @@
Discrete Gaussian Samplers over Lattices
This file implements oracles which return samples from a lattice following a
-discrete Gaussian distribution. That is, if `σ` is big enough relative to the
-provided basis, then vectors are returned with a probability proportional to
-`\exp(-|x-c|_2^2/(2σ^2))`. More precisely lattice vectors in `x ∈ Λ` are
-returned with probability:
+discrete Gaussian distribution. That is, if `\sigma` is big enough relative to
+the provided basis, then vectors are returned with a probability proportional
+to `\exp(-|x - c|_2^2 / (2\sigma^2))`. More precisely lattice vectors in `x \in
+\Lambda` are returned with probability:
- `\exp(-|x-c|_2^2/(2σ²))/(∑_{x ∈ Λ} \exp(-|x|_2^2/(2σ²)))`
+.. MATH::
+
+ \frac{\exp(-|x - c|_2^2 / (2\sigma^2))}{\sum_{v \in \Lambda} \exp(-|v|_2^2 /
+ (2\sigma^2))}.
AUTHORS:
- Martin Albrecht (2014-06-28): initial version
+- Gareth Ma (2023-09-22): implement non-spherical sampling
+
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^10, 3.0)
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^10, 3.0)
sage: D(), D(), D() # random
((3, 0, -5, 0, -1, -3, 3, 3, -7, 2), (4, 0, 1, -2, -4, -4, 4, 0, 1, -4), (-3, 0, 4, 5, 0, 1, 3, 2, 0, -1))
sage: a = D()
sage: a.parent()
Ambient free module of rank 10 over the principal ideal domain Integer Ring
"""
-#******************************************************************************
+# ******************************************************************************
#
# DGS - Discrete Gaussian Samplers
#
@@ -54,17 +58,20 @@
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the FreeBSD Project.
-#*****************************************************************************/
+# *****************************************************************************/
from sage.functions.log import exp
-from sage.functions.other import ceil
from sage.rings.real_mpfr import RealField
-from sage.rings.real_mpfr import RR
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
-from .discrete_gaussian_integer import DiscreteGaussianDistributionIntegerSampler
+from sage.stats.distributions.discrete_gaussian_integer import DiscreteGaussianDistributionIntegerSampler
from sage.structure.sage_object import SageObject
-from sage.matrix.constructor import matrix, identity_matrix
+from sage.misc.cachefunc import cached_method
+from sage.misc.functional import sqrt
+from sage.misc.prandom import normalvariate
+from sage.misc.verbose import verbose
+from sage.symbolic.constants import pi
+from sage.matrix.constructor import matrix
from sage.modules.free_module import FreeModule
from sage.modules.free_module_element import vector
@@ -90,9 +97,9 @@ def _iter_vectors(n, lower, upper, step=None):
"""
if step is None:
if ZZ(lower) >= ZZ(upper):
- raise ValueError("Expected lower < upper, but got %d >= %d" % (lower, upper))
+ raise ValueError("expected lower < upper, but got %d >= %d" % (lower, upper))
if ZZ(n) <= 0:
- raise ValueError("Expected n>0 but got %d <= 0" % n)
+ raise ValueError("expected n>0 but got %d <= 0" % n)
step = n
assert step > 0
@@ -115,9 +122,8 @@ class DiscreteGaussianDistributionLatticeSampler(SageObject):
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^10, 3.0); D
- Discrete Gaussian sampler with σ = 3.000000, c=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) over lattice with basis
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^10, 3.0); D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) over lattice with basis
[1 0 0 0 0 0 0 0 0 0]
[0 1 0 0 0 0 0 0 0 0]
@@ -133,10 +139,9 @@ class DiscreteGaussianDistributionLatticeSampler(SageObject):
We plot a histogram::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
sage: import warnings
sage: warnings.simplefilter('ignore', UserWarning)
- sage: D = DiscreteGaussianDistributionLatticeSampler(identity_matrix(2), 3.0)
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(identity_matrix(2), 3.0)
sage: S = [D() for _ in range(2^12)]
sage: l = [vector(v.list() + [S.count(v)]) for v in set(S)]
sage: list_plot3d(l, point_list=True, interpolation='nn') # needs sage.plot
@@ -156,24 +161,24 @@ def compute_precision(precision, sigma):
INPUT:
- - ``precision`` - an integer `> 53` nor ``None``.
+ - ``precision`` - an integer `>= 53` nor ``None``.
- ``sigma`` - if ``precision`` is ``None`` then the precision of
``sigma`` is used.
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(100, RR(3))
+ sage: DGL = distributions.DiscreteGaussianDistributionLatticeSampler
+ sage: DGL.compute_precision(100, RR(3))
100
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(100, RealField(200)(3))
+ sage: DGL.compute_precision(100, RealField(200)(3))
100
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(100, 3)
+ sage: DGL.compute_precision(100, 3)
100
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(None, RR(3))
+ sage: DGL.compute_precision(None, RR(3))
53
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(None, RealField(200)(3))
+ sage: DGL.compute_precision(None, RealField(200)(3))
200
- sage: DiscreteGaussianDistributionLatticeSampler.compute_precision(None, 3)
+ sage: DGL.compute_precision(None, 3)
53
"""
@@ -185,28 +190,29 @@ def compute_precision(precision, sigma):
precision = max(53, precision)
return precision
- def _normalisation_factor_zz(self, tau=3):
+ def _normalisation_factor_zz(self, tau=None, prec=None):
r"""
- This function returns an approximation of `∑_{x ∈ \ZZ^n}
- \exp(-|x|_2^2/(2σ²))`, i.e. the normalisation factor such that the sum
- over all probabilities is 1 for `\ZZⁿ`.
+ This function returns an approximation of `\sum_{x \in B}
+ \exp(-|x|_2^2 / (2\sigma^2))`, i.e. the normalization factor such that the sum
+ over all probabilities is 1 for `B`, via Poisson summation.
- If this ``self.B`` is not an identity matrix over `\ZZ` a
- :class:`NotImplementedError` is raised.
INPUT:
- - ``tau`` -- all vectors `v` with `|v|_∞ ≤ τ·σ` are enumerated
- (default: ``3``).
+ - ``tau`` -- (default: ``None``) all vectors `v` with `|v|_2^2 \leq
+ \tau \sigma` are enumerated; if none is provided, enumerate vectors
+ with increasing norm until the sum converges to given precision. For
+ high dimension lattice, this is recommended.
+
+ - ``prec`` -- (default: ``None``) passed to :meth:`compute_precision`
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
sage: n = 3; sigma = 1.0
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^n, sigma)
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^n, sigma)
sage: f = D.f
- sage: c = D._normalisation_factor_zz(); c # needs sage.symbolic
- 15.528...
+ sage: nf = D._normalisation_factor_zz(); nf
+ 15.7496...
sage: from collections import defaultdict
sage: counter = defaultdict(Integer)
@@ -222,7 +228,7 @@ def _normalisation_factor_zz(self, tau=3):
sage: while v not in counter:
....: add_samples(1000)
- sage: while abs(m*f(v)*1.0/c/counter[v] - 1.0) >= 0.1: # needs sage.symbolic
+ sage: while abs(m*f(v)*1.0/nf/counter[v] - 1.0) >= 0.1:
....: add_samples(1000)
sage: v = vector(ZZ, n, (-1, 2, 3))
@@ -230,43 +236,209 @@ def _normalisation_factor_zz(self, tau=3):
sage: while v not in counter:
....: add_samples(1000)
- sage: while abs(m*f(v)*1.0/c/counter[v] - 1.0) >= 0.2: # long time, needs sage.symbolic
+ sage: while abs(m*f(v)*1.0/nf/counter[v] - 1.0) >= 0.2: # long time
....: add_samples(1000)
+
+ sage: DGL = distributions.DiscreteGaussianDistributionLatticeSampler
+ sage: D = DGL(ZZ^8, 0.5)
+ sage: D._normalisation_factor_zz(tau=3)
+ 3.1653...
+ sage: D._normalisation_factor_zz()
+ 6.8249...
+ sage: D = DGL(ZZ^8, 1000)
+ sage: round(D._normalisation_factor_zz(prec=100))
+ 1558545456544038969634991553
+
+ sage: M = Matrix(ZZ, [[1, 3, 0], [-2, 5, 1], [3, -4, 2]])
+ sage: D = DGL(M, 1.7)
+ sage: D._normalisation_factor_zz() # long time
+ 7247.1975...
+
+ sage: M = Matrix(ZZ, [[1, 3, 0], [-2, 5, 1]])
+ sage: D = DGL(M, 3)
+ sage: D._normalisation_factor_zz()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: basis must be a square matrix for now
+
+ sage: D = DGL(ZZ^3, c=(1/2, 0, 0))
+ sage: D._normalisation_factor_zz()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: lattice must contain 0 for now
+
+ sage: D = DGL(Matrix(3, 3, 1/2))
+ sage: D._normalisation_factor_zz()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: lattice must be integral for now
"""
- if self.B != identity_matrix(ZZ, self.B.nrows()):
- raise NotImplementedError("This function is only implemented when B is an identity matrix.")
+ # If σ > 1:
+ # We use the Fourier transform g(t) of f(x) = exp(-k^2 / 2σ^2), but
+ # taking the norm of vector t^2 as input, and with norm_factor factored.
+ # If σ ≤ 1:
+ # The formula in docstring converges quickly since it has -1 / σ^2 in
+ # the exponent
+ def f_or_hat(x):
+ # Fun fact: If you remove this R() and delay the call to return,
+ # It might give an error due to precision error. For example,
+ # RR(1 + 100 * exp(-5.0 * pi^2)) == 0
+
+ if sigma > 1:
+ return R(exp(-pi**2 * (2 * sigma**2) * x))
+
+ return R(exp(-x / (2 * sigma**2)))
+
+ if not self.is_spherical:
+ # TODO: This is only a poor approximation placeholder.
+ # It should be easy to implement, since the Fourier transform
+ # is essentially the same, but I can't figure out how to
+ # tweak the `.qfrep` call below correctly.
+ from warnings import warn
+ warn("Note: `_normalisation_factor_zz` has not been properly "
+ "implemented for non-spherical distributions.")
+ import itertools
+ from sage.functions.log import log
+ basis = self.B.LLL()
+ base = vector(ZZ, [v.round() for v in basis.solve_left(self._c)])
+ BOUND = max(1, (self._RR(log(10**4, self.n)).ceil() - 1) // 2)
+ if BOUND > 10:
+ BOUND = 10
+ coords = itertools.product(range(-BOUND, BOUND + 1), repeat=self.n)
+ return sum(self.f((vector(u) + base) * self.B) for u in coords)
+
+ if self.B.nrows() != self.B.ncols():
+ raise NotImplementedError("basis must be a square matrix for now")
+
+ if self.is_spherical and not self._c_in_lattice:
+ raise NotImplementedError("lattice must contain 0 for now")
+
+ if self.B.base_ring() != ZZ:
+ raise NotImplementedError("lattice must be integral for now")
- f = self.f
- n = self.B.ncols()
sigma = self._sigma
- return sum(f(x) for x in _iter_vectors(n, -ceil(tau * sigma),
- ceil(tau * sigma)))
+ prec = DiscreteGaussianDistributionLatticeSampler.compute_precision(
+ prec, sigma
+ )
+ R = RealField(prec=prec)
+ if sigma > 1:
+ det = self.B.det()
+ norm_factor = (sigma * sqrt(2 * pi))**self.n / det
+ else:
+ det = 1
+ norm_factor = 1
+
+ # qfrep computes theta series of a quadratic form, which is *half* the
+ # generating function of number of vectors with given norm (and no 0)
+ Q = self.Q
+ if tau is not None:
+ freq = Q.__pari__().qfrep(tau * sigma, 0)
+ res = R(1)
+ for x, fq in enumerate(freq):
+ res += 2 * ZZ(fq) * f_or_hat((x + 1) / det**self.n)
+ return R(norm_factor * res)
+
+ res = R(1)
+ bound = 0
+ # There might still be precision issue but whatever
+ while True:
+ bound += 1
+ cnt = ZZ(Q.__pari__().qfrep(bound, 0)[bound - 1])
+ inc = 2 * cnt * f_or_hat(bound / det**self.n)
+ if cnt > 0 and res == res + inc:
+ return R(norm_factor * res)
+ res += inc
+
+ @cached_method
+ def _maximal_r(self):
+ r"""
+ This function computes the largest value `r > 0` such that `\Sigma - r^2BB^T`
+ is positive definite.
+
+ This is equivalent to finding `\lambda_1(\Sigma / Q) = 1 / \lambda_n(Q
+ / \Sigma)`, which is done via the Power iteration method.
- def __init__(self, B, sigma=1, c=None, precision=None):
+ EXAMPLES::
+
+ sage: n = 3
+ sage: Sigma = Matrix(ZZ, [[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: c = vector(ZZ, [7, 2, 5])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^n, Sigma, c)
+ sage: r = D._maximal_r(); r
+ 0.58402...
+ sage: e_vals = (D.sigma() - r^2 * D.Q).eigenvalues()
+ sage: assert all(e_val >= -1e-12 for e_val in e_vals)
+ """
+ assert not self.is_spherical
+
+ Q = self.Q.change_ring(self._RR) / self._sigma.change_ring(self._RR)
+ v = Q[0].change_ring(self._RR)
+ cnt = 0
+ while cnt < 10000:
+ nv = (Q * v).normalized()
+ if (nv - v).norm() < 1e-12:
+ break
+ v = nv
+ cnt += 1
+ res = (v[0] / (Q * v)[0]).sqrt()
+ return res
+
+ def _randomise(self, v):
r"""
- Construct a discrete Gaussian sampler over the lattice `Λ(B)`
+ Randomly round to the latice coset `\ZZ + v` with Gaussian parameter
+ `r`. Used at :meth:`_call_non_spherical`.
+
+ REFERENCES:
+
+ - [Pei2010]_, Section 4.1
+
+ EXAMPLES::
+
+ sage: Sigma = Matrix(ZZ, [[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, Sigma)
+ sage: all(D._randomise([0, 0, 0]).norm() <= 16 for _ in range(100))
+ True
+ """
+ return vector(ZZ, [DiscreteGaussianDistributionIntegerSampler(self.r, c=vi)() for vi in v])
+
+ def __init__(self, B, sigma=1, c=0, r=None, precision=None, sigma_basis=False):
+ r"""
+ Construct a discrete Gaussian sampler over the lattice `\Lambda(B)`
with parameter ``sigma`` and center `c`.
INPUT:
- - ``B`` -- a basis for the lattice, one of the following:
+ - ``B`` -- a (row) basis for the lattice, one of the following:
- an integer matrix,
- - an object with a ``matrix()`` method, e.g. ``ZZ^n``, or
- - an object where ``matrix(B)`` succeeds, e.g. a list of vectors.
+ - an object with a ``.matrix()`` method, e.g. ``ZZ^n``, or
+ - an object where ``matrix(B)`` succeeds, e.g. a list of vectors
- - ``sigma`` -- Gaussian parameter `σ>0`.
- - ``c`` -- center `c`, any vector in `\ZZ^n` is supported, but `c ∈ Λ(B)` is faster.
- - ``precision`` -- bit precision `≥ 53`.
+ - ``sigma`` -- Gaussian parameter, one of the following:
+
+ - a real number `\sigma > 0` (spherical),
+ - a positive definite matrix `\Sigma` (non-spherical), or
+ - any matrix-like ``S``, equivalent to `\Sigma = SS^T`, when
+ ``sigma_basis`` is set
+
+ - ``c`` -- (default: 0) center `c`, any vector in `\ZZ^n` is
+ supported, but `c \in \Lambda(B)` is faster
+
+ - ``r`` -- (default: ``None``) rounding parameter `r` as defined in
+ [Pei2010]_; ignored for spherical Gaussian parameter; if not provided,
+ set to be the maximal possible such that `\Sigma - rBB^T` is positive
+ definite
+ - ``precision`` -- bit precision `\geq 53`.
+ - ``sigma_basis`` -- (default: ``False``) When set, ``sigma`` is treated as
+ a (row) basis, i.e. the covariance matrix is computed by `\Sigma = SS^T`
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
sage: n = 2; sigma = 3.0
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^n, sigma)
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^n, sigma)
sage: f = D.f
- sage: c = D._normalisation_factor_zz(); c # needs sage.symbolic
- 56.2162803067524
+ sage: nf = D._normalisation_factor_zz(); nf # needs sage.symbolic
+ 56.5486677646...
sage: from collections import defaultdict
sage: counter = defaultdict(Integer)
@@ -279,34 +451,102 @@ def __init__(self, B, sigma=1, c=None, precision=None):
sage: v = vector(ZZ, n, (-3, -3))
sage: v.set_immutable()
- sage: while v not in counter:
- ....: add_samples(1000)
- sage: while abs(m*f(v)*1.0/c/counter[v] - 1.0) >= 0.1: # needs sage.symbolic
+ sage: while v not in counter: add_samples(1000)
+ sage: while abs(m*f(v)*1.0/nf/counter[v] - 1.0) >= 0.1: # needs sage.symbolic
....: add_samples(1000)
+ sage: counter = defaultdict(Integer)
sage: v = vector(ZZ, n, (0, 0))
sage: v.set_immutable()
sage: while v not in counter:
....: add_samples(1000)
- sage: while abs(m*f(v)*1.0/c/counter[v] - 1.0) >= 0.1: # needs sage.symbolic
+ sage: while abs(m*f(v)*1.0/nf/counter[v] - 1.0) >= 0.1: # needs sage.symbolic
....: add_samples(1000)
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: qf = QuadraticForm(matrix(3, [2, 1, 1, 1, 2, 1, 1, 1, 2]))
- sage: D = DiscreteGaussianDistributionLatticeSampler(qf, 3.0); D # needs sage.symbolic
- Discrete Gaussian sampler with σ = 3.000000, c=(0, 0, 0) over lattice with basis
+ Spherical covariance are automatically handled::
+
+ sage: distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, sigma=Matrix(3, 3, 2))
+ Discrete Gaussian sampler with Gaussian parameter σ = 2.00000000000000, c=(0, 0, 0) over lattice with basis
+
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
+
+ The sampler supports non-spherical covariance in the form of a Gram
+ matrix::
+
+ sage: n = 3
+ sage: Sigma = Matrix(ZZ, [[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: c = vector(ZZ, [7, 2, 5])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^n, Sigma, c)
+ sage: nf = D._normalisation_factor_zz(); nf # This has not been properly implemented
+ 63.76927...
+ sage: while v not in counter: add_samples(1000)
+ sage: while abs(m*f(v)*1.0/nf/counter[v] - 1.0) >= 0.1: add_samples(1000)
+
+ If the covariance provided is not positive definite, an error is thrown::
+
+ sage: Sigma = Matrix(ZZ, [[0, 1], [1, 0]])
+ sage: distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^2, Sigma)
+ Traceback (most recent call last):
+ ...
+ RuntimeError: Sigma(=[0.000000000000000 1.00000000000000]
+ [ 1.00000000000000 0.000000000000000]) is not positive definite
+
+ The sampler supports passing a basis for the covariance::
+
+ sage: n = 3
+ sage: S = Matrix(ZZ, [[2, 0, 0], [-1, 3, 0], [2, -1, 1]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^n, S, sigma_basis=True)
+ sage: D.sigma()
+ [ 4.00000000000000 -2.00000000000000 4.00000000000000]
+ [-2.00000000000000 10.0000000000000 -5.00000000000000]
+ [ 4.00000000000000 -5.00000000000000 6.00000000000000]
+
+ The non-spherical sampler supports offline computation to speed up
+ sampling. This will be useful when changing the center `c` is supported.
+ The difference is more significant for larger matrices. For 128x128 we
+ observe a 4x speedup (86s -> 20s)::
+
+ sage: D.offline_samples = []
+ sage: T = 2**12
+ sage: L = [D() for _ in range(T)] # 560ms
+ sage: D.add_offline_samples(T) # 150ms
+ sage: L = [D() for _ in range(T)] # 370ms
+
+ We can also initialise with matrix-like objects::
+
+ sage: qf = matrix(3, [2, 1, 1, 1, 2, 1, 1, 1, 2])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(qf, 3.0); D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(0, 0, 0) over lattice with basis
[2 1 1]
[1 2 1]
[1 1 2]
- sage: D().parent() is D.c.parent() # needs sage.symbolic
+ sage: D().parent() is D.c().parent()
True
"""
precision = DiscreteGaussianDistributionLatticeSampler.compute_precision(precision, sigma)
self._RR = RealField(precision)
- self._sigma = self._RR(sigma)
-
+ # Check if sigma is a (real) number or a scaled identity matrix
+ self.is_spherical = True
+ try:
+ self._sigma = self._RR(sigma)
+ except TypeError:
+ self._sigma = matrix(self._RR, sigma)
+ # Will it be "annoying" if a matrix Sigma has different behaviour
+ # sometimes? There should be a parameter in the consrtuctor
+ if self._sigma == self._sigma[0, 0]:
+ self._sigma = self._RR(self._sigma[0, 0])
+ else:
+ if sigma_basis:
+ self._sigma = self._sigma * self._sigma.T
+ if not self._sigma.is_positive_definite():
+ raise RuntimeError(f"Sigma(={self._sigma}) is not positive definite")
+ self.is_spherical = False
+
+ # TODO: Support taking a basis for the covariance
try:
B = matrix(B)
except (TypeError, ValueError):
@@ -317,40 +557,82 @@ def __init__(self, B, sigma=1, c=None, precision=None):
except AttributeError:
pass
+ self.n = B.ncols()
self.B = B
+ self.Q = B * B.T
self._G = B.gram_schmidt()[0]
+ self._c_in_lattice = False
- try:
- c = vector(ZZ, B.ncols(), c)
- except TypeError:
- try:
- c = vector(QQ, B.ncols(), c)
- except TypeError:
- c = vector(RR, B.ncols(), c)
+ self.D = None
+ self.VS = None
+ self._c_mul_B_inv = None
+ self.r = r
- self._c = c
+ self.set_c(c)
- self.f = lambda x: exp(-(vector(ZZ, B.ncols(), x) - c).norm() ** 2 / (2 * self._sigma ** 2))
+ def _precompute_data(self):
+ r"""
+ Precompute basis data. Do not call this method directly.
- # deal with trivial case first, it is common
- if self._G == 1 and self._c == 0:
- self._c_in_lattice = True
- D = DiscreteGaussianDistributionIntegerSampler(sigma=sigma)
- self.D = tuple([D for _ in range(self.B.nrows())])
- self.VS = FreeModule(ZZ, B.nrows())
- return
+ EXAMPLES::
+
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
+ sage: D.set_c((2, 0, 0))
+ sage: D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(2, 0, 0) over lattice with basis
+
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
- w = B.solve_left(c)
- if w in ZZ ** B.nrows():
- self._c_in_lattice = True
- D = []
- for i in range(self.B.nrows()):
- sigma_ = self._sigma / self._G[i].norm()
- D.append(DiscreteGaussianDistributionIntegerSampler(sigma=sigma_))
- self.D = tuple(D)
- self.VS = FreeModule(ZZ, B.nrows())
+ .. NOTE::
+
+ Do not call this method directly, it is called automatically from
+ :func:`DiscreteGaussianDistributionLatticeSampler.__init__`.
+ """
+ if self.is_spherical:
+ # deal with trivial case first, it is common
+ if self._G == 1 and self._c == 0:
+ self._c_in_lattice = True
+ D = DiscreteGaussianDistributionIntegerSampler(sigma=self._sigma)
+ self.D = tuple([D for _ in range(self.B.nrows())])
+ self.VS = FreeModule(ZZ, self.B.nrows())
+
+ else:
+ w = self.B.solve_left(self._c)
+ if w in ZZ ** self.B.nrows():
+ self._c_in_lattice = True
+ D = []
+ for i in range(self.B.nrows()):
+ sigma_ = self._sigma / self._G[i].norm()
+ D.append(DiscreteGaussianDistributionIntegerSampler(sigma=sigma_))
+ self.D = tuple(D)
+ self.VS = FreeModule(ZZ, self.B.nrows())
else:
- self._c_in_lattice = False
+ # Variables Sigma2 and r are from [Pei2010]_
+ # TODO: B is implicitly assumed to be full-rank for the
+ # non-spherical case. Remove this assumption :)
+
+ # Offline samples of B⁻¹D₁
+ self.offline_samples = []
+ self.B_inv = self.B.inverse()
+ self.sigma_inv = self._sigma.inverse()
+ self._c_mul_B_inv = self._c * self.B_inv
+
+ if self.r is None:
+ # Compute the maximal r such that (Sigma - r^2 * Q) > 0
+ self.r = self._maximal_r() * 0.9999
+ self.r = self._RR(self.r)
+
+ Sigma2 = self._sigma - self.r**2 * self.Q
+ try:
+ verbose(f"Computing Cholesky decomposition of a {Sigma2.dimensions()} matrix")
+ self.B2 = Sigma2.cholesky().T
+ self.B2_B_inv = self.B2 * self.B_inv
+ except ValueError:
+ raise ValueError("Σ₂ is not positive definite. Is your "
+ f"r(={self.r}) too large? It should be at most "
+ f"{self._maximal_r()}")
def __call__(self):
r"""
@@ -358,96 +640,167 @@ def __call__(self):
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
sage: L = [D() for _ in range(2^12)]
sage: mean_L = sum(L) / len(L)
- sage: norm(mean_L.n() - D.c) < 0.25
+ sage: norm(mean_L.n() - D.c()) < 0.25
True
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1/2,0,0))
- sage: L = [D() for _ in range(2^12)] # long time
- sage: mean_L = sum(L) / len(L) # long time
- sage: norm(mean_L.n() - D.c) < 0.25 # long time
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1/2,0,0))
+ sage: L = [D() for _ in range(2^12)] # long time
+ sage: mean_L = sum(L) / len(L) # long time
+ sage: norm(mean_L.n() - D.c()) < 0.25 # long time
True
"""
- if self._c_in_lattice:
+ if not self.is_spherical:
+ v = self._call_non_spherical()
+ elif self._c_in_lattice:
v = self._call_in_lattice()
else:
v = self._call()
v.set_immutable()
return v
- @property
+ def f(self, x):
+ r"""
+ Compute the Gaussian `\rho_{\Lambda, c, \Sigma}`.
+
+ EXAMPLES::
+
+ sage: Sigma = Matrix(ZZ, [[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, Sigma)
+ sage: D.f([1, 0, 1])
+ 0.802518797962478
+ sage: D.f([1, 0, 3])
+ 0.00562800641440405
+ """
+ try:
+ x = vector(ZZ, self.n, x)
+ except TypeError:
+ try:
+ x = vector(QQ, self.n, x)
+ except TypeError:
+ x = vector(self._RR, self.n, x)
+ x -= self._c
+ if self.is_spherical:
+ return exp(-x.norm() ** 2 / (2 * self._sigma**2))
+ return exp(-x * self.sigma_inv * x / 2)
+
def sigma(self):
r"""
- Gaussian parameter `σ`.
+ Gaussian parameter `\sigma`.
- Samples from this sampler will have expected norm `\sqrt{n}σ` where `n`
- is the dimension of the lattice.
+ If `\sigma` is a real number, samples from this sampler will have expected norm
+ `\sqrt{n}\sigma` where `n` is the dimension of the lattice.
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
- sage: D.sigma
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
+ sage: D.sigma()
3.00000000000000
"""
return self._sigma
- @property
def c(self):
- r"""Center `c`.
+ r"""
+ Center `c`.
Samples from this sampler will be centered at `c`.
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0)); D
- Discrete Gaussian sampler with σ = 3.000000, c=(1, 0, 0) over lattice with basis
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0)); D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(1, 0, 0) over lattice with basis
[1 0 0]
[0 1 0]
[0 0 1]
- sage: D.c
+ sage: D.c()
(1, 0, 0)
"""
return self._c
+ def set_c(self, c):
+ r"""
+ Modifies center `c`.
+
+ EXAMPLES::
+
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
+ sage: D.set_c((2, 0, 0))
+ sage: D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(2, 0, 0) over lattice with basis
+
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
+ """
+ if c is None:
+ self._c = None
+ return
+
+ if c == 0:
+ c = vector(ZZ, self.n)
+ else:
+ try:
+ c = vector(ZZ, self.n, c)
+ except TypeError:
+ try:
+ c = vector(QQ, self.n, c)
+ except TypeError:
+ try:
+ c = vector(self._RR, self.n, c)
+ except TypeError:
+ c = vector(self._RR, self.n)
+
+ self._c = c
+ self._precompute_data()
+
def __repr__(self):
r"""
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0)); D
- Discrete Gaussian sampler with σ = 3.000000, c=(1, 0, 0) over lattice with basis
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0)); D
+ Discrete Gaussian sampler with Gaussian parameter σ = 3.00000000000000, c=(1, 0, 0) over lattice with basis
[1 0 0]
[0 1 0]
[0 0 1]
+ sage: Sigma = Matrix(ZZ, [[10, -6, 1], [-6, 5, -1], [1, -1, 2]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, Sigma); D
+ Discrete Gaussian sampler with Gaussian parameter Σ =
+ [ 10.0000000000000 -6.00000000000000 1.00000000000000]
+ [-6.00000000000000 5.00000000000000 -1.00000000000000]
+ [ 1.00000000000000 -1.00000000000000 2.00000000000000], c=(0, 0, 0) over lattice with basis
+
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
"""
- # beware of unicode character in ascii string !
- return "Discrete Gaussian sampler with σ = %f, c=%s over lattice with basis\n\n%s" % (self._sigma, self._c, self.B)
+ if self.is_spherical:
+ sigma_str = f"σ = {self._sigma}"
+ else:
+ sigma_str = f"Σ =\n{self._sigma}"
+ return f"Discrete Gaussian sampler with Gaussian parameter {sigma_str}, c={self._c} over lattice with basis\n\n{self.B}"
def _call_in_lattice(self):
r"""
- Return a new sample assuming `c ∈ Λ(B)`.
+ Return a new sample assuming `c \in \Lambda(B)`.
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1,0,0))
sage: L = [D._call_in_lattice() for _ in range(2^12)]
sage: mean_L = sum(L) / len(L)
- sage: norm(mean_L.n() - D.c) < 0.25
+ sage: norm(mean_L.n() - D.c()) < 0.25
True
- .. note::
+ .. NOTE::
- Do not call this method directly, call :func:`DiscreteGaussianDistributionLatticeSampler.__call__` instead.
+ Do not call this method directly, call
+ :func:`DiscreteGaussianDistributionLatticeSampler.__call__` instead.
"""
w = self.VS([d() for d in self.D], check=False)
return w * self.B + self._c
@@ -458,16 +811,16 @@ def _call(self):
EXAMPLES::
- sage: from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler
- sage: D = DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1/2,0,0))
- sage: L = [D._call() for _ in range(2^12)] # long time
- sage: mean_L = sum(L) / len(L) # long time
- sage: norm(mean_L.n() - D.c) < 0.25 # long time
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, 3.0, c=(1/2,0,0))
+ sage: L = [D._call() for _ in range(2^12)]
+ sage: mean_L = sum(L) / len(L)
+ sage: norm(mean_L.n() - D.c()) < 0.25
True
- .. note::
+ .. NOTE::
- Do not call this method directly, call :func:`DiscreteGaussianDistributionLatticeSampler.__call__` instead.
+ Do not call this method directly, call
+ :func:`DiscreteGaussianDistributionLatticeSampler.__call__` instead.
"""
v = 0
c, sigma, B = self._c, self._sigma, self.B
@@ -483,3 +836,46 @@ def _call(self):
c = c - z * B[i]
v = v + z * B[i]
return v
+
+ def add_offline_samples(self, cnt=1):
+ """
+ Precompute samples from `B^{-1}D_1` to be used in :meth:`_call_non_spherical`.
+
+ EXAMPLES::
+
+ sage: Sigma = Matrix([[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, Sigma)
+ sage: assert not D.is_spherical
+ sage: D.add_offline_samples(2^12)
+ sage: L = [D() for _ in range(2^12)] # Takes less time
+ """
+ # Just to document the difference with [Pei2010]_, in the paper (Algo 1)
+ # he samples from Λ + c, but we instead sample from Λ with distribution
+ # sampled at c (D_{Λ, c}), but that's the same as c + D_{Λ - c}
+ # Also, we use row notation instead of column notation. Sorry.
+ for _ in range(cnt):
+ coord = [normalvariate(mu=0, sigma=1) for _ in range(self.n)]
+ self.offline_samples.append(vector(self._RR, coord) * self.B2_B_inv)
+
+ def _call_non_spherical(self):
+ """
+ Return a new sample.
+
+ EXAMPLES::
+
+ sage: Sigma = Matrix([[5, -2, 4], [-2, 10, -5], [4, -5, 5]])
+ sage: D = distributions.DiscreteGaussianDistributionLatticeSampler(ZZ^3, Sigma, c=(1/2,0,0))
+ sage: L = [D._call_non_spherical() for _ in range(2^12)]
+ sage: mean_L = sum(L) / len(L)
+ sage: norm(mean_L.n() - D.c()) < 0.25
+ True
+
+ .. NOTE::
+
+ Do not call this method directly, call
+ :func:`DiscreteGaussianDistributionLatticeSampler.__call__` instead.
+ """
+ if len(self.offline_samples) == 0:
+ self.add_offline_samples()
+ vec = self._c_mul_B_inv - self.offline_samples.pop()
+ return self._randomise(vec) * self.B
diff --git a/src/sage/stats/distributions/discrete_gaussian_polynomial.py b/src/sage/stats/distributions/discrete_gaussian_polynomial.py
index 7d61ab7eb0c..63c8f5b800a 100644
--- a/src/sage/stats/distributions/discrete_gaussian_polynomial.py
+++ b/src/sage/stats/distributions/discrete_gaussian_polynomial.py
@@ -23,7 +23,7 @@
(24.0, 24.0)
"""
-#******************************************************************************
+# ******************************************************************************
#
# DGS - Discrete Gaussian Samplers
#
@@ -53,7 +53,7 @@
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the FreeBSD Project.
-#*****************************************************************************/
+# *****************************************************************************/
from sage.rings.real_mpfr import RR
from sage.rings.integer_ring import ZZ
diff --git a/src/sage/version.py b/src/sage/version.py
index feec9728c2f..9c51c77cb14 100644
--- a/src/sage/version.py
+++ b/src/sage/version.py
@@ -1,5 +1,5 @@
# Sage version information for Python scripts
# This file is auto-generated by the sage-update-version script, do not edit!
-version = '10.3.rc4'
-date = '2024-03-17'
-banner = 'SageMath version 10.3.rc4, Release Date: 2024-03-17'
+version = '10.4.beta0'
+date = '2024-03-25'
+banner = 'SageMath version 10.4.beta0, Release Date: 2024-03-25'
diff --git a/src/sage_docbuild/__main__.py b/src/sage_docbuild/__main__.py
index acfcb8392a0..80b4f9270f1 100644
--- a/src/sage_docbuild/__main__.py
+++ b/src/sage_docbuild/__main__.py
@@ -289,6 +289,9 @@ def setup_parser():
standard.add_argument("--no-plot", dest="no_plot",
action="store_true",
help="do not include graphics auto-generated using the '.. plot' markup")
+ standard.add_argument("--no-preparsed-examples", dest="no_preparsed_examples",
+ action="store_true",
+ help="do not show preparsed versions of EXAMPLES blocks")
standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True,
action="store_false",
help="include TESTS blocks in the reference manual")
@@ -478,6 +481,8 @@ def excepthook(*exc_info):
build_options.ALLSPHINXOPTS += "-n "
if args.no_plot:
os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes'
+ if args.no_preparsed_examples:
+ os.environ['SAGE_PREPARSED_DOC'] = 'no'
if args.live_doc:
os.environ['SAGE_LIVE_DOC'] = 'yes'
if args.skip_tests:
diff --git a/src/sage_docbuild/conf.py b/src/sage_docbuild/conf.py
index c5329e79cfd..1c705c68d1f 100644
--- a/src/sage_docbuild/conf.py
+++ b/src/sage_docbuild/conf.py
@@ -39,6 +39,9 @@
# General configuration
# ---------------------
+SAGE_LIVE_DOC = os.environ.get('SAGE_LIVE_DOC', 'no')
+SAGE_PREPARSED_DOC = os.environ.get('SAGE_PREPARSED_DOC', 'yes')
+
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
@@ -57,7 +60,7 @@
jupyter_execute_default_kernel = 'sagemath'
-if os.environ.get('SAGE_LIVE_DOC', 'no') == 'yes':
+if SAGE_LIVE_DOC == 'yes':
SAGE_JUPYTER_SERVER = os.environ.get('SAGE_JUPYTER_SERVER', 'binder')
if SAGE_JUPYTER_SERVER.startswith('binder'):
# format: "binder" or
@@ -230,7 +233,7 @@ def sphinx_plot(graphics, **kwds):
# console lexers. 'ipycon' is the IPython console, which is what we want
# for most code blocks: anything with "sage:" prompts. For other IPython,
# like blocks which might appear in a notebook cell, use 'ipython'.
-highlighting.lexers['ipycon'] = IPythonConsoleLexer(in1_regex=r'sage: ', in2_regex=r'[.][.][.][.]: ')
+highlighting.lexers['ipycon'] = IPythonConsoleLexer(in1_regex=r'(sage:|>>>)', in2_regex=r'([.][.][.][.]:|[.][.][.])')
highlighting.lexers['ipython'] = IPyLexer()
highlight_language = 'ipycon'
@@ -305,7 +308,7 @@ def set_intersphinx_mappings(app, config):
multidocs_is_master = True
# https://sphinx-copybutton.readthedocs.io/en/latest/use.html
-copybutton_prompt_text = r"sage: |[.][.][.][.]: |\$ "
+copybutton_prompt_text = r"sage: |[.][.][.][.]: |>>> |[.][.][.] |\$ "
copybutton_prompt_is_regexp = True
copybutton_exclude = '.linenos, .c1' # exclude single comments (in particular, # optional!)
copybutton_only_copy_prompt_lines = True
@@ -789,8 +792,6 @@ class will be properly documented inside its surrounding class.
return skip
-from jupyter_sphinx.ast import JupyterCellNode, CellInputNode
-
class SagecodeTransform(SphinxTransform):
"""
Transform a code block to a live code block enabled by jupyter-sphinx.
@@ -828,29 +829,87 @@ def apply(self):
if self.app.builder.tags.has('html') or self.app.builder.tags.has('inventory'):
for node in self.document.traverse(nodes.literal_block):
if node.get('language') is None and node.astext().startswith('sage:'):
- source = node.rawsource
- lines = []
- for line in source.splitlines():
- newline = line.lstrip()
- if newline.startswith('sage: ') or newline.startswith('....: '):
- lines.append(newline[6:])
- cell_node = JupyterCellNode(
- execute=False,
- hide_code=True,
- hide_output=True,
- emphasize_lines=[],
- raises=False,
- stderr=True,
- code_below=False,
- classes=["jupyter_cell"])
- cell_input = CellInputNode(classes=['cell_input','live-doc'])
- cell_input += nodes.literal_block(
- text='\n'.join(lines),
- linenos=False,
- linenostart=1)
- cell_node += cell_input
-
- node.parent.insert(node.parent.index(node) + 1, cell_node)
+ from docutils.nodes import container as Container, label as Label, literal_block as LiteralBlock, Text
+ from sphinx_inline_tabs._impl import TabContainer
+ parent = node.parent
+ index = parent.index(node)
+ if isinstance(node.previous_sibling(), TabContainer):
+ # Make sure not to merge inline tabs for adjacent literal blocks
+ parent.insert(index, Text(''))
+ index += 1
+ parent.remove(node)
+ # Tab for Sage code
+ container = TabContainer("", type="tab", new_set=False)
+ textnodes = [Text('Sage')]
+ label = Label("", "", *textnodes)
+ container += label
+ content = Container("", is_div=True, classes=["tab-content"])
+ content += node
+ container += content
+ parent.insert(index, container)
+ if SAGE_PREPARSED_DOC == 'yes':
+ # Tab for preparsed version
+ from sage.repl.preparse import preparse
+ container = TabContainer("", type="tab", new_set=False)
+ textnodes = [Text('Python')]
+ label = Label("", "", *textnodes)
+ container += label
+ content = Container("", is_div=True, classes=["tab-content"])
+ example_lines = []
+ preparsed_lines = ['>>> from sage.all import *']
+ for line in node.rawsource.splitlines() + ['']: # one extra to process last example
+ newline = line.lstrip()
+ if newline.startswith('....: '):
+ example_lines.append(newline[6:])
+ else:
+ if example_lines:
+ preparsed_example = preparse('\n'.join(example_lines))
+ prompt = '>>> '
+ for preparsed_line in preparsed_example.splitlines():
+ preparsed_lines.append(prompt + preparsed_line)
+ prompt = '... '
+ example_lines = []
+ if newline.startswith('sage: '):
+ example_lines.append(newline[6:])
+ else:
+ preparsed_lines.append(line)
+ preparsed = '\n'.join(preparsed_lines)
+ preparsed_node = LiteralBlock(preparsed, preparsed, language='ipycon')
+ content += preparsed_node
+ container += content
+ parent.insert(index + 1, container)
+ if SAGE_LIVE_DOC == 'yes':
+ # Tab for Jupyter-sphinx cell
+ from jupyter_sphinx.ast import JupyterCellNode, CellInputNode
+ source = node.rawsource
+ lines = []
+ for line in source.splitlines():
+ newline = line.lstrip()
+ if newline.startswith('sage: ') or newline.startswith('....: '):
+ lines.append(newline[6:])
+ cell_node = JupyterCellNode(
+ execute=False,
+ hide_code=False,
+ hide_output=True,
+ emphasize_lines=[],
+ raises=False,
+ stderr=True,
+ code_below=False,
+ classes=["jupyter_cell"])
+ cell_input = CellInputNode(classes=['cell_input','live-doc'])
+ cell_input += nodes.literal_block(
+ text='\n'.join(lines),
+ linenos=False,
+ linenostart=1)
+ cell_node += cell_input
+ container = TabContainer("", type="tab", new_set=False)
+ textnodes = [Text('Sage Live')]
+ label = Label("", "", *textnodes)
+ container += label
+ content = Container("", is_div=True, classes=["tab-content"])
+ content += cell_node
+ container += content
+ parent.insert(index + 1, container)
# This replaces the setup() in sage.misc.sagedoc_conf
@@ -864,7 +923,7 @@ def setup(app):
app.connect('autodoc-process-docstring', skip_TESTS_block)
app.connect('autodoc-skip-member', skip_member)
app.add_transform(SagemathTransform)
- if os.environ.get('SAGE_LIVE_DOC', 'no') == 'yes':
+ if SAGE_LIVE_DOC == 'yes' or SAGE_PREPARSED_DOC == 'yes':
app.add_transform(SagecodeTransform)
# When building the standard docs, app.srcdir is set to SAGE_DOC_SRC +
diff --git a/tox.ini b/tox.ini
index a9ff0423488..99d04aad388 100644
--- a/tox.ini
+++ b/tox.ini
@@ -143,7 +143,7 @@ passenv =
docker: EXTRA_DOCKER_BUILD_ARGS
docker: EXTRA_DOCKER_TAGS
docker: DOCKER_TAG
- # Use DOCKER_BUILDKIT=1 for new version - for which unfortunately we cannot save failed builds as an image
+ # Use DOCKER_BUILDKIT=0 for legacy builder
docker: DOCKER_BUILDKIT
docker: BUILDKIT_INLINE_CACHE
# Set for example to "with-system-packages configured with-targets-pre with-targets"
@@ -760,7 +760,7 @@ commands =
docker: BUILD_IMAGE=$DOCKER_PUSH_REPOSITORY$BUILD_IMAGE_STEM-$docker_target; \
docker: BUILD_TAG={env:DOCKER_TAG:$(git describe --dirty --always)}; \
docker: TAG_ARGS=$(for tag in $BUILD_TAG {env:EXTRA_DOCKER_TAGS:}; do echo --tag $BUILD_IMAGE:$tag; done); \
- docker: DOCKER_BUILDKIT={env:DOCKER_BUILDKIT:0} \
+ docker: DOCKER_BUILDKIT={env:DOCKER_BUILDKIT:1}; \
docker: docker build . -f {envdir}/Dockerfile \
docker: --target $docker_target \
docker: $TAG_ARGS \
@@ -772,16 +772,38 @@ commands =
docker: --build-arg TARGETS="{posargs:build}" \
docker: --build-arg TARGETS_OPTIONAL="{env:TARGETS_OPTIONAL:ptest}" \
docker: {env:EXTRA_DOCKER_BUILD_ARGS:}; status=$?; \
+ docker: unset CONTAINER; \
docker: if [ $status != 0 ]; then \
- docker: BUILD_TAG="$BUILD_TAG-failed"; docker commit $(docker ps -l -q) $BUILD_IMAGE:$BUILD_TAG; PUSH_TAGS=$BUILD_IMAGE:$BUILD_TAG; \
- docker: else \
- docker: PUSH_TAGS=$(echo $BUILD_IMAGE:$BUILD_TAG; for tag in {env:EXTRA_DOCKER_TAGS:}; do echo "$BUILD_IMAGE:$tag"; done); \
+ docker: if [ $DOCKER_BUILDKIT = 0 ]; then \
+ docker: BUILD_TAG="$BUILD_TAG-failed"; CONTAINER=$(docker ps -l -q); docker commit $CONTAINER $BUILD_IMAGE:$BUILD_TAG; \
+ docker: else \
+ docker: unset BUILD_TAG; echo "DOCKER_BUILDKIT=1, so we cannot commit and tag the failed image"; \
+ docker: fi; \
docker: fi; \
- docker: echo $BUILD_IMAGE:$BUILD_TAG >> {envdir}/Dockertags; \
- docker: if [ x"{env:DOCKER_PUSH_REPOSITORY:}" != x ]; then \
+ docker: if [ -n "$BUILD_TAG" ]; then \
+ docker: echo "Copying logs from the container to {envdir}/sage/"; \
+ docker: rm -f {envdir}/sage/STATUS; \
+ docker: docker run $BUILD_IMAGE:$BUILD_TAG bash -c " \
+ docker: tar -c --ignore-failed-read -f - \
+ docker: /sage/STATUS /sage/logs /sage/{prefix,venv}/var/lib/sage/installed \
+ docker: ~/.sage/timings2.json /sage/pkgs/*/.tox/*/.sage/timings2.json \
+ docker: /sage/pkgs/*/.tox/*/logs 2> /dev/null" \
+ docker: | (cd {envdir} && tar xf -); \
+ docker: if [ -f {envdir}/sage/STATUS ]; then status=$(cat {envdir}/sage/STATUS); fi; \
+ docker: fi; \
+ docker: unset PUSH_TAGS; \
+ docker: if [ -n "$BUILD_TAG" ]; then \
+ docker: if [ $status != 0 ]; then \
+ docker: BUILD_TAG="${BUILD_TAG%-failed}-failed"; PUSH_TAGS=$BUILD_IMAGE:$BUILD_TAG; \
+ docker: else \
+ docker: PUSH_TAGS=$(echo $BUILD_IMAGE:$BUILD_TAG; for tag in {env:EXTRA_DOCKER_TAGS:}; do echo "$BUILD_IMAGE:$tag"; done); \
+ docker: fi; \
+ docker: echo $BUILD_IMAGE:$BUILD_TAG >> {envdir}/Dockertags; \
+ docker: fi; \
+ docker: if [ x"{env:DOCKER_PUSH_REPOSITORY:}" != x -a x"$PUSH_TAGS" != x ]; then \
docker: echo Pushing $PUSH_TAGS; \
docker: for tag in $PUSH_TAGS; do \
- docker: docker push $tag || echo "(ignoring errors)"; \
+ docker: if docker push $tag; then echo $tag >> {envdir}/Dockertags.pushed; else echo "(ignoring errors)"; fi; \
docker: done; \
docker: fi; \
docker: if [ $status != 0 ]; then exit $status; fi; \