Source code for surface_dynamics.misc.constellation

r"""
Constellations

A constellation is a tuple `(g_0, g_1, \dots, g_k)` of permutations
such that the product `g_0 g_1 ... g_k` is the identity. One often
assumes that the group generated by `g_0, g_1, \dots, g_k` acts
transitively ([LanZvo04]_ definition 1). Geometrically, it corresponds
to a covering of the 2-sphere ramified over `k` points (the transitivity
condition corresponds to the connectivity of the covering).

.. NOTE::

    The permutations are on `[0, n-1]` and not on `[1, n]` for
    algorithmic and practical reasons.

EXAMPLES::

    sage: from surface_dynamics.misc.constellation import Constellation, Constellations
    sage: c = Constellation(['(0,1)', '(0,2)', None])
    sage: c
    Constellation of length 3 and degree 3
    g0 (0,1)(2)
    g1 (0,2)(1)
    g2 (0,2,1)
    sage: C = Constellations(3,4); C
    Connected constellations of length 3 and degree 4
    sage: C.cardinality()
    426
"""
# ****************************************************************************
#       Copyright (C) 2010 Vincent Delecroix <20100.delecroix@gmail.com>
#
#  Distributed under the terms of the GNU General Public License (GPL)
#  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 __future__ import print_function, absolute_import, division
from six.moves import range, zip, filter

from sage.structure.parent import Parent
from sage.structure.element import Element
from sage.structure.unique_representation import UniqueRepresentation

from sage.rings.integer import Integer
from sage.combinat.partition import Partition
from sage.structure.sage_object import SageObject
from sage.groups.perm_gps.permgroup_element import PermutationGroupElement
from sage.graphs.graph import Graph

# we import everything from sage.misc.permutation
from surface_dynamics.misc.permutation import *

ZZ_0 = Integer(0)


# constructors

