Source code for zetalib.algebra

"""
Basic functionality for 'additively free ZZ-algebras', i.e. non-associative
ring structures on ZZ^n.
"""

from __future__ import absolute_import
from sage.all import *
from .convex import PositiveOrthant
from .toric import ToricDatum
from .util import normalise_poly, monomial_log, cached_simple_method, \
    upper_triangular_matrix, is_block_diagonal_matrix, basis_of_matrix_algebra

from . import common
import multiprocessing

import itertools
import six
from six.moves import range

#TODO: Use SageObject instead of object.
[docs]class Algebra(object): """Additively free non-associative ZZ-algebras with optional operators. """ def __init__(self, table=None, rank=None, blocks=None, operators=None, matrix=None, bilinear=False, simple_basis=False, descr=None, matrix_basis=None, product=None): """Construct an additively free ZZ-algebra (possibly with operators). """ self.descr = descr self.operators = [] if (operators is None) else [Matrix(QQ,x) for x in operators] if not(matrix is None): A = Matrix(QQ,matrix) if not(bilinear or simple_basis): raise TypeError('You need to set either bilinear or simple_basis') if simple_basis: self.rank = A.ncols() e = MatrixSpace(QQ, self.rank).identity_matrix().rows() def f(i,j): if abs(A[i,j]) > self.rank: raise TypeError('Matrix entries out of range') if A[i,j] == 0: return (Integer(0) * e[0]) elif A[i,j] > 0: return (e[A[i,j]-1]) elif A[i,j] < 0: return (-e[-A[i,j]-1]) self.table = [ [ f(i,j) for j in range(self.rank) ] for i in range(self.rank) ] elif bilinear: n = A.ncols() self.rank = n + Integer(1) w = vector(QQ, n * [Integer(0)] + [Integer(1)]) self.table = [ [ A[i,j] * w for j in range(n) ] + [ 0 * w ] for i in range(n) ] + [ [ 0 * w for j in range(n+1) ] ] self.blocks = (self.rank,) if blocks is None else tuple(blocks) return # If the rank is set, always initialise an empty table. if not(rank is None): self.rank = rank self.table = [ [ vector(QQ, self.rank) for j in range(self.rank) ] for i in range(self.rank) ] self.blocks = (self.rank,) if blocks is None else tuple(blocks) if isinstance(table, dict): if rank is None: raise TypeError('You need to specify the rank') for pos, v in six.iteritems(table): self.table[pos[0]][pos[1]] = vector(QQ, v) return if isinstance(table, list): self.rank = len(table) self.blocks = (self.rank,) if blocks is None else tuple(blocks) self.table = [ [ vector(QQ, v) for v in row ] for row in table ] return # 'basis' is a list of QQ-linearly independent matrixy things # spanning a Lie or associative algebra. if not(matrix_basis is None): if product is None: raise TypeError('need to specify Lie or associative product') if product == 'Lie': mult = lambda x, y: x*y - y*x elif product == 'associative': mult = lambda x, y: x*y else: raise TypeError('unknown type of product') self.rank = len(matrix_basis) self.blocks = (self.rank,) if blocks is None else tuple(blocks) matrix_basis = [Matrix(QQ, b) for b in matrix_basis] A = Matrix(QQ, [b.list() for b in matrix_basis]) if A.rank() != len(matrix_basis): raise TypeError('matrices specified are linearly dependent') self.table = [ [ A.solve_left(vector(mult(b,c).list())) for c in matrix_basis] for b in matrix_basis ] return if rank is None: raise ValueError('need to specify the rank')
[docs] def multiply(self, v, w, ring=QQ): return vector(ring, [ sum(v[i] * w[j] * ring(self.table[i][j][k]) for i in range(self.rank) for j in range(self.rank)) for k in range(self.rank) ] )
[docs] def right_multiplication(self, w, ring=QQ): return matrix(ring, [self.multiply(v,w) for v in identity_matrix(ring, self.rank)])
[docs] def ideal(self, vecs, ring=QQ): V = ring**self.rank B = list(identity_matrix(QQ,self.rank)) U = V.subspace(vecs) while True: li = list(U.basis()) xi = [] for u,e in itertools.product(li,B): xi.append(self.multiply(u,e,ring=ring)) xi.append(self.multiply(e,u,ring=ring)) W = V.subspace(li+xi) if U == W: return U else: U = W
@cached_simple_method def derived_series(self, ring=QQ): V = ring**self.rank dseries = [V] while True: W = self.ideal([self.multiply(v,w,ring=ring) for v in V.basis() for w in V.basis()]) if V == W: return dseries else: V = W dseries.append(V) @cached_simple_method def derived_length(self): dseries = self.derived_series() return Infinity if dseries[-1].dimension() > 0 else len(dseries)-1 @cached_simple_method def is_soluble(self): return self.derived_length() < Infinity @cached_simple_method def lower_central_series(self, ring=QQ): V = ring**self.rank B = list(V.basis()) lcseries = [V] while True: li = [] for v,e in itertools.product(V.basis(), B): li.append(self.multiply(v,e)) li.append(self.multiply(e,v)) W = self.ideal(li) if V == W: return lcseries else: V = W lcseries.append(V) @cached_simple_method def nilpotency_class(self): lcseries = self.lower_central_series() return Infinity if lcseries[-1].dimension() > 0 else len(lcseries)-1 @cached_simple_method def is_nilpotent(self): return self.nilpotency_class() < Infinity @cached_simple_method def is_commutative(self): for u,v in itertools.product((QQ**self.rank).basis_matrix(),repeat=2): if self.multiply(u,v) != self.multiply(v,u): return False return True @cached_simple_method def is_anticommutative(self): for u,v in itertools.product((QQ**self.rank).basis_matrix(),repeat=2): if self.multiply(u,v) != -self.multiply(v,u): return False return True @cached_simple_method def is_associative(self): for u,v,w in itertools.product((QQ**self.rank).basis_matrix(),repeat=3): if self.multiply(self.multiply(u,v),w) != self.multiply(u,self.multiply(v,w)): return False return True @cached_simple_method def is_Lie(self): if not self.is_anticommutative(): return False for u,v,w in itertools.product((QQ**self.rank).basis_matrix(),repeat=3): if self.multiply(self.multiply(u,v),w) != self.multiply(self.multiply(u,w),v) + self.multiply(u,self.multiply(v,w)): return False return True # Use rows of a matrix as the new basis.
[docs] def change_basis(self, A, check=True): if check and not is_block_diagonal_matrix(A, self.blocks): raise ValueError('the given matrix does not preserve the block structure') A = matrix(QQ, A) if not A.is_invertible(): raise TypeError('The argument must be an invertible matrix over QQ.') return Algebra( table = [[ self.multiply( A[i], A[j] ) * A.inverse() for j in range(self.rank) ] for i in range(self.rank)], operators = [ A * op * A**(-1) for op in self.operators ], blocks = self.blocks )
@cached_simple_method def _Lie_centre(self): # Construct the QQ-space of all z with a*z == z*a == 0 for all a. Z = QQ**self.rank for i in range(self.rank): Z = Z.intersection( matrix(QQ, [self.table[i][j] for j in range(self.rank)]).left_kernel() ) if self.is_anticommutative() or self.is_commutative(): continue Z = Z.intersection( matrix(QQ, [self.table[j][i] for j in range(self.rank)]).left_kernel() ) return Z @cached_simple_method def _adjusted_basis(self): # Construct a basis of 'self' as in (Stasinski & Voll 2014, Section 2.2.2); # we ignore finitely many primes. if len(self.blocks) > 1: raise NotImplementedError if not (self.is_Lie() and self.is_nilpotent() and self.rank > 0): raise NotImplementedError() Z = self._Lie_centre() D = self.derived_series()[1] M = Z.intersection(D) A = Z.intersection(M.complement()) B = D.intersection(M.complement()) C = (B+M+A).complement() li = list(itertools.chain(C.basis(), B.basis(), M.basis(), A.basis())) return matrix(QQ, [v.denominator()*v for v in li]), (dim(C), dim(B), dim(M), dim(A)) @cached_simple_method def _adjoint_representation(self): # Construct a basis of the image of `self' under its adjoint # representation. assert self.is_anticommutative() return basis_of_matrix_algebra([self.right_multiplication(e) for e in identity_matrix(QQ, self.rank)], product='zero') @cached_simple_method def _commutator_matrices(self): if len(self.blocks) > 1: raise NotImplementedError A, dims = self._adjusted_basis() d = dims[1] + dims[2] # = dim of commutator ideal r = dims[0] + dims[1] # = codim of centre ring = PolynomialRing(QQ, 'y', d) y = vector(ring, ring.gens()) T = self.change_basis(A) a = dims[0] ; b = a + d # [a,b) == indices of basis vectors of the commutator ideal R = matrix(ring, [ [ y * vector(ring, [T.table[i][j][k] for k in range(a,b)]) for j in range(r) ] for i in range(r) ] ) # get last dims[1] columns S = (R.transpose()[r-dims[1]:r]).transpose() return R, S
[docs] def toric_datum(self, objects='subalgebras', name='x'): """Construct a toric datum associated with the enumeration of subobjects. We can enumerate subalgebras as well as left/right/2-sided ideals. """ if not isinstance(objects, six.string_types): raise TypeError('String expected') if not objects in ['subalgebras', 'ideals', 'left ideals', 'right ideals']: raise TypeError('Unknown objects.') if sum(self.blocks) != self.rank: raise ValueError('blocks and rank do not match') nvars = sum(binomial(a+1,2) for a in self.blocks) R = PolynomialRing(QQ, nvars, name) entries = iter(R.gens()) M = block_diagonal_matrix([upper_triangular_matrix(R, a, entries) for a in self.blocks]) D = prod(M.diagonal()) diag = iter(M.diagonal()) zoo = prod(next(diag)**(i+1) for a in self.blocks for i in range(a)) integrand = (monomial_log(D), monomial_log(zoo) - vector(QQ,R.ngens() * [1])) Madj = M.adjugate() E = identity_matrix(R, self.rank) li = [] def insert_components(w): for rhs in w: if rhs.is_zero(): continue cand = normalise_poly(rhs) if not cand in li: li.append(cand) for i in range(self.rank): for j in range(self.rank): if objects == 'subalgebras': insert_components(self.multiply(M[i], M[j], R) * Madj) if (objects == 'right ideals') or (objects == 'ideals'): insert_components(self.multiply(M[i], E[j], R) * Madj) if (objects == 'left ideals') or (objects == 'ideals'): insert_components(self.multiply(E[j], M[i], R) * Madj) for op in self.operators: for row in M * matrix(R, op) * Madj: insert_components(row) if len(li) > 0: li = ideal(li).interreduced_basis() cc_raw = [(D,f) for f in li] # Perform some trivial simplifications cc = [] for c in cc_raw: lhs, rhs = c if rhs.is_zero(): continue g = gcd(lhs, rhs) rhs = rhs//g lhs = lhs//g if lhs.is_constant(): continue mon, coeff = rhs.monomials(), rhs.coefficients() # Discard monomials in 'rhs' that are divisible by 'lhs' idx = [k for k in range(len(mon)) if not lhs.divides(mon[k])] rhs = sum(coeff[k] * mon[k] for k in idx) if rhs.is_zero(): continue cand = (normalise_poly(lhs), normalise_poly(rhs)) if not cand in cc: cc.append(cand) cc.sort() T = ToricDatum(ring=R, integrand=integrand, cc=cc, initials=None, polyhedron=PositiveOrthant(R.ngens()) ) return T.simplify() if T.weight() < common._simplify_bound else T
[docs] def find_good_basis(self, objects='subalgebras', name='x'): """Find a basis yielding a 'good' toric datum. Presently we only look at permutations of the given basis (preserving the block structure, if any). """ def weight(g): A = g.matrix() L = self.change_basis(A) return L.toric_datum(objects=objects, name=name).weight() Sn = SymmetricGroup(self.rank) partial_sums = [sum(self.blocks[:i]) for i in range(len(self.blocks)+1)] gens = [] for r in range(len(partial_sums)-1): a = partial_sums[r] b = partial_sums[r+1] G = SymmetricGroup(list(range(a+1,b+1))) # NOTE: SymmetricGroup(n) acts on [1,...,n] instead of range(n). f = G.hom(Sn) gens.extend(f(g) for g in G.gens()) G = Sn.subgroup(gens) best_weight, best_permutation = Infinity, None for (arg,w) in (parallel(ncpus=common.ncpus)(weight))(list(G)): g = arg[0][0] if w == 'NO DATA': w = weight(g) if w < best_weight: best_weight, best_permutation = w, g return matrix(QQ, best_permutation.matrix())
def __str__(self): if self.descr: S = self.descr + '\n' else: S = "Additively free ZZ-algebra of rank " + str(self.rank) + " with multiplication table\n" if len(self.blocks) > 1: S = S + 'Blocks: %s\n' % list(self.blocks) for row in self.table: S = S + '\t' for vec in row: S = S + str(vec) + ', ' S = S[:-2] + '\n' if len(self.operators) > 0: S += 'Operators: ' + str(len(self.operators)) + '\n' return S[:-1] def __repr__(self): return '%s(**%s)' % (self.__class__, self.__dict__)
[docs]def tensor_with_duals(L): if len(L.blocks) > 1: raise NotImplementedError n = L.rank shift = n * [n] zero = n * [0] def f(i,j): if i < n and j < n: return list(L.table[i][j]) + zero elif i < n and j >= n: return zero + list(L.table[i][j-n]) elif i >= n and j < n: return zero + list(L.table[i-n][j]) else: return zero + zero return Algebra(table=[[f(i,j) for j in range(2*n)] for i in range(2*n)])
[docs]def tensor_with_3duals(L): if len(L.blocks) > 1: raise NotImplementedError n = L.rank shift = n * [n] zero = n * [0] def f(i,j): if i < n and j < n: return list(L.table[i][j]) + zero + zero elif i < n and n <= j < 2*n: return zero + list(L.table[i][j-n]) + zero elif i < n and 2*n <= j: return zero + zero + list(L.table[i][j-2*n]) elif n <= i < 2*n and j < n: return zero + list(L.table[i-n][j]) + zero elif n <= i < 2*n and n <= j < 2*n: return zero + zero + list(L.table[i-n][j-n]) elif 2*n <= i and j < n: return zero + zero + list(L.table[i-2*n][j]) else: return zero + zero + zero return Algebra(table=[[f(i,j) for j in range(3*n)] for i in range(3*n)])