|
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 |
|
<free group on the generators (x, y, z)> |
|
>>> x**2*y**-1 |
|
x**2*y**-1 |
|
>>> type(_) |
|
<class 'sympy.combinatorics.free_groups.FreeGroupElement'> |
|
|
|
""" |
|
_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 |
|
<free group on the generators (x, y, z)> |
|
>>> y**2*x**-2*z**-1 |
|
y**2*x**-2*z**-1 |
|
>>> type(_) |
|
<class 'sympy.combinatorics.free_groups.FreeGroupElement'> |
|
|
|
""" |
|
_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") |
|
<free group on the generators (x, y, z)> |
|
>>> x**2*y**-2*z # noqa: F821 |
|
x**2*y**-2*z |
|
>>> type(_) |
|
<class 'sympy.combinatorics.free_groups.FreeGroupElement'> |
|
|
|
""" |
|
_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_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 |
|
|
|
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 __getnewargs__(self): |
|
"""Return a tuple of arguments that must be passed to __new__ in order to support pickling this object.""" |
|
return (self.symbols,) |
|
|
|
def __getstate__(self): |
|
|
|
return None |
|
|
|
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 = "<free group with %s generators>" % self.rank |
|
else: |
|
str_form = "<free group on the generators " |
|
gens = self.generators |
|
str_form += str(gens) + ">" |
|
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 |
|
{<identity>} |
|
|
|
""" |
|
if self.rank == 0: |
|
|
|
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} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
|
""" |
|
__slots__ = () |
|
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): |
|
return not self.array_form |
|
|
|
@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 "<identity>" |
|
|
|
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) |
|
result = self.group.identity |
|
if n == 0: |
|
return result |
|
if n < 0: |
|
n = -n |
|
x = self.inverse() |
|
else: |
|
x = self |
|
while True: |
|
if n % 2: |
|
result *= x |
|
n >>= 1 |
|
if not n: |
|
break |
|
x *= x |
|
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 |
|
<identity> |
|
|
|
""" |
|
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 |
|
<free group on the generators (swapnil0, swapnil1)> |
|
>>> g, swap0, swap1 = free_group("swap0 swap1") |
|
>>> g |
|
<free group on the generators (swap0, swap1)> |
|
|
|
>>> 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) |
|
|
|
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 = {group.dtype(((syllable[0], 1),)) for syllable in self.array_form} |
|
return 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) |
|
<identity> |
|
|
|
""" |
|
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") |
|
|
|
|
|
|
|
|
|
if from_i == 0 and to_j == lw: |
|
return by |
|
elif from_i == 0: |
|
return by*self.subword(to_j, lw) |
|
elif to_j == lw: |
|
return self.subword(0, from_i)*by |
|
else: |
|
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: |
|
|
|
gens = self.contains_generators() |
|
s = other in gens or other**-1 in gens |
|
return len(gens) == 1 and s |
|
|
|
|
|
|
|
|
|
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 |
|
|