[docs] def Constellations(*data, **options): r""" Build a set of constellations. INPUT: - ``profile`` -- an optional profile - ``length`` -- an optional length - ``degree`` -- an optional degree - ``connected`` -- an optional boolean EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation, Constellations The set of all constellations:: sage: Constellations() Connected constellations Constellations with constrained length and degree:: sage: Constellations(4,2) Connected constellations of length 4 and degree 2 """ profile = options.get('profile', None) length = options.get('length', None) degree = options.get('degree', None) connected = options.get('connected', True) if data: if len(data) == 1: if isinstance(data[0],(tuple,list)): profile = data[0] else: length = Integer(data[0]) elif len(data) == 2: length = Integer(data[0]) degree = Integer(data[1]) if profile: profile = tuple(map(Partition,profile)) return Constellations_p(profile, bool(connected)) if degree is None and length is None: return Constellations_all(bool(connected)) elif degree is not None and length is not None: return Constellations_ld(Integer(length), Integer(degree), bool(connected)) else: raise NotImplementedError("one cannot give just degree or just length")
[docs] def Constellation(g=None, mutable=False, connected=True, check=True): r""" Constellation INPUT: - ``g`` -- a list of permutations - ``mutable`` -- whether the result is mutable or not. Default is ``False``. - ``connected`` -- whether the result should be connected. Default is ``True``. - ``check`` -- whether or not to check. If it is ``True``, then the list g must contains no ``None``. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation Simple initialization:: sage: Constellation(['(0,1)','(0,3)(1,2)','(0,3,1,2)']) Constellation of length 3 and degree 4 g0 (0,1)(2)(3) g1 (0,3)(1,2) g2 (0,3,1,2) One of the permutation can be omitted:: sage: Constellation(['(0,1)', None, '(0,4)(1,2,3)']) Constellation of length 3 and degree 5 g0 (0,1)(2)(3)(4) g1 (0,3,2,1,4) g2 (0,4)(1,2,3) One can define mutable constellations:: sage: Constellation(([0,2,1], [2,1,0], [1,2,0]), mutable=True) Constellation of length 3 and degree 3 g0 (0)(1,2) g1 (0,2)(1) g2 (0,1,2) """ return Constellations(connected=connected)(g,check=check,mutable=mutable)
# classes
[docs] class Constellation_class(Element): r""" Constellation A constellation or a tuple of permutations `(g_0,g_1,...,g_k)` such that the product `g_0 g_1 ... g_k` is the identity. """ def __init__(self, parent, g, check=True, connected=True, mutable=False): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation([[1,2,0],[0,2,1],[1,0,2],None]) sage: c == loads(dumps(c)) True sage: g0 = '(0,1)(2,4)' sage: g1 = '(0,3)(1,4)' sage: g2 = '(2,4,3)' sage: g3 = '(0,3)(1,2)' sage: c0 = Constellation([g0,g1,g2,g3]) sage: c0 == Constellation([None,g1,g2,g3]) True sage: c0 == Constellation([g0,None,g2,g3]) True sage: c0 == Constellation([g0,g1,None,g3]) True sage: c0 == Constellation([g0,g1,g2,None]) True """ Element.__init__(self, parent) self._connected = connected self._mutable = mutable self._g = g if check: self._check() def __hash__(self): r""" Return a hash for ``self`` EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False) sage: c.__hash__() # random 2148362403144019871 """ if self._mutable: raise ValueError("can not hash mutable constellation") return hash(tuple(map(tuple, self._g)))
[docs] def set_immutable(self): r""" Does nothing, as ``self`` is already immutable EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False) sage: c.set_immutable() sage: c.is_mutable() False """ self._mutable = False
[docs] def is_mutable(self): r""" Return ``False`` as ``self`` is immutable EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False) sage: c.is_mutable() False """ return self._mutable
[docs] def switch(self, i, j0, j1): r""" Switch the vertices and edges at `i` and `i+1` The modification is local in the sense that it modifies `g_i` and `g_{i+1}` but does not modify the product `g_i g_{i+1}`. | j0 -- g_i --> k0 -- g_i0 --> l0 | j1 -- g_i+1 --> k1 -- g_i+1 --> l1 becomes | j0 -- g_i --> k1 -- g_i --> l0 | j1 -- g_i+1 --> k1 -- g_i+1 --> l1 EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None], mutable=True); c Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,4)(2)(3) g2 (0,1,3,2,4) sage: c.switch(1,2,3); c Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,4)(2,3) g2 (0,1,3,4)(2) """ if not self._mutable: raise ValueError("this constellation is immutable. Take a mutable copy first.") perm_switch(self._g[i], self._g[i+1], j0, j1)
def _set_g_unsafe(self, i, j, k): r""" set gi[j] to k Advice: try a _check() after your modification TESTS:: sage: from surface_dynamics import * sage: c = Constellation([[1,0],[0,1],[1,0]], mutable=True) sage: c Constellation of length 3 and degree 2 g0 (0,1) g1 (0)(1) g2 (0,1) sage: c._set_g_unsafe(0,0,0) sage: c._set_g_unsafe(0,1,1) sage: c._set_g_unsafe(1,0,1) sage: c._set_g_unsafe(1,1,0) sage: c Constellation of length 3 and degree 2 g0 (0)(1) g1 (0,1) g2 (0,1) sage: c._check() """ if not self._mutable: raise ValueError("not mutable") self._g[i][j] = int(k)
[docs] def euler_characteristic(self): r""" Return the Euler characteristic of the surface ALGORITHM: Hurwitz formula EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)', '(0,2)', None]) sage: c.euler_characteristic() 2 """ return Integer(self.degree()*2 - sum(sum(i-1 for i in self.profile(i)) for i in range(self.length())))
[docs] def genus(self): r""" Returns the genus of the surface EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)', '(0,2)', None]) sage: c.genus() 0 """ return 1 - self.euler_characteristic()/2
def _check(self): r""" Check that the constellation is valid and if not raise ValueError TESTS:: sage: from surface_dynamics import * sage: c = Constellation([[1,0],[1,0]], mutable=True) sage: c._set_g_unsafe(0,0,0) sage: c._check() Traceback (most recent call last): ... ValueError: permutation 0 invalid sage: c._set_g_unsafe(0,1,1) sage: c._check() Traceback (most recent call last): ... ValueError: The product is not identity sage: c._set_g_unsafe(1,0,0) sage: c._set_g_unsafe(1,1,1) sage: c._check() Traceback (most recent call last): ... ValueError: not connected """ for i in range(self.length()): if not perm_check(self._g[i]): raise ValueError("permutation {} invalid".format(i)) h = perm_one(self.degree()) for p in self._g: h = perm_compose(h, p) if not perm_is_one(h): raise ValueError("The product is not identity") if self._connected and not perms_are_transitive(self._g): raise ValueError("not connected") def __copy__(self): r""" TESTS:: sage: from surface_dynamics import * sage: c = Constellation([[0,2,1],[1,0,2],[2,1,0],None]) sage: c == copy(c) True sage: c is copy(c) False sage: c = Constellation([[0,2,1],[1,0,2],[2,1,0],None],mutable=True) sage: c == copy(c) True sage: c is copy(c) False """ return self.parent()([gg[:] for gg in self._g], check=False, mutable=self._mutable) copy = __copy__
[docs] def mutable_copy(self): r""" Return a mutable copy of ``self`` EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False) sage: d = c.mutable_copy() sage: d.is_mutable() True """ return self.parent()([gg[:] for gg in self._g], check=False, mutable=True)
## GENERAL PROPERTIES
[docs] def is_connected(self): r""" Test of connectedness EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2)', None, '(0,1)(2)'], connected=False) sage: c.is_connected() False sage: c = Constellation(['(0,1,2)', None], connected=False) sage: c.is_connected() True """ if self._connected: return True else: return perms_are_transitive(self._g)
[docs] def connected_components(self): """ Returns the connected components ? EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2)', None, '(0,1)(2)'], connected=False) sage: c.connected_components() [Constellation of length 3 and degree 2 g0 (0,1) g1 (0)(1) g2 (0,1), Constellation of length 3 and degree 1 g0 (0) g1 (0) g2 (0)] sage: c = Constellation(['(0,1,2)', None], connected=False) sage: c.connected_components() [Constellation of length 2 and degree 3 g0 (0,1,2) g1 (0,2,1)] """ if self._connected: return [self] G = Graph(loops=True, multiedges=True) for p in self._g: G.add_edges(enumerate(p)) m = G.connected_components(sort=True) if len(m) == 1: return [self] for mm in m: mm.sort() m.sort() g = [[] for _ in range(len(m))] m_inv = [None]*self.degree() for t in range(len(m)): for i in range(len(m[t])): m_inv[m[t][i]] = i for k in range(self.length()): tmp = [None]*len(m[t]) for i in range(len(m[t])): tmp[i] = m_inv[self._g[k][m[t][i]]] g[t].append(tmp) return [Constellation(g=g[i], check=False) for i in range(len(m))]
def __eq__(self, other): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellation sage: Constellation(['(0,1,2)', None]) == Constellation(['(0,1,2)', None]) True sage: Constellation(['(0,1)','(0,2)',None]) == Constellation(['(0,1)',None,'(0,2)']) False """ return type(self) == type(other) and self._g == other._g def __ne__(self, other): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellation sage: Constellation(['(0,1,2)', None]) != Constellation(['(0,1,2)', None]) False sage: Constellation(['(0,1)','(0,2)',None]) != Constellation(['(0,1)',None,'(0,2)']) True """ return type(self) != type(other) or self._g != other._g
[docs] def is_isomorphic(self, other, return_map=False): r""" Test of isomorphism Returns ``True`` if the constellations are isomorphic (i.e. common conjugacy) and returns the permutation that conjugate the two permutations if return_map is ``True`` in such a way that self.relabel(m) == other ALGORITHM: uses canonical labels obtained from the method :meth:`relabel`. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation([[1,0,2],[2,1,0],[0,2,1],None]) sage: d = Constellation([[2,1,0],[0,2,1],[1,0,2],None]) sage: answer, mapping = c.is_isomorphic(d,return_map=True) sage: answer True sage: c.relabel(mapping) == d True """ if return_map: if (self.degree() != other.degree() or self.length() != other.length()): return False, None sn, sn_map = self.relabel(return_map=True) on, on_map = other.relabel(return_map=True) if sn != on: return False, None return True, perm_compose(sn_map, perm_invert(on_map)) return (self.degree() == other.degree() and self.length() == other.length() and self.relabel() == other.relabel())
def __lt__(self, other): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellation sage: c1 = Constellation([[1,2,0],None]) sage: c2 = Constellation([[2,0,1],None]) sage: c1 == c2 False sage: c1 != c2 True sage: c1 < c2 or c1 <= c2 True sage: c1 > c2 or c1 >= c2 False sage: c2 < c1 or c2 <= c1 False sage: c2 > c1 and c2 >= c1 True """ if type(self) != type(other): raise TypeError if self.length() < other.length(): return True elif self.length() > other.length(): return False if self.degree() < other.degree(): return True elif self.degree() > other.degree(): return False for i in range(self.length()-1): for j in range(self.degree()-1): if self._g[i][j] < other._g[i][j]: return True elif self._g[i][j] > other._g[i][j]: return False # equality return False def __le__(self, other): return self == other or self < other def __ge__(self, other): return not self < other def __gt__(self, other): return not self <= other def _repr_(self): r""" String representation EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation([[1,0,2],[2,1,0],[0,2,1],None]) sage: c._repr_() 'Constellation of length 4 and degree 3\ng0 (0,1)(2)\ng1 (0,2)(1)\ng2 (0)(1,2)\ng3 (0,2)(1)' """ s = "Constellation of length %d and degree %d" % (self.length(), self.degree()) for i in range(self.length()): s += "\ng%d %s" % (i, perm_cycle_string(self._g[i], True)) return s
[docs] def degree(self): r""" Return the degree of the constellation The degree of a constellation is the number `n` that corresponds to the symmetric group `S(n)` in which the permutations of the constellation are defined. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c=Constellation(['(0,1)',None]) sage: c.degree() 2 sage: c=Constellation(['(0,1)','(0,3,2)(1,5)',None,'(4,3,2,1)']) sage: c.degree() 6 """ return ZZ_0 if len(self._g) == 0 else Integer(len(self._g[0]))
[docs] def length(self): r""" Number of permutations EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)','(0,2)','(0,3)',None]) sage: c.length() 4 sage: c = Constellation(['(0,1,3)',None,'(1,2)']) sage: c.length() 3 """ return len(self._g)
[docs] def profile(self, i=None): r""" Return the profile of ``self`` The profile of a constellation is the tuple of partitions associated to the conjugacy classes of the permutations of the constellation. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1,2)(3,4)','(0,3)',None]) sage: c.profile() ([3, 2], [2, 1, 1, 1], [5]) """ if i is None: return tuple(self.profile(j) for j in range(self.length())) else: return Partition(sorted(map(len, perm_cycles(self._g[i], True)), reverse=True))
passport = profile ## ACCESS TO INDIVIDUAL PERMUTATION
[docs] def g(self, i=None): r""" Return the permutation i of the constellation as a list EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1,2)(3,4)','(0,3)',None]) sage: c.g(0) [1, 2, 0, 4, 3] sage: c.g(1) [3, 1, 2, 0, 4] sage: c.g(2) [4, 0, 1, 2, 3] """ from copy import copy if i is None: return list(map(copy, self._g)) else: return copy(self._g[i])
[docs] def g_cycle_tuples(self, i, singletons=False): r""" Return the tuples associated to the cycles of the permutations of ``self`` EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(0,4)(1,3)',None]) sage: c.g_cycle_tuples(0) [[0, 1], [2, 3, 4]] sage: c.g_cycle_tuples(1) [[0, 4], [1, 3]] """ if i is None: return [perm_cycles(p, singletons) for p in self._g] else: return perm_cycles(self._g[i], singletons)
[docs] def g_cycle_string(self, i, singletons=False): r""" Return the permutations of a constellation, as cycles If `i` is ``None``, return all of them Otherwise, return just the permutation `g_i` as a cycle EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(0,4)(1,3)',None]) sage: c.g_cycle_string(None) ['(0,1)(2,3,4)', '(0,4)(1,3)', '(0,3)(1,2,4)'] sage: c.g_cycle_string(2) '(0,3)(1,2,4)' """ if i is None: return [perm_cycle_string(p, singletons) for p in self._g] else: return perm_cycle_string(self._g[i], singletons)
[docs] def g_next(self, i, j): r""" Return the image of `j` for the permutation `g_i` in constant time EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(1,2,0,5)','(2,3,4)(5,1)',None]) sage: c.g_next(0,0) 5 sage: all(c.g(i)[j] == c.g_next(i,j) for i in range(3) for j in range(6)) True """ if i is None: return [p[j] for p in self._g] else: return self._g[i][j]
[docs] def g_prev(self, i, j): r""" Return the image of `j` for the permutation `g_i` in constant time EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1,2)(3,4)','(0,4,2)',None]) sage: c.g_prev(0,0) 2 sage: all(c.g(i)[c.g_prev(i,j)] == j for i in range(3) for j in range(5)) True """ if i is None: return [self.g_prev(k, j) for k in range(self.length())] else: for k in range(i+1, self.length()): j = self._g[k][j] for k in range(0, i): j = self._g[k][j] return j
[docs] def g_orbit(self, i, j): r""" Returns the orbit of `j` under `g_i` EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]) sage: c.g_orbit(0,3) [3, 4, 2] sage: c.g_orbit(1,0) [0] """ if i is None: return [self.g_orbit(k, j) for k in range(self.length())] else: return perm_orbit(self._g[i], j)
[docs] def relabel(self, perm=None, return_map=False): r""" Relabel ``self``. If ``perm`` is provided then relabel with respect to ``perm``. Otherwise use canonical labels. In that case, if ``return_map`` is provided, the return also the map used for canonical labels. Algorithm: the cycle for g(0) are adjacent and the cycle are joined with respect to the other permutations. The minimum is taken for all possible renumerotations. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,4)(2)(3) g2 (0,1,3,2,4) sage: c2 = c.relabel(); c2 Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,2)(3)(4) g2 (0,1,4,3,2) The map returned when the option ``return_map`` is set to True can be used to set the relabelling:: sage: c3, perm = c.relabel(return_map=True) sage: c3 == c2 and c3 == c.relabel(perm=perm) True sage: d = c.relabel([4,3,1,0,2]); d Constellation of length 3 and degree 5 g0 (0,2,1)(3,4) g1 (0)(1)(2,3)(4) g2 (0,1,2,4,3) sage: d.is_isomorphic(c) True We check that after a random relabelling the new constellation is isomorphic to the initial one:: sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]) sage: p = SymmetricGroup(c.degree()).random_element() sage: cc = c.relabel([x-1 for x in p.domain()]).relabel() sage: cc.is_isomorphic(c) True """ if perm is not None: perm.extend(range(len(perm), self.degree())) g = [[None]*self.degree() for _ in range(self.length())] for i in range(len(perm)): for k in range(self.length()): g[k][perm[i]] = perm[self._g[k][i]] return Constellation(g=g, check=False, mutable=self.is_mutable()) if return_map: try: return self._normal_form, self._normal_form_map except AttributeError: pass else: try: return self._normal_form except AttributeError: pass # compute canonical labels if not self.is_connected(): raise ValueError("No canonical labels implemented for non connected constellation") c_win, m_win = perms_canonical_labels(self._g) c_win = self.parent()(c_win, mutable=False, check=False) if not self.is_mutable(): self._normal_form = c_win self._normal_form_map = m_win c_win._normal_form = c_win c_win._normal_form_map = m_win if return_map: return c_win, m_win else: return c_win
# BRAID GROUP ACTION
[docs] def braid_group_action(self, i): r""" Acts on self as the braid group generator that exchanges position `i` and `i+1` INPUT: - ``i`` -- integer in `[0, n-1]` where `n` is the length of self EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: sigma = lambda c, i: c.braid_group_action(i) sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,4)(2)(3) g2 (0,1,3,2,4) sage: sigma(c, 1) Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0,1,3,2,4) g2 (0,3)(1)(2)(4) Check the commutation relation:: sage: c = Constellation(['(0,1)(2,3,4)','(1,4)','(2,5)(0,4)',None]) sage: d = Constellation(['(0,1,3,5)','(2,3,4)','(0,3,5)',None]) sage: c13 = sigma(sigma(c, 0), 2) sage: c31 = sigma(sigma(c, 2), 0) sage: c13 == c31 True sage: d13 = sigma(sigma(d, 0), 2) sage: d31 = sigma(sigma(d, 2), 0) sage: d13 == d31 True Check the braid relation:: sage: c121 = sigma(sigma(sigma(c, 1), 2), 1) sage: c212 = sigma(sigma(sigma(c, 2), 1), 2) sage: c121 == c212 True sage: d121 = sigma(sigma(sigma(d, 1), 2), 1) sage: d212 = sigma(sigma(sigma(d, 2), 1), 2) sage: d121 == d212 True """ if i < 0 or i >= self.length(): raise ValueError("i should be between 0 and %d"%(self.length()-1)) j = i+1 if j == self.length(): # wrap around the cylinder j = 0 h = self.copy() si = self._g[i] sj = self._g[j] h._g[i] = sj h._g[j] = perm_compose(perm_compose(perm_invert(sj), si), sj) return h
[docs] def braid_group_orbit(self): r""" Returns the graph of the action of the braid group. The action is considered up to isomorphism of constellation. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c Constellation of length 3 and degree 5 g0 (0,1)(2,3,4) g1 (0)(1,4)(2)(3) g2 (0,1,3,2,4) sage: G = c.braid_group_orbit() sage: G.num_verts() 4 sage: G.num_edges() 12 """ from sage.graphs.digraph import DiGraph G = DiGraph(multiedges=True, loops=True) waiting = [self.relabel()] while waiting: c = waiting.pop() G.add_vertex(c) for i in range(self.length()): cc = self.braid_group_action(i).relabel() if cc not in G: waiting.append(cc) G.add_edge(c, cc, i) return G
[docs] class Constellations_all(UniqueRepresentation, Parent): Element = Constellation_class def __init__(self, connected=True): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: TestSuite(Constellations()).run() """ from sage.categories.infinite_enumerated_sets import InfiniteEnumeratedSets Parent.__init__(self, category=InfiniteEnumeratedSets()) self._connected = connected def __contains__(self,elt): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(connected=True) sage: D = Constellations(connected=False) sage: C([[1,0],None]) in C True sage: C([[1,0],None]) in D True sage: D([[1,0],None]) in C True sage: D([[1,0],None]) in D True sage: D([[0,1],None]) in C False sage: D([[0,1],None]) in D True """ return isinstance(elt, Constellation_class) and (not self._connected or elt.is_connected()) def _repr_(self): r""" String representation. TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: Constellations() # indirect doctest Connected constellations """ if self._connected: return "Connected constellations" else: return "Constellations" def _element_constructor_(self, g, check=True, mutable=False): r""" Returns a constellation. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellation sage: Constellation([[1,0],[1,0]]) # indirect doctest Constellation of length 2 and degree 2 g0 (0,1) g1 (0,1) """ if g is None or len(g) == 0: gg = [] else: if g.count(None) == 0: i = None elif g.count(None) == 1: i = g.index(None) del g[i] else: raise ValueError("only one permutation must be None") gg = list(map(perm_init, g)) equalize_perms(gg) if i is not None: h = range(len(gg[0])) for p in gg[i:]: h = perm_compose(h, p) for p in gg[:i]: h = perm_compose(h, p) gg.insert(i, perm_invert(h)) return self.element_class(self, gg, check, self._connected, mutable)
[docs] def an_element(self): r""" Return a constellation. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: Constellations().an_element() Constellation of length 2 and degree 2 g0 (0,1) g1 (0,1) """ return Constellations(2,2).an_element()
def __iter__(self): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: I = iter(Constellations()) sage: for _ in range(100): print(next(I)) Constellation of length 1 and degree 1 g0 (0) Constellation of length 2 and degree 1 g0 (0) g1 (0) Constellation of length 3 and degree 1 g0 (0) g1 (0) g2 (0) Constellation of length 2 and degree 2 g0 (0,1) g1 (0,1) ... Constellation of length 4 and degree 3 g0 (0)(1,2) g1 (0)(1,2) g2 (0,2,1) g3 (0,1,2) Constellation of length 4 and degree 3 g0 (0)(1,2) g1 (0)(1,2) g2 (0,2)(1) g3 (0,2)(1) """ n = 2 while True: for d in range(1, n): l = n - d yield from Constellations(l, d, connected=self._connected) n += 1
[docs] class Constellations_ld(UniqueRepresentation, Parent): r""" Constellations of given length and degree. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(2,3); C Connected constellations of length 2 and degree 3 sage: C([[1,2,0],[2,0,1]]) Constellation of length 2 and degree 3 g0 (0,1,2) g1 (0,2,1) sage: C.cardinality() 2 sage: Constellations(2,3,connected=False).cardinality() 6 """ def __init__(self, length, degree, connected=True): """ TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: TestSuite(Constellations(length=3,degree=4)).run() """ from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets Parent.__init__(self, facade=Constellations_all(), category=FiniteEnumeratedSets()) self._length = Integer(length) self._degree = Integer(degree) if self._length <= 0: raise ValueError("length should be a positive integer") if self._degree <= 0: raise ValueError("degree should be a positive integer") self._connected = bool(connected) def __contains__(self, elt): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(2,3,connected=True) sage: D = Constellations(2,3,connected=False) sage: C([[2,0,1],None]) in C True sage: D([[2,0,1],None]) in C True sage: D([[2,0,1],None]) in D True sage: D([[0,1,2],None]) in C False sage: D([[0,1,2],None]) in D True """ if elt not in Constellations(connected=self._connected): return False return elt.length() == self._length and elt.degree() == self._degree
[docs] def first(self): r""" Returns the first element in lexicographic order EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: const = Constellations(3,3);const Connected constellations of length 3 and degree 3 sage: const.first() Constellation of length 3 and degree 3 g0 (0,1,2) g1 (0,1,2) g2 (0,1,2) """ if self._connected: g = [str(tuple(range(self._degree)))] * self._length else: g = [range(d)] * self._length return self(g, check=False)
[docs] def last(self): r""" Returns the last element in lexicographic order EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: const=Constellations(3,3);const Connected constellations of length 3 and degree 3 sage: const.last() Constellation of length 3 and degree 3 g0 (0,2,1) g1 (0,2,1) g2 (0,2,1) """ if self._connected: g = [str(tuple(range(self._degree-1, -1, -1)))] * self._length else: g = [range(d-1,-1,-1)] * self._length return Constellation(g, check=False, mutable=False)
def _repr_(self): """ TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: Constellations(3,3)._repr_() 'Connected constellations of length 3 and degree 3' sage: Constellations(3,3,connected=False)._repr_() 'Constellations of length 3 and degree 3' """ s = "of length %d and degree %d" % (self._length, self._degree) if self._connected: return "Connected constellations " + s return "Constellations " + s def __iter__(self): """ iterator over all constellations of given degree and length EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: const = Constellations(3,3); const Connected constellations of length 3 and degree 3 sage: len([v for v in const]) 26 One can check the first few terms of sequence :oeis:`220754`:: sage: Constellations(4,1).cardinality() 1 sage: Constellations(4,2).cardinality() 7 sage: Constellations(4,3).cardinality() 194 sage: Constellations(4,4).cardinality() # long time 12858 """ from sage.arith.srange import srange from itertools import product, permutations if self._length == 1: if self._degree == 1: yield self([[0]]) return for p in product(permutations(srange(self._degree)), repeat=self._length-1): if self._connected and not perms_are_transitive(p): continue yield self(list(map(list,p))+[None], check=False)
[docs] def random_element(self, mutable=False): r""" a random element EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: const = Constellations(3,3) sage: const.random_element() Constellation of length 3 and degree 3 ... ... ... sage: c = const.random_element() sage: c.degree() == 3 and c.length() == 3 True """ from sage.groups.perm_gps.permgroup_named import SymmetricGroup from sage.groups.perm_gps.permgroup import PermutationGroup l = self._length d = self._degree g = [SymmetricGroup(d).random_element() for _ in range(l-1)] G = PermutationGroup(g) while not G.degree() == d or (self._connected and not G.is_transitive()): g = [SymmetricGroup(d).random_element() for _ in range(l-1)] G = PermutationGroup(g) return self(list(map(permutation_to_perm, g))+[None], mutable=mutable)
def _element_constructor_(self, *data, **options): r""" Build an element of self. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(2,3) sage: C([[1,2,0],[2,0,1]]) Constellation of length 2 and degree 3 g0 (0,1,2) g1 (0,2,1) sage: C([[2,1,0],[2,1,0]]) Traceback (most recent call last): ... ValueError: not connected """ c = Constellations(connected=self._connected)(*data, **options) if c.degree() != self._degree: raise ValueError("not able to build a constellation of degree %d from the given data"%self._degree) if c.length() != self._length: raise ValueError("not able to build a constellation of length %d from the given data"%self._length) return c
[docs] def an_element(self): r""" Return a constellation in ``self``. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: Constellations(2,3).an_element() Constellation of length 2 and degree 3 g0 (0,2,1) g1 (0,1,2) sage: Constellations(7,3).an_element() Constellation of length 7 and degree 3 g0 (0,2,1) g1 (0,1,2) g2 (0)(1)(2) g3 (0)(1)(2) g4 (0)(1)(2) g5 (0)(1)(2) g6 (0)(1)(2) """ if self._degree == 0 and self._length == 0: return self()([]) elif self._length == 1: if self._degree > 1: from sage.categories.sets_cat import EmptySetError raise EmptySetError return self()([[0]]) d = self._degree if self._connected: g = [[d-1] + list(range(d-1)), list(range(1,d)) + [0]] + [list(range(d))]*(self._length-2) else: g = [list(range(d))]*self._length return self(g)
[docs] def braid_group_action(self): r""" List of graphs that corresponds to the braid group action on ``self`` up to isomorphism. OUTPUT: - list of graphs EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(3,3) sage: C.braid_group_action() [Looped multi-digraph on 3 vertices, Looped multi-digraph on 3 vertices, Looped multi-digraph on 1 vertex] """ G = [] for c in self: c = c.relabel() if any(c in g for g in G): continue G.append(c.braid_group_orbit()) return G
[docs] def braid_group_orbits(self): r""" Orbits under the action of braid group. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(3,3) sage: O = C.braid_group_orbits() sage: len(O) 3 sage: [x.profile() for x in O[0]] [([1, 1, 1], [3], [3]), ([3], [1, 1, 1], [3]), ([3], [3], [1, 1, 1])] sage: [x.profile() for x in O[1]] [([2, 1], [2, 1], [3]), ([2, 1], [3], [2, 1]), ([3], [2, 1], [2, 1])] sage: [x.profile() for x in O[2]] [([3], [3], [3])] """ return [g.vertices(sort=True) for g in self.braid_group_action()]
[docs] class Constellations_p(UniqueRepresentation, Parent): r""" Constellations with fixed profile. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations([[3,1],[3,1],[2,2]]); C Constellations with profile ([3, 1], [3, 1], [2, 2]) sage: C.cardinality() 24 sage: C.first() Constellation of length 3 and degree 4 g0 (0)(1,2,3) g1 (0,1,2)(3) g2 (0,1)(2,3) sage: C.last() Constellation of length 3 and degree 4 g0 (0,3,2)(1) g1 (0,3,1)(2) g2 (0,1)(2,3) Note that the cardinality can also be computed using characters of the symmetric group (Frobenius formula):: sage: P = Partitions(4) sage: p1 = Partition([3,1]) sage: p2 = Partition([3,1]) sage: p3 = Partition([2,2]) sage: i1 = P.cardinality() - P.rank(p1) - 1 sage: i2 = P.cardinality() - P.rank(p2) - 1 sage: i3 = P.cardinality() - P.rank(p3) - 1 sage: s = 0 sage: for c in SymmetricGroup(4).irreducible_characters(): ....: v = c.values() ....: s += v[i1] * v[i2] * v[i3] / v[0] sage: c1 = p1.conjugacy_class_size() sage: c2 = p2.conjugacy_class_size() sage: c3 = p3.conjugacy_class_size() sage: c1 * c2 * c3 / factorial(4)**2 * s 1 The number obtained above is up to isomorphism. And we can check:: sage: len(C.isomorphism_representatives()) 1 """ def __init__(self, profile, connected=True): r""" OPTIONS: - ``profile`` -- a list of integer partitions of the same integer - ``connected`` -- a boolean (default: True) that specify if we consider only connected constellations. TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations([(3,1),(3,1),(2,2)]) sage: TestSuite(C).run() """ l = Integer(len(profile)) d = Integer(sum(profile[0])) for p in profile[1:]: if sum(p) != d: raise ValueError("all partition in the passport should have the same sum.") from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets Parent.__init__(self, facade=Constellations_all(), category=FiniteEnumeratedSets()) self._profile = profile self._degree = d self._length = l self._connected = connected def _repr_(self): r""" TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: Constellations(([3,2],[2,2,1])) Constellations with profile ([3, 2], [2, 2, 1]) """ return "Constellations with profile %s"%(self._profile,)
[docs] def isomorphism_representatives(self): r""" Return a set of isomorphism representative of ``self``. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations([[5], [4,1], [3,2]]) sage: C.cardinality() 240 sage: ir = sorted(C.isomorphism_representatives()) sage: len(ir) 2 sage: ir[0] Constellation of length 3 and degree 5 g0 (0,1,2,3,4) g1 (0)(1,2,3,4) g2 (0,4,2)(1,3) sage: ir[1] Constellation of length 3 and degree 5 g0 (0,1,2,3,4) g1 (0)(1,4,2,3) g2 (0,4)(1,2,3) """ result = set() for c in self: cc = c.relabel() if cc not in result: result.add(cc) return result
def _element_constructor_(self, *data, **options): r""" Build an element of self. TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations([(3,1),(3,1),(2,2)]) sage: c = C(['(1,2,3)','(0,1,2)','(0,1)(2,3)']); c Constellation of length 3 and degree 4 g0 (0)(1,2,3) g1 (0,1,2)(3) g2 (0,1)(2,3) sage: C(['(1,2,3)','(0,1,3)',None]) Traceback (most recent call last): ... ValueError: not able to build a constellation with profile ([3, 1], [3, 1], [2, 2]) from the given data """ c = Constellations(connected=self._connected)(*data, **options) if options.get('check', True): if c.profile() != self._profile: raise ValueError("not able to build a constellation with profile %s from the given data"%(self._profile,)) return c def __iter__(self): r""" Iterator of the elements in ``self``. .. NOTE:: The algorithm is highly non optimized and is *very* slow for large entries! In order to make a better one, we need an iterator over conjugacy classes of the symmetric group. TESTS:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations([(3,1),(3,1),(2,2)]) sage: for c in C: print(c) Constellation of length 3 and degree 4 g0 (0)(1,2,3) g1 (0,1,2)(3) g2 (0,1)(2,3) Constellation of length 3 and degree 4 g0 (0)(1,2,3) g1 (0,2,3)(1) g2 (0,2)(1,3) ... Constellation of length 3 and degree 4 g0 (0,3,2)(1) g1 (0,1,2)(3) g2 (0,3)(1,2) Constellation of length 3 and degree 4 g0 (0,3,2)(1) g1 (0,3,1)(2) g2 (0,1)(2,3) """ from sage.arith.srange import srange from itertools import product, permutations if self._length == 1: if self._degree == 1: yield self([[0]]) return for p in product(permutations(srange(self._degree)), repeat=self._length-1): if self._connected and not perms_are_transitive(p): continue c = Constellations(connected=self._connected)(list(map(list,p))+[None], check=False) if c.profile() == self._profile: yield c
[docs] def braid_group_action(self): r""" List of graphs that corresponds to the braid group action on ``self`` up to isomorphism. OUTPUT: - list of graphs EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(3,3) sage: C.braid_group_action() [Looped multi-digraph on 3 vertices, Looped multi-digraph on 3 vertices, Looped multi-digraph on 1 vertex] """ G = [] for c in self: c = c.relabel() if any(c in g for g in G): continue G.append(c.braid_group_orbit()) return G
[docs] def braid_group_orbits(self): r""" Orbits under the action of braid group. EXAMPLES:: sage: from surface_dynamics.misc.constellation import Constellations sage: C = Constellations(3,3) sage: O = C.braid_group_orbits() sage: len(O) 3 sage: [x.profile() for x in O[0]] [([1, 1, 1], [3], [3]), ([3], [1, 1, 1], [3]), ([3], [3], [1, 1, 1])] sage: [x.profile() for x in O[1]] [([2, 1], [2, 1], [3]), ([2, 1], [3], [2, 1]), ([3], [2, 1], [2, 1])] sage: [x.profile() for x in O[2]] [([3], [3], [3])] """ return [g.vertices(sort=True) for g in self.braid_group_action()]