diff --git a/src/sage/groups/artin.py b/src/sage/groups/artin.py index 618e6e38055..a5f7a8e4529 100644 --- a/src/sage/groups/artin.py +++ b/src/sage/groups/artin.py @@ -710,6 +710,47 @@ def as_permutation_group(self): """ raise ValueError("the group is infinite") + def cayley_graph(self, side='right', simple=False, elements=None, + generators=None, connecting_set=None): + r""" + Return the Cayley graph of ``self``. + + Since Artin groups are infinite, this method requires ``elements`` + to be specified. It uses the generic semigroup Cayley graph + implementation. + + INPUT: + + - ``side`` -- ``'left'``, ``'right'``, or ``'twosided'``: + the side on which the generators act (default: ``'right'``) + - ``simple`` -- boolean (default: ``False``); if ``True``, returns + a simple graph (no loops, no labels, no multiple edges) + - ``generators`` -- list, tuple, or family of elements + of ``self`` (default: ``self.gens()``) + - ``connecting_set`` -- alias for ``generators``; deprecated + - ``elements`` -- list (or iterable) of elements of ``self`` + (required for infinite groups) + + OUTPUT: :class:`DiGraph` + + EXAMPLES:: + + sage: def ball(group, radius): + ....: ret = set() + ....: ret.add(group.one()) + ....: for length in range(1, radius): + ....: for w in Words(alphabet=group.gens(), length=length): + ....: ret.add(prod(w)) + ....: return ret + sage: A = ArtinGroup(['A',2]) # needs sage.rings.number_field + sage: GA = A.cayley_graph(elements=ball(A, 4), generators=A.gens()); GA # needs sage.combinat sage.graphs sage.rings.number_field + Digraph on 14 vertices + """ + from sage.categories.semigroups import Semigroups + return Semigroups().ParentMethods.cayley_graph( + self, side=side, simple=simple, elements=elements, + generators=generators, connecting_set=connecting_set) + def coxeter_type(self): """ Return the Coxeter type of ``self``. diff --git a/src/sage/groups/braid.py b/src/sage/groups/braid.py index 7af2d1cdabe..f19351a0ac1 100644 --- a/src/sage/groups/braid.py +++ b/src/sage/groups/braid.py @@ -2757,6 +2757,47 @@ def as_permutation_group(self): """ raise ValueError("the group is infinite") + def cayley_graph(self, side='right', simple=False, elements=None, + generators=None, connecting_set=None): + r""" + Return the Cayley graph of ``self``. + + Since braid groups are infinite, this method requires ``elements`` + to be specified. It uses the generic semigroup Cayley graph + implementation. + + INPUT: + + - ``side`` -- ``'left'``, ``'right'``, or ``'twosided'``: + the side on which the generators act (default: ``'right'``) + - ``simple`` -- boolean (default: ``False``); if ``True``, returns + a simple graph (no loops, no labels, no multiple edges) + - ``generators`` -- list, tuple, or family of elements + of ``self`` (default: ``self.gens()``) + - ``connecting_set`` -- alias for ``generators``; deprecated + - ``elements`` -- list (or iterable) of elements of ``self`` + (required for infinite groups) + + OUTPUT: :class:`DiGraph` + + EXAMPLES:: + + sage: def ball(group, radius): + ....: ret = set() + ....: ret.add(group.one()) + ....: for length in range(1, radius): + ....: for w in Words(alphabet=group.gens(), length=length): + ....: ret.add(prod(w)) + ....: return ret + sage: B = BraidGroup(4) + sage: GB = B.cayley_graph(elements=ball(B, 4), generators=B.gens()); GB # needs sage.combinat sage.graphs + Digraph on 31 vertices + """ + from sage.categories.semigroups import Semigroups + return Semigroups().ParentMethods.cayley_graph( + self, side=side, simple=simple, elements=elements, + generators=generators, connecting_set=connecting_set) + def strands(self): """ Return the number of strands. diff --git a/src/sage/groups/finitely_presented.py b/src/sage/groups/finitely_presented.py index 0716b9003d6..9c8ec07d5ce 100644 --- a/src/sage/groups/finitely_presented.py +++ b/src/sage/groups/finitely_presented.py @@ -313,6 +313,41 @@ def Tietze(self): tl = self.gap().UnderlyingElement().TietzeWordAbstractWord() return tuple(tl.sage()) + def __hash__(self): + """ + Return a hash for this group element. + + .. WARNING:: + + The hash is based on the Tietze word representation of the + element, not on the actual group element. Two elements that + are equal in the group but have different Tietze representations + will have different hashes. This is because the word problem + for finitely presented groups is undecidable in general, so + it is not possible to compute a consistent hash based on the + actual group element. Use :meth:`cayley_graph` on the group + for Cayley graph computations, as it handles this properly. + + EXAMPLES:: + + sage: G. = FreeGroup() + sage: H = G / [a^2, b^3, a*b*a^-1*b^-1] + sage: H.inject_variables() + Defining a, b + sage: hash(a*b) == hash(H([1, 2])) + True + + Note that equal elements may have different hashes:: + + sage: x = a*a + sage: y = H.one() + sage: x == y + True + sage: hash(x) == hash(y) # hashes are based on Tietze words + False + """ + return hash(self.Tietze()) + def __call__(self, *values, **kwds): """ Replace the generators of the free group with ``values``. @@ -1015,6 +1050,217 @@ def as_permutation_group(self, limit=4096000): return PermutationGroup([ Permutation(coset_table[2*i]) for i in range(len(coset_table)//2)]) + def cayley_graph(self, side='right', simple=False, elements=None, + generators=None, limit=4096000): + r""" + Return the Cayley graph of this finitely presented group. + + Unlike the generic Cayley graph method from the category of semigroups, + this method properly identifies group elements that are equal but + represented by different words. It does this by first converting + the group to a permutation group representation. + + INPUT: + + - ``side`` -- ``'right'``, ``'left'``, or ``'twosided'``: + the side on which the generators act (default: ``'right'``) + - ``simple`` -- boolean (default: ``False``); if ``True``, returns + a simple graph (no loops, no labels, no multiple edges) + - ``generators`` -- list, tuple, or family of elements + of ``self`` (default: ``self.gens()``) + - ``elements`` -- list (or iterable) of elements of ``self`` + (default: all elements if the group is finite) + - ``limit`` -- integer (default: 4096000); the maximal number + of cosets for the permutation group computation + + OUTPUT: :class:`DiGraph` + + EXAMPLES: + + The Cayley graph of a cyclic group:: + + sage: G. = FreeGroup() + sage: H = G / [a^6] + sage: cg = H.cayley_graph() + sage: cg.num_verts() + 6 + sage: cg.num_edges() + 6 + + A non-abelian example - the semidirect product of `\ZZ/4\ZZ` and + `\ZZ/13\ZZ`:: + + sage: F. = FreeGroup() + sage: G = F / [x^4, y^13, x*y*x^-1*y^-5] + sage: a, b = G.gens() + sage: G.order() + 52 + sage: cg = G.cayley_graph() + sage: cg.num_verts() + 52 + + The dihedral group `D_4`:: + + sage: G. = FreeGroup() + sage: H = G / [a^2, b^4, (a*b)^2] + sage: cg = H.cayley_graph() + sage: cg.num_verts() + 8 + + Using specific generators:: + + sage: G. = FreeGroup() + sage: H = G / [a^3, b^3, (a*b)^2] + sage: cg = H.cayley_graph(generators=[H(a)]) + sage: cg.num_edges() + 12 + + TESTS:: + + sage: G. = FreeGroup() + sage: H = G / [a^2, b^3, a*b*a^-1*b^-1] + sage: cg = H.cayley_graph(side='left') + sage: cg.num_verts() + 6 + sage: cg = H.cayley_graph(side='twosided') + sage: cg.num_verts() + 6 + + .. WARNING:: + + This method requires the group to be finite (or ``elements`` + must be provided). Computing whether a finitely presented + group is finite is undecidable in general. + + ALGORITHM: + + Uses :meth:`as_permutation_group` to convert the group to a + permutation group, constructs the Cayley graph there, and + converts vertices back to elements of the finitely presented + group. + """ + from sage.graphs.digraph import DiGraph + from sage.categories.groups import Groups + + if side not in ["left", "right", "twosided"]: + raise ValueError("option 'side' must be 'left', 'right' or 'twosided'") + + # Get the permutation group representation + perm_group = self.as_permutation_group(limit=limit) + + # The generators of perm_group correspond to generators of self + perm_gens = perm_group.gens() + self_gens = self.gens() + + if generators is None: + generators = list(self_gens) + else: + generators = [self(g) for g in generators] + + # Get all elements via the permutation group + if elements is None: + perm_elements = list(perm_group) + else: + elements = [self(e) for e in elements] + perm_elements = [] + for e in elements: + t = e.Tietze() + perm_e = perm_group.one() + for i in t: + if i > 0: + perm_e = perm_e * perm_gens[i-1] + else: + perm_e = perm_e * perm_gens[-i-1].inverse() + perm_elements.append(perm_e) + + # Build a mapping from perm elements to fp elements by doing a BFS from the identity + perm_to_fp = {perm_group.one(): self.one()} + visited = {perm_group.one()} + queue = [perm_group.one()] + + # Precompute generator pairs (fp element, perm element) + gen_pairs = [] + for g in self_gens: + t = g.Tietze() + perm_g = perm_group.one() + for i in t: + if i > 0: + perm_g = perm_g * perm_gens[i-1] + else: + perm_g = perm_g * perm_gens[-i-1].inverse() + gen_pairs.append((g, perm_g)) + + while queue: + current_perm = queue.pop(0) + current_fp = perm_to_fp[current_perm] + for fp_g, perm_g in gen_pairs: + # Try right multiplication + next_perm = current_perm * perm_g + if next_perm not in visited: + visited.add(next_perm) + perm_to_fp[next_perm] = current_fp * fp_g + queue.append(next_perm) + # Try right multiplication by inverse + next_perm = current_perm * perm_g.inverse() + if next_perm not in visited: + visited.add(next_perm) + perm_to_fp[next_perm] = current_fp * fp_g**(-1) + queue.append(next_perm) + + # Filter to only requested elements + perm_elements_set = set(perm_elements) + perm_elements = [pe for pe in perm_to_fp if pe in perm_elements_set] + + # Convert generators to permutation group + perm_generators = {} + for g in generators: + t = g.Tietze() + perm_g = perm_group.one() + for i in t: + if i > 0: + perm_g = perm_g * perm_gens[i-1] + else: + perm_g = perm_g * perm_gens[-i-1].inverse() + perm_generators[g] = perm_g + + # Build the graph + if simple or self in Groups(): + result = DiGraph() + else: + result = DiGraph(multiedges=True, loops=True) + + # Add vertices (using fp group elements) + result.add_vertices(perm_to_fp[pe] for pe in perm_elements) + + left = (side == "left" or side == "twosided") + right = (side == "right" or side == "twosided") + + for perm_x in perm_elements: + fp_x = perm_to_fp[perm_x] + for fp_g, perm_g in perm_generators.items(): + if left: + perm_target = perm_g * perm_x + if perm_target in perm_to_fp: + if simple: + if fp_x != perm_to_fp[perm_target]: + result.add_edge([fp_x, perm_to_fp[perm_target]]) + elif side == "twosided": + result.add_edge([fp_x, perm_to_fp[perm_target], (fp_g, "left")]) + else: + result.add_edge([fp_x, perm_to_fp[perm_target], fp_g]) + if right: + perm_target = perm_x * perm_g + if perm_target in perm_to_fp: + if simple: + if fp_x != perm_to_fp[perm_target]: + result.add_edge([fp_x, perm_to_fp[perm_target]]) + elif side == "twosided": + result.add_edge([fp_x, perm_to_fp[perm_target], (fp_g, "right")]) + else: + result.add_edge([fp_x, perm_to_fp[perm_target], fp_g]) + + return result + def direct_product(self, H, reduced=False, new_names=True): r""" Return the direct product of ``self`` with finitely presented