from __future__ import annotations from sympy.core import S from sympy.core.expr import Expr from sympy.core.symbol import Symbol, symbols as _symbols from sympy.core.sympify import CantSympify from sympy.printing.defaults import DefaultPrinting from sympy.utilities import public from sympy.utilities.iterables import flatten, is_sequence from sympy.utilities.magic import pollute from sympy.utilities.misc import as_int @public def free_group(symbols): """Construct a free group returning ``(FreeGroup, (f_0, f_1, ..., f_(n-1))``. Parameters ========== symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y, z = free_group("x, y, z") >>> F >>> x**2*y**-1 x**2*y**-1 >>> type(_) """ _free_group = FreeGroup(symbols) return (_free_group,) + tuple(_free_group.generators) @public def xfree_group(symbols): """Construct a free group returning ``(FreeGroup, (f_0, f_1, ..., f_(n-1)))``. Parameters ========== symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) Examples ======== >>> from sympy.combinatorics.free_groups import xfree_group >>> F, (x, y, z) = xfree_group("x, y, z") >>> F >>> y**2*x**-2*z**-1 y**2*x**-2*z**-1 >>> type(_) """ _free_group = FreeGroup(symbols) return (_free_group, _free_group.generators) @public def vfree_group(symbols): """Construct a free group and inject ``f_0, f_1, ..., f_(n-1)`` as symbols into the global namespace. Parameters ========== symbols : str, Symbol/Expr or sequence of str, Symbol/Expr (may be empty) Examples ======== >>> from sympy.combinatorics.free_groups import vfree_group >>> vfree_group("x, y, z") >>> x**2*y**-2*z # noqa: F821 x**2*y**-2*z >>> type(_) """ _free_group = FreeGroup(symbols) pollute([sym.name for sym in _free_group.symbols], _free_group.generators) return _free_group def _parse_symbols(symbols): if not symbols: return () if isinstance(symbols, str): return _symbols(symbols, seq=True) elif isinstance(symbols, (Expr, FreeGroupElement)): return (symbols,) elif is_sequence(symbols): if all(isinstance(s, str) for s in symbols): return _symbols(symbols) elif all(isinstance(s, Expr) for s in symbols): return symbols raise ValueError("The type of `symbols` must be one of the following: " "a str, Symbol/Expr or a sequence of " "one of these types") ############################################################################## # FREE GROUP # ############################################################################## _free_group_cache: dict[int, FreeGroup] = {} class FreeGroup(DefaultPrinting): """ Free group with finite or infinite number of generators. Its input API is that of a str, Symbol/Expr or a sequence of one of these types (which may be empty) See Also ======== sympy.polys.rings.PolyRing References ========== .. [1] https://www.gap-system.org/Manuals/doc/ref/chap37.html .. [2] https://en.wikipedia.org/wiki/Free_group """ is_associative = True is_group = True is_FreeGroup = True is_PermutationGroup = False relators: list[Expr] = [] def __new__(cls, symbols): symbols = tuple(_parse_symbols(symbols)) rank = len(symbols) _hash = hash((cls.__name__, symbols, rank)) obj = _free_group_cache.get(_hash) if obj is None: obj = object.__new__(cls) obj._hash = _hash obj._rank = rank # dtype method is used to create new instances of FreeGroupElement obj.dtype = type("FreeGroupElement", (FreeGroupElement,), {"group": obj}) obj.symbols = symbols obj.generators = obj._generators() obj._gens_set = set(obj.generators) for symbol, generator in zip(obj.symbols, obj.generators): if isinstance(symbol, Symbol): name = symbol.name if hasattr(obj, name): setattr(obj, name, generator) _free_group_cache[_hash] = obj return obj def _generators(group): """Returns the generators of the FreeGroup. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y, z = free_group("x, y, z") >>> F.generators (x, y, z) """ gens = [] for sym in group.symbols: elm = ((sym, 1),) gens.append(group.dtype(elm)) return tuple(gens) def clone(self, symbols=None): return self.__class__(symbols or self.symbols) def __contains__(self, i): """Return True if ``i`` is contained in FreeGroup.""" if not isinstance(i, FreeGroupElement): return False group = i.group return self == group def __hash__(self): return self._hash def __len__(self): return self.rank def __str__(self): if self.rank > 30: str_form = "" % self.rank else: str_form = "" return str_form __repr__ = __str__ def __getitem__(self, index): symbols = self.symbols[index] return self.clone(symbols=symbols) def __eq__(self, other): """No ``FreeGroup`` is equal to any "other" ``FreeGroup``. """ return self is other def index(self, gen): """Return the index of the generator `gen` from ``(f_0, ..., f_(n-1))``. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> F.index(y) 1 >>> F.index(x) 0 """ if isinstance(gen, self.dtype): return self.generators.index(gen) else: raise ValueError("expected a generator of Free Group %s, got %s" % (self, gen)) def order(self): """Return the order of the free group. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> F.order() oo >>> free_group("")[0].order() 1 """ if self.rank == 0: return S.One else: return S.Infinity @property def elements(self): """ Return the elements of the free group. Examples ======== >>> from sympy.combinatorics import free_group >>> (z,) = free_group("") >>> z.elements {} """ if self.rank == 0: # A set containing Identity element of `FreeGroup` self is returned return {self.identity} else: raise ValueError("Group contains infinitely many elements" ", hence cannot be represented") @property def rank(self): r""" In group theory, the `rank` of a group `G`, denoted `G.rank`, can refer to the smallest cardinality of a generating set for G, that is \operatorname{rank}(G)=\min\{ |X|: X\subseteq G, \left\langle X\right\rangle =G\}. """ return self._rank @property def is_abelian(self): """Returns if the group is Abelian. Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> f.is_abelian False """ return self.rank in (0, 1) @property def identity(self): """Returns the identity element of free group.""" return self.dtype() def contains(self, g): """Tests if Free Group element ``g`` belong to self, ``G``. In mathematical terms any linear combination of generators of a Free Group is contained in it. Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> f.contains(x**3*y**2) True """ if not isinstance(g, FreeGroupElement): return False elif self != g.group: return False else: return True def center(self): """Returns the center of the free group `self`.""" return {self.identity} ############################################################################ # FreeGroupElement # ############################################################################ class FreeGroupElement(CantSympify, DefaultPrinting, tuple): """Used to create elements of FreeGroup. It cannot be used directly to create a free group element. It is called by the `dtype` method of the `FreeGroup` class. """ is_assoc_word = True def new(self, init): return self.__class__(init) _hash = None def __hash__(self): _hash = self._hash if _hash is None: self._hash = _hash = hash((self.group, frozenset(tuple(self)))) return _hash def copy(self): return self.new(self) @property def is_identity(self): if self.array_form == (): return True else: return False @property def array_form(self): """ SymPy provides two different internal kinds of representation of associative words. The first one is called the `array_form` which is a tuple containing `tuples` as its elements, where the size of each tuple is two. At the first position the tuple contains the `symbol-generator`, while at the second position of tuple contains the exponent of that generator at the position. Since elements (i.e. words) do not commute, the indexing of tuple makes that property to stay. The structure in ``array_form`` of ``FreeGroupElement`` is of form: ``( ( symbol_of_gen, exponent ), ( , ), ... ( , ) )`` Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> (x*z).array_form ((x, 1), (z, 1)) >>> (x**2*z*y*x**2).array_form ((x, 2), (z, 1), (y, 1), (x, 2)) See Also ======== letter_repr """ return tuple(self) @property def letter_form(self): """ The letter representation of a ``FreeGroupElement`` is a tuple of generator symbols, with each entry corresponding to a group generator. Inverses of the generators are represented by negative generator symbols. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b, c, d = free_group("a b c d") >>> (a**3).letter_form (a, a, a) >>> (a**2*d**-2*a*b**-4).letter_form (a, a, -d, -d, a, -b, -b, -b, -b) >>> (a**-2*b**3*d).letter_form (-a, -a, b, b, b, d) See Also ======== array_form """ return tuple(flatten([(i,)*j if j > 0 else (-i,)*(-j) for i, j in self.array_form])) def __getitem__(self, i): group = self.group r = self.letter_form[i] if r.is_Symbol: return group.dtype(((r, 1),)) else: return group.dtype(((-r, -1),)) def index(self, gen): if len(gen) != 1: raise ValueError() return (self.letter_form).index(gen.letter_form[0]) @property def letter_form_elm(self): """ """ group = self.group r = self.letter_form return [group.dtype(((elm,1),)) if elm.is_Symbol \ else group.dtype(((-elm,-1),)) for elm in r] @property def ext_rep(self): """This is called the External Representation of ``FreeGroupElement`` """ return tuple(flatten(self.array_form)) def __contains__(self, gen): return gen.array_form[0][0] in tuple([r[0] for r in self.array_form]) def __str__(self): if self.is_identity: return "" str_form = "" array_form = self.array_form for i in range(len(array_form)): if i == len(array_form) - 1: if array_form[i][1] == 1: str_form += str(array_form[i][0]) else: str_form += str(array_form[i][0]) + \ "**" + str(array_form[i][1]) else: if array_form[i][1] == 1: str_form += str(array_form[i][0]) + "*" else: str_form += str(array_form[i][0]) + \ "**" + str(array_form[i][1]) + "*" return str_form __repr__ = __str__ def __pow__(self, n): n = as_int(n) group = self.group if n == 0: return group.identity if n < 0: n = -n return (self.inverse())**n result = self for i in range(n - 1): result = result*self # this method can be improved instead of just returning the # multiplication of elements return result def __mul__(self, other): """Returns the product of elements belonging to the same ``FreeGroup``. Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> x*y**2*y**-4 x*y**-2 >>> z*y**-2 z*y**-2 >>> x**2*y*y**-1*x**-2 """ group = self.group if not isinstance(other, group.dtype): raise TypeError("only FreeGroup elements of same FreeGroup can " "be multiplied") if self.is_identity: return other if other.is_identity: return self r = list(self.array_form + other.array_form) zero_mul_simp(r, len(self.array_form) - 1) return group.dtype(tuple(r)) def __truediv__(self, other): group = self.group if not isinstance(other, group.dtype): raise TypeError("only FreeGroup elements of same FreeGroup can " "be multiplied") return self*(other.inverse()) def __rtruediv__(self, other): group = self.group if not isinstance(other, group.dtype): raise TypeError("only FreeGroup elements of same FreeGroup can " "be multiplied") return other*(self.inverse()) def __add__(self, other): return NotImplemented def inverse(self): """ Returns the inverse of a ``FreeGroupElement`` element Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> x.inverse() x**-1 >>> (x*y).inverse() y**-1*x**-1 """ group = self.group r = tuple([(i, -j) for i, j in self.array_form[::-1]]) return group.dtype(r) def order(self): """Find the order of a ``FreeGroupElement``. Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y = free_group("x y") >>> (x**2*y*y**-1*x**-2).order() 1 """ if self.is_identity: return S.One else: return S.Infinity def commutator(self, other): """ Return the commutator of `self` and `x`: ``~x*~self*x*self`` """ group = self.group if not isinstance(other, group.dtype): raise ValueError("commutator of only FreeGroupElement of the same " "FreeGroup exists") else: return self.inverse()*other.inverse()*self*other def eliminate_words(self, words, _all=False, inverse=True): ''' Replace each subword from the dictionary `words` by words[subword]. If words is a list, replace the words by the identity. ''' again = True new = self if isinstance(words, dict): while again: again = False for sub in words: prev = new new = new.eliminate_word(sub, words[sub], _all=_all, inverse=inverse) if new != prev: again = True else: while again: again = False for sub in words: prev = new new = new.eliminate_word(sub, _all=_all, inverse=inverse) if new != prev: again = True return new def eliminate_word(self, gen, by=None, _all=False, inverse=True): """ For an associative word `self`, a subword `gen`, and an associative word `by` (identity by default), return the associative word obtained by replacing each occurrence of `gen` in `self` by `by`. If `_all = True`, the occurrences of `gen` that may appear after the first substitution will also be replaced and so on until no occurrences are found. This might not always terminate (e.g. `(x).eliminate_word(x, x**2, _all=True)`). Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y = free_group("x y") >>> w = x**5*y*x**2*y**-4*x >>> w.eliminate_word( x, x**2 ) x**10*y*x**4*y**-4*x**2 >>> w.eliminate_word( x, y**-1 ) y**-11 >>> w.eliminate_word(x**5) y*x**2*y**-4*x >>> w.eliminate_word(x*y, y) x**4*y*x**2*y**-4*x See Also ======== substituted_word """ if by is None: by = self.group.identity if self.is_independent(gen) or gen == by: return self if gen == self: return by if gen**-1 == by: _all = False word = self l = len(gen) try: i = word.subword_index(gen) k = 1 except ValueError: if not inverse: return word try: i = word.subword_index(gen**-1) k = -1 except ValueError: return word word = word.subword(0, i)*by**k*word.subword(i+l, len(word)).eliminate_word(gen, by) if _all: return word.eliminate_word(gen, by, _all=True, inverse=inverse) else: return word def __len__(self): """ For an associative word `self`, returns the number of letters in it. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> w = a**5*b*a**2*b**-4*a >>> len(w) 13 >>> len(a**17) 17 >>> len(w**0) 0 """ return sum(abs(j) for (i, j) in self) def __eq__(self, other): """ Two associative words are equal if they are words over the same alphabet and if they are sequences of the same letters. This is equivalent to saying that the external representations of the words are equal. There is no "universal" empty word, every alphabet has its own empty word. Examples ======== >>> from sympy.combinatorics import free_group >>> f, swapnil0, swapnil1 = free_group("swapnil0 swapnil1") >>> f >>> g, swap0, swap1 = free_group("swap0 swap1") >>> g >>> swapnil0 == swapnil1 False >>> swapnil0*swapnil1 == swapnil1/swapnil1*swapnil0*swapnil1 True >>> swapnil0*swapnil1 == swapnil1*swapnil0 False >>> swapnil1**0 == swap0**0 False """ group = self.group if not isinstance(other, group.dtype): return False return tuple.__eq__(self, other) def __lt__(self, other): """ The ordering of associative words is defined by length and lexicography (this ordering is called short-lex ordering), that is, shorter words are smaller than longer words, and words of the same length are compared w.r.t. the lexicographical ordering induced by the ordering of generators. Generators are sorted according to the order in which they were created. If the generators are invertible then each generator `g` is larger than its inverse `g^{-1}`, and `g^{-1}` is larger than every generator that is smaller than `g`. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> b < a False >>> a < a.inverse() False """ group = self.group if not isinstance(other, group.dtype): raise TypeError("only FreeGroup elements of same FreeGroup can " "be compared") l = len(self) m = len(other) # implement lenlex order if l < m: return True elif l > m: return False for i in range(l): a = self[i].array_form[0] b = other[i].array_form[0] p = group.symbols.index(a[0]) q = group.symbols.index(b[0]) if p < q: return True elif p > q: return False elif a[1] < b[1]: return True elif a[1] > b[1]: return False return False def __le__(self, other): return (self == other or self < other) def __gt__(self, other): """ Examples ======== >>> from sympy.combinatorics import free_group >>> f, x, y, z = free_group("x y z") >>> y**2 > x**2 True >>> y*z > z*y False >>> x > x.inverse() True """ group = self.group if not isinstance(other, group.dtype): raise TypeError("only FreeGroup elements of same FreeGroup can " "be compared") return not self <= other def __ge__(self, other): return not self < other def exponent_sum(self, gen): """ For an associative word `self` and a generator or inverse of generator `gen`, ``exponent_sum`` returns the number of times `gen` appears in `self` minus the number of times its inverse appears in `self`. If neither `gen` nor its inverse occur in `self` then 0 is returned. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> w = x**2*y**3 >>> w.exponent_sum(x) 2 >>> w.exponent_sum(x**-1) -2 >>> w = x**2*y**4*x**-3 >>> w.exponent_sum(x) -1 See Also ======== generator_count """ if len(gen) != 1: raise ValueError("gen must be a generator or inverse of a generator") s = gen.array_form[0] return s[1]*sum([i[1] for i in self.array_form if i[0] == s[0]]) def generator_count(self, gen): """ For an associative word `self` and a generator `gen`, ``generator_count`` returns the multiplicity of generator `gen` in `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> w = x**2*y**3 >>> w.generator_count(x) 2 >>> w = x**2*y**4*x**-3 >>> w.generator_count(x) 5 See Also ======== exponent_sum """ if len(gen) != 1 or gen.array_form[0][1] < 0: raise ValueError("gen must be a generator") s = gen.array_form[0] return s[1]*sum([abs(i[1]) for i in self.array_form if i[0] == s[0]]) def subword(self, from_i, to_j, strict=True): """ For an associative word `self` and two positive integers `from_i` and `to_j`, `subword` returns the subword of `self` that begins at position `from_i` and ends at `to_j - 1`, indexing is done with origin 0. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> w = a**5*b*a**2*b**-4*a >>> w.subword(2, 6) a**3*b """ group = self.group if not strict: from_i = max(from_i, 0) to_j = min(len(self), to_j) if from_i < 0 or to_j > len(self): raise ValueError("`from_i`, `to_j` must be positive and no greater than " "the length of associative word") if to_j <= from_i: return group.identity else: letter_form = self.letter_form[from_i: to_j] array_form = letter_form_to_array_form(letter_form, group) return group.dtype(array_form) def subword_index(self, word, start = 0): ''' Find the index of `word` in `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> w = a**2*b*a*b**3 >>> w.subword_index(a*b*a*b) 1 ''' l = len(word) self_lf = self.letter_form word_lf = word.letter_form index = None for i in range(start,len(self_lf)-l+1): if self_lf[i:i+l] == word_lf: index = i break if index is not None: return index else: raise ValueError("The given word is not a subword of self") def is_dependent(self, word): """ Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> (x**4*y**-3).is_dependent(x**4*y**-2) True >>> (x**2*y**-1).is_dependent(x*y) False >>> (x*y**2*x*y**2).is_dependent(x*y**2) True >>> (x**12).is_dependent(x**-4) True See Also ======== is_independent """ try: return self.subword_index(word) is not None except ValueError: pass try: return self.subword_index(word**-1) is not None except ValueError: return False def is_independent(self, word): """ See Also ======== is_dependent """ return not self.is_dependent(word) def contains_generators(self): """ Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y, z = free_group("x, y, z") >>> (x**2*y**-1).contains_generators() {x, y} >>> (x**3*z).contains_generators() {x, z} """ group = self.group gens = set() for syllable in self.array_form: gens.add(group.dtype(((syllable[0], 1),))) return set(gens) def cyclic_subword(self, from_i, to_j): group = self.group l = len(self) letter_form = self.letter_form period1 = int(from_i/l) if from_i >= l: from_i -= l*period1 to_j -= l*period1 diff = to_j - from_i word = letter_form[from_i: to_j] period2 = int(to_j/l) - 1 word += letter_form*period2 + letter_form[:diff-l+from_i-l*period2] word = letter_form_to_array_form(word, group) return group.dtype(word) def cyclic_conjugates(self): """Returns a words which are cyclic to the word `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> w = x*y*x*y*x >>> w.cyclic_conjugates() {x*y*x**2*y, x**2*y*x*y, y*x*y*x**2, y*x**2*y*x, x*y*x*y*x} >>> s = x*y*x**2*y*x >>> s.cyclic_conjugates() {x**2*y*x**2*y, y*x**2*y*x**2, x*y*x**2*y*x} References ========== .. [1] https://planetmath.org/cyclicpermutation """ return {self.cyclic_subword(i, i+len(self)) for i in range(len(self))} def is_cyclic_conjugate(self, w): """ Checks whether words ``self``, ``w`` are cyclic conjugates. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> w1 = x**2*y**5 >>> w2 = x*y**5*x >>> w1.is_cyclic_conjugate(w2) True >>> w3 = x**-1*y**5*x**-1 >>> w3.is_cyclic_conjugate(w2) False """ l1 = len(self) l2 = len(w) if l1 != l2: return False w1 = self.identity_cyclic_reduction() w2 = w.identity_cyclic_reduction() letter1 = w1.letter_form letter2 = w2.letter_form str1 = ' '.join(map(str, letter1)) str2 = ' '.join(map(str, letter2)) if len(str1) != len(str2): return False return str1 in str2 + ' ' + str2 def number_syllables(self): """Returns the number of syllables of the associative word `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> f, swapnil0, swapnil1 = free_group("swapnil0 swapnil1") >>> (swapnil1**3*swapnil0*swapnil1**-1).number_syllables() 3 """ return len(self.array_form) def exponent_syllable(self, i): """ Returns the exponent of the `i`-th syllable of the associative word `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> w = a**5*b*a**2*b**-4*a >>> w.exponent_syllable( 2 ) 2 """ return self.array_form[i][1] def generator_syllable(self, i): """ Returns the symbol of the generator that is involved in the i-th syllable of the associative word `self`. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a b") >>> w = a**5*b*a**2*b**-4*a >>> w.generator_syllable( 3 ) b """ return self.array_form[i][0] def sub_syllables(self, from_i, to_j): """ `sub_syllables` returns the subword of the associative word `self` that consists of syllables from positions `from_to` to `to_j`, where `from_to` and `to_j` must be positive integers and indexing is done with origin 0. Examples ======== >>> from sympy.combinatorics import free_group >>> f, a, b = free_group("a, b") >>> w = a**5*b*a**2*b**-4*a >>> w.sub_syllables(1, 2) b >>> w.sub_syllables(3, 3) """ if not isinstance(from_i, int) or not isinstance(to_j, int): raise ValueError("both arguments should be integers") group = self.group if to_j <= from_i: return group.identity else: r = tuple(self.array_form[from_i: to_j]) return group.dtype(r) def substituted_word(self, from_i, to_j, by): """ Returns the associative word obtained by replacing the subword of `self` that begins at position `from_i` and ends at position `to_j - 1` by the associative word `by`. `from_i` and `to_j` must be positive integers, indexing is done with origin 0. In other words, `w.substituted_word(w, from_i, to_j, by)` is the product of the three words: `w.subword(0, from_i)`, `by`, and `w.subword(to_j len(w))`. See Also ======== eliminate_word """ lw = len(self) if from_i >= to_j or from_i > lw or to_j > lw: raise ValueError("values should be within bounds") # otherwise there are four possibilities # first if from=1 and to=lw then if from_i == 0 and to_j == lw: return by elif from_i == 0: # second if from_i=1 (and to_j < lw) then return by*self.subword(to_j, lw) elif to_j == lw: # third if to_j=1 (and from_i > 1) then return self.subword(0, from_i)*by else: # finally return self.subword(0, from_i)*by*self.subword(to_j, lw) def is_cyclically_reduced(self): r"""Returns whether the word is cyclically reduced or not. A word is cyclically reduced if by forming the cycle of the word, the word is not reduced, i.e a word w = `a_1 ... a_n` is called cyclically reduced if `a_1 \ne a_n^{-1}`. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> (x**2*y**-1*x**-1).is_cyclically_reduced() False >>> (y*x**2*y**2).is_cyclically_reduced() True """ if not self: return True return self[0] != self[-1]**-1 def identity_cyclic_reduction(self): """Return a unique cyclically reduced version of the word. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> (x**2*y**2*x**-1).identity_cyclic_reduction() x*y**2 >>> (x**-3*y**-1*x**5).identity_cyclic_reduction() x**2*y**-1 References ========== .. [1] https://planetmath.org/cyclicallyreduced """ word = self.copy() group = self.group while not word.is_cyclically_reduced(): exp1 = word.exponent_syllable(0) exp2 = word.exponent_syllable(-1) r = exp1 + exp2 if r == 0: rep = word.array_form[1: word.number_syllables() - 1] else: rep = ((word.generator_syllable(0), exp1 + exp2),) + \ word.array_form[1: word.number_syllables() - 1] word = group.dtype(rep) return word def cyclic_reduction(self, removed=False): """Return a cyclically reduced version of the word. Unlike `identity_cyclic_reduction`, this will not cyclically permute the reduced word - just remove the "unreduced" bits on either side of it. Compare the examples with those of `identity_cyclic_reduction`. When `removed` is `True`, return a tuple `(word, r)` where self `r` is such that before the reduction the word was either `r*word*r**-1`. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> (x**2*y**2*x**-1).cyclic_reduction() x*y**2 >>> (x**-3*y**-1*x**5).cyclic_reduction() y**-1*x**2 >>> (x**-3*y**-1*x**5).cyclic_reduction(removed=True) (y**-1*x**2, x**-3) """ word = self.copy() g = self.group.identity while not word.is_cyclically_reduced(): exp1 = abs(word.exponent_syllable(0)) exp2 = abs(word.exponent_syllable(-1)) exp = min(exp1, exp2) start = word[0]**abs(exp) end = word[-1]**abs(exp) word = start**-1*word*end**-1 g = g*start if removed: return word, g return word def power_of(self, other): ''' Check if `self == other**n` for some integer n. Examples ======== >>> from sympy.combinatorics import free_group >>> F, x, y = free_group("x, y") >>> ((x*y)**2).power_of(x*y) True >>> (x**-3*y**-2*x**3).power_of(x**-3*y*x**3) True ''' if self.is_identity: return True l = len(other) if l == 1: # self has to be a power of one generator gens = self.contains_generators() s = other in gens or other**-1 in gens return len(gens) == 1 and s # if self is not cyclically reduced and it is a power of other, # other isn't cyclically reduced and the parts removed during # their reduction must be equal reduced, r1 = self.cyclic_reduction(removed=True) if not r1.is_identity: other, r2 = other.cyclic_reduction(removed=True) if r1 == r2: return reduced.power_of(other) return False if len(self) < l or len(self) % l: return False prefix = self.subword(0, l) if prefix == other or prefix**-1 == other: rest = self.subword(l, len(self)) return rest.power_of(other) return False def letter_form_to_array_form(array_form, group): """ This method converts a list given with possible repetitions of elements in it. It returns a new list such that repetitions of consecutive elements is removed and replace with a tuple element of size two such that the first index contains `value` and the second index contains the number of consecutive repetitions of `value`. """ a = list(array_form[:]) new_array = [] n = 1 symbols = group.symbols for i in range(len(a)): if i == len(a) - 1: if a[i] == a[i - 1]: if (-a[i]) in symbols: new_array.append((-a[i], -n)) else: new_array.append((a[i], n)) else: if (-a[i]) in symbols: new_array.append((-a[i], -1)) else: new_array.append((a[i], 1)) return new_array elif a[i] == a[i + 1]: n += 1 else: if (-a[i]) in symbols: new_array.append((-a[i], -n)) else: new_array.append((a[i], n)) n = 1 def zero_mul_simp(l, index): """Used to combine two reduced words.""" while index >=0 and index < len(l) - 1 and l[index][0] == l[index + 1][0]: exp = l[index][1] + l[index + 1][1] base = l[index][0] l[index] = (base, exp) del l[index + 1] if l[index][1] == 0: del l[index] index -= 1