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