free_groups_26 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- free_groups_26/__init__.py +44 -0
- free_groups_26/aut_orbit_bruteforce.py +16 -0
- free_groups_26/free_group.py +49 -0
- free_groups_26/letter.py +146 -0
- free_groups_26/log.py +44 -0
- free_groups_26/minimize_bruteforce.py +88 -0
- free_groups_26/morphism.py +58 -0
- free_groups_26/whitehead_automorphism.py +92 -0
- free_groups_26/whitehead_graph.py +56 -0
- free_groups_26/whitehead_minimization.py +120 -0
- free_groups_26/word/__init__.py +21 -0
- free_groups_26/word/defn.py +163 -0
- free_groups_26/word/functions.py +84 -0
- free_groups_26-1.2.0.dist-info/METADATA +31 -0
- free_groups_26-1.2.0.dist-info/RECORD +17 -0
- free_groups_26-1.2.0.dist-info/WHEEL +4 -0
- free_groups_26-1.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
__version__ = "1.2.0"
|
|
2
|
+
|
|
3
|
+
from .letter import (
|
|
4
|
+
letter_from_str,
|
|
5
|
+
letter_from_str_b,
|
|
6
|
+
letter_from_str_a,
|
|
7
|
+
Letter,
|
|
8
|
+
Symbol,
|
|
9
|
+
Exponent,
|
|
10
|
+
)
|
|
11
|
+
from .word import (
|
|
12
|
+
word_from_str,
|
|
13
|
+
word_from_str_a,
|
|
14
|
+
word_from_str_b,
|
|
15
|
+
Word,
|
|
16
|
+
generate_random_word,
|
|
17
|
+
read,
|
|
18
|
+
)
|
|
19
|
+
from .free_group import FreeGroup, get_free_group
|
|
20
|
+
from .morphism import Morphism
|
|
21
|
+
from .whitehead_automorphism import (
|
|
22
|
+
generate_whitehead_automorphism_t2,
|
|
23
|
+
generate_all_t2_whitehead_automorphisms,
|
|
24
|
+
generate_t2_wh_aut_lazy,
|
|
25
|
+
)
|
|
26
|
+
from .minimize_bruteforce import (
|
|
27
|
+
minimize_once_bruteforce,
|
|
28
|
+
minimize_bruteforce,
|
|
29
|
+
minimize_once_bruteforce_lazy,
|
|
30
|
+
)
|
|
31
|
+
from .whitehead_graph import (
|
|
32
|
+
WhiteheadGraph,
|
|
33
|
+
generate_whg,
|
|
34
|
+
change_whg_edge_or_weight,
|
|
35
|
+
draw_graph,
|
|
36
|
+
)
|
|
37
|
+
from .whitehead_minimization import (
|
|
38
|
+
minimize_whitehead_once,
|
|
39
|
+
minimize_whitehead,
|
|
40
|
+
is_minimal,
|
|
41
|
+
type_1_minimize,
|
|
42
|
+
)
|
|
43
|
+
from .aut_orbit_bruteforce import find_minimal_automorphic_orbit
|
|
44
|
+
from .log import Log, LogItem, new_log, display_log
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from warnings import warn
|
|
2
|
+
from free_groups_26.morphism import Morphism
|
|
3
|
+
from free_groups_26.whitehead_automorphism import generate_t2_wh_aut_lazy
|
|
4
|
+
from free_groups_26.whitehead_minimization import is_minimal
|
|
5
|
+
from free_groups_26.word import Word
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_minimal_automorphic_orbit(word: Word) -> list[tuple[Morphism, Word]]:
|
|
9
|
+
aut_list: list[tuple[Morphism, Word]] = []
|
|
10
|
+
if not is_minimal(word):
|
|
11
|
+
warn("Finding automorphic orbit of non-minimal word.")
|
|
12
|
+
for phi in generate_t2_wh_aut_lazy(word.infer_free_group()):
|
|
13
|
+
new_word = phi(word)
|
|
14
|
+
if new_word.length == word.length:
|
|
15
|
+
aut_list.append((phi, new_word))
|
|
16
|
+
return aut_list
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from collections.abc import MutableSet
|
|
2
|
+
|
|
3
|
+
from .letter import Symbol, Letter
|
|
4
|
+
from sortedcontainers import SortedSet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FreeGroup:
|
|
8
|
+
|
|
9
|
+
basis: MutableSet[Symbol] #: Implemented using a SortedSet from sortedcontainers.
|
|
10
|
+
alphabet: MutableSet[Letter]
|
|
11
|
+
"""
|
|
12
|
+
Also implemented using a SortedSet. Represented by :math:`X^{\\pm}`.
|
|
13
|
+
|
|
14
|
+
Automatically inferred from :py:attr:`basis`. For :math:`x \\in X`, :math:`x, x^{-1} \\in X^{\\pm}`.
|
|
15
|
+
"""
|
|
16
|
+
rank: int #: Automatically inferred from :py:attr:`basis`.
|
|
17
|
+
|
|
18
|
+
def __init__(self, basis: MutableSet[Symbol]) -> None:
|
|
19
|
+
"""
|
|
20
|
+
This class represents a Free Group.
|
|
21
|
+
|
|
22
|
+
:param basis: The basis of the free group.
|
|
23
|
+
:type basis: set[Symbol]
|
|
24
|
+
"""
|
|
25
|
+
self.basis = SortedSet(basis)
|
|
26
|
+
self.rank = len(basis)
|
|
27
|
+
temp_alphabet: MutableSet[Letter] = SortedSet()
|
|
28
|
+
for sym in basis:
|
|
29
|
+
temp_alphabet.add(Letter(sym, 1))
|
|
30
|
+
temp_alphabet.add(Letter(sym, -1))
|
|
31
|
+
self.alphabet = temp_alphabet
|
|
32
|
+
|
|
33
|
+
def _get_hash_dict(self) -> dict[int, Letter]:
|
|
34
|
+
hash_dict: dict[int, Letter] = dict()
|
|
35
|
+
for elem in self.alphabet:
|
|
36
|
+
hash_dict[hash(elem)] = elem
|
|
37
|
+
return hash_dict
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_free_group(rank: int) -> FreeGroup:
|
|
41
|
+
"""
|
|
42
|
+
Generates a free group of the given rank. The basis of the free group will be the symbols from the English Alphabet from ``a`` (in unicode order).
|
|
43
|
+
|
|
44
|
+
:param int rank: The rank of the free group.
|
|
45
|
+
"""
|
|
46
|
+
basisSet: MutableSet[Symbol] = SortedSet()
|
|
47
|
+
for i in range(97, 97 + rank):
|
|
48
|
+
basisSet.add(chr(i))
|
|
49
|
+
return FreeGroup(basisSet)
|
free_groups_26/letter.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This submodule deals with Letters. Exposes the Letter class and a method to read letters, namely :py:func:`letter_from_str`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import override
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import total_ordering
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Letter",
|
|
13
|
+
"letter_from_str",
|
|
14
|
+
"letter_from_str_a",
|
|
15
|
+
"letter_from_str_b",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
type Symbol = str
|
|
19
|
+
type Exponent = int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@total_ordering
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Letter:
|
|
25
|
+
"""
|
|
26
|
+
This class represents a letter. Implements a total ordering, ``__eq__`` and ``__hash__``.
|
|
27
|
+
|
|
28
|
+
.. note::
|
|
29
|
+
|
|
30
|
+
Public access of both instance variables (:py:attr:`sym` and :py:attr:`exp`) is encouraged.
|
|
31
|
+
|
|
32
|
+
:param symbol: The symbol of the letter. All strings are valid symbols.
|
|
33
|
+
:type symbol: :py:type:`Symbol`
|
|
34
|
+
:param exponent: The exponent of the letter.
|
|
35
|
+
:type exponent: :py:type:`Exponent`
|
|
36
|
+
|
|
37
|
+
>>> Letter("a", 1)
|
|
38
|
+
a
|
|
39
|
+
>>> Letter("x", 3)
|
|
40
|
+
x^3
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
sym: Symbol #:
|
|
44
|
+
exp: Exponent #:
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
if self.exp == 1:
|
|
49
|
+
return self.sym
|
|
50
|
+
else:
|
|
51
|
+
return self.sym + "^" + str(self.exp)
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return abs(self.exp) * (self.sym if self.exp >= 0 else self.sym.upper())
|
|
56
|
+
|
|
57
|
+
def __lt__(self, other: object) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Checks if ``self < other``. The following ordering is followed:
|
|
60
|
+
|
|
61
|
+
.. math::
|
|
62
|
+
|
|
63
|
+
a < a^2 < \\ldots < a^{-2} < a^{-1} < b < \\ldots
|
|
64
|
+
|
|
65
|
+
:param object other: The object to compare against.
|
|
66
|
+
|
|
67
|
+
Returns ``NotImplemented`` if ``other`` is not a :py:type:`Letter`.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if not isinstance(other, Letter):
|
|
71
|
+
return NotImplemented
|
|
72
|
+
else:
|
|
73
|
+
if self.sym < other.sym:
|
|
74
|
+
return True
|
|
75
|
+
else:
|
|
76
|
+
if self.exp * other.exp > 0:
|
|
77
|
+
return self.exp < other.exp
|
|
78
|
+
else:
|
|
79
|
+
return self.exp > 0
|
|
80
|
+
|
|
81
|
+
def is_inverse(self) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Returns ``True`` if the exponent is less than 0.
|
|
84
|
+
"""
|
|
85
|
+
return self.exp < 0
|
|
86
|
+
|
|
87
|
+
def inv(self) -> Letter:
|
|
88
|
+
"""
|
|
89
|
+
Returns the inverse.
|
|
90
|
+
"""
|
|
91
|
+
return Letter(self.sym, -self.exp)
|
|
92
|
+
|
|
93
|
+
def get_base(self) -> Letter:
|
|
94
|
+
"""
|
|
95
|
+
Returns the base of the letter, which is defined as :math:`a^{\\frac{b}{|b|}}` for :math:`a^b`.
|
|
96
|
+
"""
|
|
97
|
+
return Letter(self.sym, (self.exp > 0) - (self.exp < 0))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def letter_from_str_b(raw: str) -> Letter:
|
|
101
|
+
"""
|
|
102
|
+
Do not use unless you're sure what you're doing! Use :py:func:`letter_from_str` instead. \n
|
|
103
|
+
Returns a letter from a string of the format ``{sym}^{exp}``.
|
|
104
|
+
|
|
105
|
+
>>> letter_from_str_b("b^32")
|
|
106
|
+
b³²
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
splits = raw.split("^")
|
|
110
|
+
if len(splits) == 1:
|
|
111
|
+
return Letter(splits[0], 1)
|
|
112
|
+
return Letter(splits[0], int(splits[1]))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def letter_from_str_a(char: str) -> Letter:
|
|
116
|
+
"""
|
|
117
|
+
Do not use unless you're sure what you're doing! Use :py:func:`letter_from_str` instead. \n
|
|
118
|
+
Another way to generate a letter from a string. This function works exclusively on the English Alphabet, where it considers uppercase letters the inverse of lowercase letters.
|
|
119
|
+
|
|
120
|
+
>>> letter_from_str_a("a")
|
|
121
|
+
a
|
|
122
|
+
>>> letter_from_str_a("A")
|
|
123
|
+
a^-1
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
if len(char) > 1:
|
|
127
|
+
raise ValueError("Expected single character. Found {char}")
|
|
128
|
+
return Letter(
|
|
129
|
+
char.lower(),
|
|
130
|
+
1 if char.islower() else -1,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def letter_from_str(char: str) -> Letter:
|
|
135
|
+
"""
|
|
136
|
+
Use of this function is always recommended. Intelligently chooses between the two types of conversion.
|
|
137
|
+
|
|
138
|
+
>>> letter_from_str("b^32")
|
|
139
|
+
b^32
|
|
140
|
+
>>> letter_from_str("a")
|
|
141
|
+
a
|
|
142
|
+
>>> letter_from_str("A")
|
|
143
|
+
a^-1
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
return letter_from_str_b(char) if "^" in char else letter_from_str_a(char)
|
free_groups_26/log.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from free_groups_26.morphism import Morphism
|
|
4
|
+
from free_groups_26.word import Word
|
|
5
|
+
|
|
6
|
+
type Log = list[LogItem]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LogItem:
|
|
10
|
+
"""
|
|
11
|
+
Initializing this class manually is never recommended. Use :py:func:`new_log` to generate a :py:type:`Log` to pass where it is required.
|
|
12
|
+
Manual access of the instance variables is encouraged, however.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
word: Word
|
|
16
|
+
"""
|
|
17
|
+
The current word in the log.
|
|
18
|
+
"""
|
|
19
|
+
morphism: Morphism
|
|
20
|
+
"""
|
|
21
|
+
The morphism that led to the current word.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, word: Word, morphism: Morphism) -> None:
|
|
25
|
+
self.word = word
|
|
26
|
+
self.morphism = morphism
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return str(self.word) + " : " + str(self.morphism.morphism_map)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def new_log() -> Log:
|
|
34
|
+
"""
|
|
35
|
+
Generates a new :py:type:`Log`. Recommended over using ``[]`` for readability.
|
|
36
|
+
"""
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def display_log(log: Log) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Prints out a :py:type:`Log` in a human readable fashion.
|
|
43
|
+
"""
|
|
44
|
+
print("\n".join([str(log_item) for log_item in log]))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from free_groups_26.log import Log, LogItem
|
|
4
|
+
from .morphism import Morphism
|
|
5
|
+
from .whitehead_automorphism import (
|
|
6
|
+
generate_all_t2_whitehead_automorphisms,
|
|
7
|
+
generate_t2_wh_aut_lazy,
|
|
8
|
+
)
|
|
9
|
+
from .word import Word
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def minimize_once_bruteforce(
|
|
13
|
+
word: Word, morphism_list: Iterable[Morphism], /, log: Log | None = None
|
|
14
|
+
) -> tuple[Word, bool]:
|
|
15
|
+
"""
|
|
16
|
+
Minimizes a word once, if possible.
|
|
17
|
+
|
|
18
|
+
:param word: The word to be minimized.
|
|
19
|
+
:param morphism_list: The list of all morphisms to try while minimizing the word.
|
|
20
|
+
:param log: (*keyword argument only*) Optional argument. The new word and the morphism used will be appended.
|
|
21
|
+
:return: Returns a tuple of (:py:class:`Word`, ``bool``). The second element specifies whether the word was reduced.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
has_log: bool = log is not None
|
|
25
|
+
current_length = word.length
|
|
26
|
+
for morphism in morphism_list:
|
|
27
|
+
new_word = morphism.map(word)
|
|
28
|
+
if new_word.length < current_length:
|
|
29
|
+
if has_log:
|
|
30
|
+
log.append(LogItem(new_word, morphism))
|
|
31
|
+
return (new_word, True)
|
|
32
|
+
return (word, False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def minimize_once_bruteforce_lazy(
|
|
36
|
+
word: Word,
|
|
37
|
+
morphism_list: Iterable[Morphism] | None = None,
|
|
38
|
+
log: Log | None = None,
|
|
39
|
+
) -> tuple[Word, bool]:
|
|
40
|
+
"""
|
|
41
|
+
Similar to :py:func:`minimize_once_bruteforce`, but lazy evaluated. This is almost always recommended.
|
|
42
|
+
|
|
43
|
+
:param morphism_list: Passing a Morphism List has no effect.
|
|
44
|
+
"""
|
|
45
|
+
has_log: bool = log is not None
|
|
46
|
+
for morphism in generate_t2_wh_aut_lazy(word.infer_free_group()):
|
|
47
|
+
new_word = morphism(word)
|
|
48
|
+
if new_word.length < word.length:
|
|
49
|
+
if has_log:
|
|
50
|
+
log.append(LogItem(new_word, morphism))
|
|
51
|
+
return (new_word, True)
|
|
52
|
+
return (word, False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def minimize_bruteforce(
|
|
56
|
+
word: Word,
|
|
57
|
+
lazy: bool = True,
|
|
58
|
+
morphism_list: Iterable[Morphism] | None = None,
|
|
59
|
+
/,
|
|
60
|
+
log: Log | None = None,
|
|
61
|
+
) -> Word:
|
|
62
|
+
"""
|
|
63
|
+
Returns the minimized form of the word, which might be the same word.
|
|
64
|
+
|
|
65
|
+
:param lazy: (Recommended) Enables lazy evaluation of the automorphisms. If this is on, passing a ``morphism_list`` has no effect.
|
|
66
|
+
:param morphism_list: If an iterable of morphisms is not provided, :py:func:`generate_all_t2_whitehead_automorphisms` is used to generate all possible Whitehead automorphisms for the given word. It is recommended that the list is generated and passed manually if working with multiple words from the same free group.
|
|
67
|
+
:param log: *(keyword argument only)* All mimization operations are appended to the log, if passed.
|
|
68
|
+
|
|
69
|
+
>>> log = new_log()
|
|
70
|
+
>>> minimize_bruteforce(rd("a b^2 c"), log=log)
|
|
71
|
+
c
|
|
72
|
+
>>> display_log(log)
|
|
73
|
+
abc : SortedDict({'a': ab⁻¹})
|
|
74
|
+
bc : SortedDict({'b': a⁻¹b})
|
|
75
|
+
c : SortedDict({'c': b⁻¹c})
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
if morphism_list is None and not lazy:
|
|
79
|
+
morphism_list = generate_all_t2_whitehead_automorphisms(word.infer_free_group())
|
|
80
|
+
new_word = word
|
|
81
|
+
minimize_function = (
|
|
82
|
+
minimize_once_bruteforce_lazy if lazy else minimize_once_bruteforce
|
|
83
|
+
)
|
|
84
|
+
while True:
|
|
85
|
+
new_word, reduced_in_cycle = minimize_function(new_word, morphism_list, log=log)
|
|
86
|
+
if not reduced_in_cycle:
|
|
87
|
+
break
|
|
88
|
+
return new_word
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import override, Any
|
|
2
|
+
from sortedcontainers import SortedDict
|
|
3
|
+
from .letter import Letter, Symbol
|
|
4
|
+
from .word import Word
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Morphism:
|
|
8
|
+
morphism_map: dict[Symbol, Word]
|
|
9
|
+
"""
|
|
10
|
+
Implemented using a SortedDict from sortedcontainers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, map: SortedDict | dict[Symbol, Word]):
|
|
14
|
+
"""
|
|
15
|
+
This class represents a morphism.
|
|
16
|
+
|
|
17
|
+
:param map: The mapping of the morphism.
|
|
18
|
+
:type map: sortedcontainers.SortedDict or dict[Symbol, Word]
|
|
19
|
+
|
|
20
|
+
If a python dictionary is passed, it is automatically converted into a SortedDict. The SortedDict must be a mapping of :py:type:`Symbol` s to :py:class:`Word` s.
|
|
21
|
+
"""
|
|
22
|
+
self.morphism_map = SortedDict(map)
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def __eq__(self, value: object, /) -> bool:
|
|
26
|
+
if not isinstance(value, Morphism):
|
|
27
|
+
return NotImplemented
|
|
28
|
+
else:
|
|
29
|
+
return self.morphism_map == value.morphism_map
|
|
30
|
+
|
|
31
|
+
def map(self, word: Word, reduce_cyclic: bool = False) -> Word:
|
|
32
|
+
"""
|
|
33
|
+
Applies the morphism to a given word. If a symbol in the word has a map in :py:attr:`morphism_map`, then the corresponding mapping takes place, otherwise, the symbol is mapped to itself.
|
|
34
|
+
|
|
35
|
+
>>> d = SortedDict({'a' : wfs("a b^-1")})
|
|
36
|
+
>>> phi = Morphism(d)
|
|
37
|
+
>>> w = wfs("a^2 b^3")
|
|
38
|
+
>>> phi.map(w)
|
|
39
|
+
ab⁻¹ab²
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
newWord: list[Letter] = []
|
|
43
|
+
for letter in word.word:
|
|
44
|
+
if letter.sym not in self.morphism_map:
|
|
45
|
+
newWord.append(letter)
|
|
46
|
+
else:
|
|
47
|
+
newWord.extend((self.morphism_map[letter.sym] ** letter.exp).word)
|
|
48
|
+
return Word(newWord, reduce_cyclic)
|
|
49
|
+
|
|
50
|
+
def __call__(self, word: Word, *args: Any, **kwds: Any) -> Word:
|
|
51
|
+
"""
|
|
52
|
+
Morphisms can be applied to words by directly calling them. (Aliased to :py:meth:`map`.)
|
|
53
|
+
|
|
54
|
+
>>> phi(w)
|
|
55
|
+
ab⁻¹ab²
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
return self.map(word)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from collections.abc import Iterable, Generator
|
|
2
|
+
from itertools import chain, combinations
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sortedcontainers import SortedDict, SortedSet
|
|
6
|
+
|
|
7
|
+
from .free_group import FreeGroup
|
|
8
|
+
from .letter import Letter, Symbol
|
|
9
|
+
from .morphism import Morphism
|
|
10
|
+
from .word import Word
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _powerset(iterable):
|
|
14
|
+
s = list(iterable)
|
|
15
|
+
return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_whitehead_automorphism_t2(x: Letter, A: Iterable[Letter]) -> Morphism:
|
|
19
|
+
"""
|
|
20
|
+
This function generates a Type 2 Whitehead Automorphism, using the following piecewise definition. (Credit: Virning, 1988)
|
|
21
|
+
|
|
22
|
+
.. math::
|
|
23
|
+
|
|
24
|
+
(A, x) y = \\begin{cases}
|
|
25
|
+
yx & \\text{if } y \\in A, \\bar{y} \\notin A, y \\notin \\{x, \\bar{x}\\} \\\\
|
|
26
|
+
\\bar{x} y & \\text{if } y \\notin A, \\bar{y} \\in A, y \\notin \\{x, \\bar{x}\\} \\\\
|
|
27
|
+
\\bar{x} yx & \\text{if } y, \\bar{y} \\in A \\\\
|
|
28
|
+
y & \\text{otherwise}
|
|
29
|
+
\\end{cases}
|
|
30
|
+
|
|
31
|
+
:param x: Corresponds to :math:`x` in the definition.
|
|
32
|
+
:param A: Corresponds to :math:`A` in the definition.
|
|
33
|
+
|
|
34
|
+
Instead of iterating through all the symbols in a free group, this function only goes through the symbols in ``A`` as the rest get mapped to themselves.
|
|
35
|
+
|
|
36
|
+
>>> wh = generate_whitehead_automorphism_t2(lfs("b"), {lfs("a"), lfs("a^-1"), lfs("b"), lfs("c^-1")})
|
|
37
|
+
>>> print(wh.morphism_map)
|
|
38
|
+
SortedDict({'a': b⁻¹ab, 'c': b⁻¹c})
|
|
39
|
+
>>> print(wh.map(wfsa("bac")))
|
|
40
|
+
ac
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
phi_map: dict[Symbol, Word] = SortedDict()
|
|
45
|
+
|
|
46
|
+
for ysym in SortedSet([k.sym for k in A]):
|
|
47
|
+
y = Letter(ysym, 1)
|
|
48
|
+
|
|
49
|
+
if y not in [x, x.inv()]:
|
|
50
|
+
if y in A and y.inv() in A:
|
|
51
|
+
phi_map[ysym] = Word((x.inv(), y, x))
|
|
52
|
+
elif y in A:
|
|
53
|
+
phi_map[ysym] = Word((y, x))
|
|
54
|
+
else:
|
|
55
|
+
phi_map[ysym] = Word((x.inv(), y))
|
|
56
|
+
|
|
57
|
+
return Morphism(phi_map)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_all_t2_whitehead_automorphisms(
|
|
61
|
+
inp: FreeGroup | set[Symbol],
|
|
62
|
+
) -> list[Morphism]:
|
|
63
|
+
"""
|
|
64
|
+
This function generates all Type 2 Whitehead Automorphisms for a given set of symbols. This function has a time and space complexity of :math:`O(4^n)`.
|
|
65
|
+
"""
|
|
66
|
+
L_n = inp.alphabet if isinstance(inp, FreeGroup) else FreeGroup(inp).alphabet
|
|
67
|
+
morphism_list: list[Morphism] = []
|
|
68
|
+
for A in _powerset(L_n):
|
|
69
|
+
for x in A:
|
|
70
|
+
if x.inv() in A:
|
|
71
|
+
continue
|
|
72
|
+
phi = generate_whitehead_automorphism_t2(x, A)
|
|
73
|
+
if phi.morphism_map != {}:
|
|
74
|
+
morphism_list.append(phi)
|
|
75
|
+
return morphism_list
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def generate_t2_wh_aut_lazy(
|
|
79
|
+
inp: FreeGroup | set[Symbol],
|
|
80
|
+
) -> Generator[Morphism, Any, None]:
|
|
81
|
+
"""
|
|
82
|
+
Lazily generates all the Type 2 Whitehead Automorphisms for the given set of symbols / :py:class:`FreeGroup`.
|
|
83
|
+
Similar to :py:func:`generate_all_t2_whitehead_automorphisms`, but recommended for more efficient memory usage.
|
|
84
|
+
"""
|
|
85
|
+
L_n = inp.alphabet if isinstance(inp, FreeGroup) else FreeGroup(inp).alphabet
|
|
86
|
+
for A in _powerset(L_n):
|
|
87
|
+
for x in A:
|
|
88
|
+
if x.inv() in A:
|
|
89
|
+
continue
|
|
90
|
+
phi = generate_whitehead_automorphism_t2(x, A)
|
|
91
|
+
if phi.morphism_map != {}:
|
|
92
|
+
yield phi
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
|
|
4
|
+
from networkx import Graph
|
|
5
|
+
from .letter import Letter
|
|
6
|
+
from .word import Word
|
|
7
|
+
|
|
8
|
+
type WhiteheadGraph = Graph[Letter]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_whg(word: Word) -> WhiteheadGraph:
|
|
12
|
+
"""
|
|
13
|
+
Generates a Whitehead Graph for the given word.
|
|
14
|
+
"""
|
|
15
|
+
whg: WhiteheadGraph = Graph()
|
|
16
|
+
cyclic = word.word + (word.word[0],)
|
|
17
|
+
|
|
18
|
+
for i in range(len(cyclic) - 1):
|
|
19
|
+
curr = cyclic[i].get_base()
|
|
20
|
+
next = cyclic[i + 1].get_base()
|
|
21
|
+
|
|
22
|
+
w_add = abs(cyclic[i].exp) - 1
|
|
23
|
+
if w_add != 0:
|
|
24
|
+
change_whg_edge_or_weight(whg, curr, curr.inv(), w_add)
|
|
25
|
+
change_whg_edge_or_weight(whg, curr, next.inv())
|
|
26
|
+
|
|
27
|
+
return whg
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def change_whg_edge_or_weight(
|
|
31
|
+
whg: WhiteheadGraph, v1: Letter, v2: Letter, weight: int = 1
|
|
32
|
+
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Changes the edge weights for an edge if it exists, otherwise adds the given edge to the graph.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
whg[v1][v2]["weight"] += weight
|
|
38
|
+
except KeyError:
|
|
39
|
+
_ = whg.add_edge(v1, v2, weight=weight)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def draw_graph(G: WhiteheadGraph) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Uses matplotlib to draw the given Whitehead Graph.
|
|
45
|
+
"""
|
|
46
|
+
pos = nx.spring_layout(G)
|
|
47
|
+
_ = nx.draw_networkx_nodes(G, pos, node_size=700)
|
|
48
|
+
_ = nx.draw_networkx_labels(G, pos, font_size=20)
|
|
49
|
+
_ = nx.draw_networkx_edges(G, pos, edgelist=G.edges, width=1)
|
|
50
|
+
edge_labels: dict[tuple[Letter, Letter], int] = nx.get_edge_attributes(G, "weight")
|
|
51
|
+
_ = nx.draw_networkx_edge_labels(G, pos, edge_labels)
|
|
52
|
+
ax = plt.gca()
|
|
53
|
+
_ = ax.margins(0.08)
|
|
54
|
+
plt.axis("off")
|
|
55
|
+
plt.tight_layout()
|
|
56
|
+
plt.show()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from networkx import minimum_cut, NetworkXError
|
|
2
|
+
from networkx.algorithms.flow import edmonds_karp
|
|
3
|
+
from sortedcontainers import SortedDict
|
|
4
|
+
|
|
5
|
+
from free_groups_26.log import Log, LogItem
|
|
6
|
+
|
|
7
|
+
from .free_group import FreeGroup, get_free_group
|
|
8
|
+
from .letter import Letter, Symbol
|
|
9
|
+
from .morphism import Morphism
|
|
10
|
+
from .word import Word
|
|
11
|
+
from .whitehead_automorphism import generate_whitehead_automorphism_t2
|
|
12
|
+
from .whitehead_graph import WhiteheadGraph, generate_whg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def minimize_whitehead_once(
|
|
16
|
+
word: Word, /, fg: FreeGroup | None = None, log: Log | None = None
|
|
17
|
+
) -> Word | None:
|
|
18
|
+
"""
|
|
19
|
+
Performs Whitehead Minimization once.
|
|
20
|
+
|
|
21
|
+
:param word: The word to be minimized.
|
|
22
|
+
:param fg: An optional free group can be passed if you wish to avoid the overhead of inferring the free group from the word. (Recommended for large words.)
|
|
23
|
+
:type fg: :py:class:`FreeGroup` or ``None``
|
|
24
|
+
:param log: An optional log. If passed, all morphisms tried will be appended as strings, with some other information.
|
|
25
|
+
:type log: :py:type:`Log` or ``None``.
|
|
26
|
+
:return: If minimal, returns ``None``, otherwise returns the once-minimized word.
|
|
27
|
+
|
|
28
|
+
**Current Implementation**: Iterate through the alphabet of the free group (inferred or given), if the max flow of the current alphabet to its inverse is less than its degree, try creating and applying a whitehead
|
|
29
|
+
automorphism with :py:func:`generate_whitehead_automorphism_t2`. If the length of the word is less than the given word (not guaranteed by a min cut), return it, otherwise continue iterating.
|
|
30
|
+
|
|
31
|
+
The min cut is computed using Edmonds Karp.
|
|
32
|
+
|
|
33
|
+
>>> log = new_log()
|
|
34
|
+
>>> minimize_whitehead_once(rd("a b^2 c"), log=log)
|
|
35
|
+
b²c
|
|
36
|
+
>>> display_log(log)
|
|
37
|
+
b²c : SortedDict({'b': a⁻¹ba, 'c': a⁻¹c})
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
graph: WhiteheadGraph = generate_whg(word)
|
|
41
|
+
if fg is None:
|
|
42
|
+
fg = word.infer_free_group()
|
|
43
|
+
|
|
44
|
+
for letter in fg.alphabet:
|
|
45
|
+
try:
|
|
46
|
+
value, partitions = minimum_cut(
|
|
47
|
+
graph, letter, letter.inv(), capacity="weight", flow_func=edmonds_karp
|
|
48
|
+
)
|
|
49
|
+
partitions: tuple[set[Letter], set[Letter]]
|
|
50
|
+
value: int
|
|
51
|
+
except NetworkXError:
|
|
52
|
+
continue
|
|
53
|
+
if value < graph.degree(letter, weight="weight"):
|
|
54
|
+
phi = generate_whitehead_automorphism_t2(letter, partitions[0])
|
|
55
|
+
new_word = phi.map(word, True)
|
|
56
|
+
if log is not None:
|
|
57
|
+
log.append(LogItem(new_word, phi))
|
|
58
|
+
if new_word.length < word.length:
|
|
59
|
+
return new_word
|
|
60
|
+
else:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def minimize_whitehead(
|
|
65
|
+
word: Word, /, fg: FreeGroup | None = None, log: Log | None = None
|
|
66
|
+
) -> Word:
|
|
67
|
+
"""
|
|
68
|
+
Whitehead Minimizes a word.
|
|
69
|
+
|
|
70
|
+
:param word: The word to be minimized.
|
|
71
|
+
:param fg: The optional free group. If not passed, it is inferred from the word.
|
|
72
|
+
:type fg: :py:class:`FreeGroup` or ``None``
|
|
73
|
+
:param log: All operations performed are appended to the log, if passed.
|
|
74
|
+
:type log: :py:type:`Log` or ``None``.
|
|
75
|
+
|
|
76
|
+
**Current Implementation**: Keep trying :py:func:`minimize_whitehead_once` on the word, reassigning the returns. If it returns a ``None``, then this function returns the minimized word.
|
|
77
|
+
|
|
78
|
+
>>> minimize_whitehead(wfs("c^-3 b^-1 a^2"))
|
|
79
|
+
c⁻¹
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
fg = fg if fg is not None else word.infer_free_group()
|
|
83
|
+
new_word = word
|
|
84
|
+
while True:
|
|
85
|
+
minimized = minimize_whitehead_once(new_word, fg=fg, log=log)
|
|
86
|
+
if minimized is None:
|
|
87
|
+
break
|
|
88
|
+
new_word = minimized
|
|
89
|
+
return new_word
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_minimal(word: Word, fg: FreeGroup | None = None) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
:return: Whether the word is already whitehead minimal.
|
|
95
|
+
"""
|
|
96
|
+
return minimize_whitehead(word, fg=fg) == word
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def type_1_minimize(word: Word) -> tuple[Word, Morphism]:
|
|
100
|
+
"""
|
|
101
|
+
Finds a Type 1 Whitehead Automorphism that makes the word lexicographically minimal.
|
|
102
|
+
|
|
103
|
+
>>> w = rd("k m^-1 y^-2 m y")
|
|
104
|
+
>>> nw, m = type_1_minimize(w)
|
|
105
|
+
>>> nw, m.morphism_map
|
|
106
|
+
(abc²b⁻¹c⁻¹, SortedDict({'k': a, 'm': b⁻¹, 'y': c⁻¹}))
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
canonical_free_group = get_free_group(word.infer_free_group().rank)
|
|
110
|
+
generator = iter(canonical_free_group.basis)
|
|
111
|
+
word_iter = iter(word)
|
|
112
|
+
phi_map: dict[Symbol, Word] = SortedDict()
|
|
113
|
+
while len(phi_map) < canonical_free_group.rank:
|
|
114
|
+
curr_letter = next(word_iter)
|
|
115
|
+
if curr_letter.sym not in phi_map:
|
|
116
|
+
phi_map[curr_letter.sym] = Word(
|
|
117
|
+
(Letter(next(generator), curr_letter.get_base().exp),)
|
|
118
|
+
)
|
|
119
|
+
phi = Morphism(phi_map)
|
|
120
|
+
return (phi(word), phi)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Word Class and functions on words.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .defn import Word
|
|
6
|
+
from .functions import (
|
|
7
|
+
generate_random_word,
|
|
8
|
+
word_from_str_a,
|
|
9
|
+
word_from_str_b,
|
|
10
|
+
word_from_str,
|
|
11
|
+
read,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Word",
|
|
16
|
+
"generate_random_word",
|
|
17
|
+
"word_from_str_a",
|
|
18
|
+
"word_from_str_b",
|
|
19
|
+
"word_from_str",
|
|
20
|
+
"read",
|
|
21
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from collections.abc import Iterable, MutableSet, Sequence
|
|
3
|
+
from symtable import Symbol
|
|
4
|
+
from typing import overload, override
|
|
5
|
+
|
|
6
|
+
from sortedcontainers import SortedSet
|
|
7
|
+
|
|
8
|
+
from free_groups_26.free_group import FreeGroup
|
|
9
|
+
from free_groups_26.letter import Exponent, Letter
|
|
10
|
+
|
|
11
|
+
__all__ = ["Word"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Word(Sequence[Letter]):
|
|
15
|
+
word: tuple[Letter, ...]
|
|
16
|
+
length: int
|
|
17
|
+
|
|
18
|
+
def __init__(self, word: Iterable[Letter], cyclically_reduce: bool = False) -> None:
|
|
19
|
+
"""
|
|
20
|
+
This class represents a word.
|
|
21
|
+
|
|
22
|
+
:param word: Any :py:type:`Iterable` containing :py:class:`Letter` s is accepted.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
m_word: list[Letter] = []
|
|
26
|
+
for letter in word:
|
|
27
|
+
reduce_word_helper(m_word, letter)
|
|
28
|
+
if cyclically_reduce:
|
|
29
|
+
reduce_cyclic(m_word)
|
|
30
|
+
self.word = tuple(m_word)
|
|
31
|
+
self.length = sum(abs(x.exp) for x in self.word)
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
def __len__(self) -> int:
|
|
35
|
+
return self.length
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def __getitem__(self, index: int) -> Letter: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def __getitem__(self, index: slice) -> Word: ...
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
def __getitem__(self, index: int | slice) -> Letter | Word:
|
|
45
|
+
if isinstance(index, int):
|
|
46
|
+
return self.word[index]
|
|
47
|
+
else:
|
|
48
|
+
return Word(self.word[index])
|
|
49
|
+
|
|
50
|
+
def __mul__(self, other: object) -> Word:
|
|
51
|
+
if isinstance(other, Word):
|
|
52
|
+
return Word(self.word + other.word)
|
|
53
|
+
elif isinstance(other, Letter):
|
|
54
|
+
return Word(self.word + (other,))
|
|
55
|
+
else:
|
|
56
|
+
return NotImplemented
|
|
57
|
+
|
|
58
|
+
def __rmul__(self, other: object) -> Word:
|
|
59
|
+
if isinstance(other, Word):
|
|
60
|
+
return other * self
|
|
61
|
+
elif isinstance(other, Letter):
|
|
62
|
+
return Word((other,) + self.word)
|
|
63
|
+
else:
|
|
64
|
+
return NotImplemented
|
|
65
|
+
|
|
66
|
+
def inv(self) -> Word:
|
|
67
|
+
return Word(
|
|
68
|
+
Letter(element.sym, -1 * element.exp) for element in self.word[::-1]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@override
|
|
72
|
+
def __eq__(self, value: object, /) -> bool:
|
|
73
|
+
return isinstance(value, Word) and self.word == value.word
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
def __hash__(self) -> int:
|
|
77
|
+
return hash(self.word)
|
|
78
|
+
|
|
79
|
+
def __pow__(self, exp: Exponent) -> Word:
|
|
80
|
+
"""
|
|
81
|
+
Words can be exponentiated.
|
|
82
|
+
|
|
83
|
+
>>> read("a^3 b^2 c^-3") ** 4
|
|
84
|
+
a³b²c⁻³a³b²c⁻³a³b²c⁻³a³b²c⁻³
|
|
85
|
+
>>> read("a^3 b^-2") ** -2
|
|
86
|
+
b²a⁻³b²a⁻³
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
if exp < 0:
|
|
90
|
+
return (self**-exp).inv()
|
|
91
|
+
elif exp == 0:
|
|
92
|
+
return Word(())
|
|
93
|
+
else:
|
|
94
|
+
newWord: list[Letter] = []
|
|
95
|
+
for _ in range(exp):
|
|
96
|
+
newWord.extend(self.word)
|
|
97
|
+
return Word(newWord)
|
|
98
|
+
|
|
99
|
+
def is_cyclically_reduced(self) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
:return: whether the word is cyclically reduced. Assumes that the given word is reduced linearly.
|
|
102
|
+
"""
|
|
103
|
+
if len(self.word) <= 1:
|
|
104
|
+
return True
|
|
105
|
+
return self.word[0].sym != self.word[-1].sym
|
|
106
|
+
|
|
107
|
+
def infer_free_group(self) -> FreeGroup:
|
|
108
|
+
"""
|
|
109
|
+
Infers the :py:class:`FreeGroup` ``self`` is an element of.
|
|
110
|
+
|
|
111
|
+
>>> wfs("a^2 b k^3 b^-2").infer_free_group().basis
|
|
112
|
+
SortedSet(['a', 'b', 'k'])
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
basis: MutableSet[Symbol] = SortedSet()
|
|
116
|
+
for letter in self.word:
|
|
117
|
+
if letter.sym not in basis:
|
|
118
|
+
basis.add(letter.sym) # pyright: ignore[reportUnknownMemberType]
|
|
119
|
+
return FreeGroup(basis)
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
def __repr__(self) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Words are represented with unicode characters by default.
|
|
125
|
+
"""
|
|
126
|
+
if self.length == 0:
|
|
127
|
+
return "ε"
|
|
128
|
+
return " ".join([repr(elem) for elem in self.word])
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Words are represented with unicode characters by default.
|
|
134
|
+
"""
|
|
135
|
+
if self.length == 0:
|
|
136
|
+
return "ε"
|
|
137
|
+
return "".join([str(elem) for elem in self.word])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def reduce_word_helper(stack: list[Letter], letter: Letter) -> None:
|
|
141
|
+
if letter.exp == 0:
|
|
142
|
+
return
|
|
143
|
+
elif len(stack) == 0:
|
|
144
|
+
stack.append(letter)
|
|
145
|
+
elif stack[-1].sym == letter.sym:
|
|
146
|
+
reduce_word_helper(stack, Letter(letter.sym, stack.pop().exp + letter.exp))
|
|
147
|
+
else:
|
|
148
|
+
stack.append(letter)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def reduce_cyclic(stack: list[Letter]) -> None:
|
|
152
|
+
while len(stack) > 1 and stack[0].sym == stack[-1].sym:
|
|
153
|
+
reduce_word_helper(stack, stack.pop(0))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ReducedWord(Word):
|
|
157
|
+
def __init__(self, word: Iterable[Letter], cyclic: bool = False) -> None:
|
|
158
|
+
m_word: list[Letter] = []
|
|
159
|
+
for letter in self.word:
|
|
160
|
+
reduce_word_helper(m_word, letter)
|
|
161
|
+
if cyclic:
|
|
162
|
+
reduce_cyclic(m_word)
|
|
163
|
+
super().__init__(m_word)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from free_groups_26.free_group import FreeGroup
|
|
2
|
+
from free_groups_26.letter import Letter, Symbol, letter_from_str_a, letter_from_str_b
|
|
3
|
+
from .defn import Word, reduce_cyclic
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
__all__ = ["generate_random_word"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_random_word(group: FreeGroup, length: int, variation: int) -> Word:
|
|
10
|
+
"""
|
|
11
|
+
Generates a cyclically reduced pseudo-random word from the given free group.
|
|
12
|
+
|
|
13
|
+
:param group: The free group of the generated word.
|
|
14
|
+
:type group: :py:class:`FreeGroup`
|
|
15
|
+
:param int length: The length of the generated word will be roughly around ``length`` :math:`\\pm` ``variation``.
|
|
16
|
+
:param int variation: The absolute value of the highest power of a letter in the free group. For example, if ``variation = 4``, the exponents of
|
|
17
|
+
the letters in the word are guaranteed to be between 4 and -4.
|
|
18
|
+
"""
|
|
19
|
+
newWord: list[Letter] = []
|
|
20
|
+
prevSym: Symbol = ""
|
|
21
|
+
count = 0
|
|
22
|
+
while count <= length:
|
|
23
|
+
sym: Symbol = tuple(group.basis)[random.randint(0, group.rank - 1)]
|
|
24
|
+
if sym == prevSym:
|
|
25
|
+
continue
|
|
26
|
+
prevSym = sym
|
|
27
|
+
expo = 0
|
|
28
|
+
while expo == 0:
|
|
29
|
+
expo = random.randint(-variation, variation)
|
|
30
|
+
newWord.append(Letter(sym, expo))
|
|
31
|
+
count += abs(expo)
|
|
32
|
+
reduce_cyclic(newWord)
|
|
33
|
+
return Word(newWord)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def word_from_str_b(raw: str) -> Word:
|
|
37
|
+
"""
|
|
38
|
+
Generates a word from a string of the format ``"{sym}^{exp} {letter} ...``. This is consistent with :py:func:`letter_from_str_b`.
|
|
39
|
+
Use of :py:func:`read` is recommended.
|
|
40
|
+
|
|
41
|
+
:param str raw: The string to be parsed.
|
|
42
|
+
|
|
43
|
+
>>> word_from_str("a^2 b^3")
|
|
44
|
+
a^2 b^3
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
return Word(letter_from_str_b(i) for i in raw.split(" "))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def word_from_str_a(raw: str) -> Word:
|
|
51
|
+
"""
|
|
52
|
+
Another way to generate a :py:type:`word` from a string, when your letters are exclusively from the English alphabet.
|
|
53
|
+
Uppercase letters are considered the inverses of lowercase letters. See below for examples.
|
|
54
|
+
Use of :py:func:`read` is recommended.
|
|
55
|
+
|
|
56
|
+
See :py:func:`letter_from_str_alphabet`.
|
|
57
|
+
|
|
58
|
+
:param str raw: The string to be parsed.
|
|
59
|
+
:return: The returned word is reduced by default.
|
|
60
|
+
|
|
61
|
+
>>> str(word_from_str_alphabet("aaaAbBCCccccc"))
|
|
62
|
+
aaaAbBCCccccc
|
|
63
|
+
>>> str(word_from_str_alphabet("AAAABBBBHHHHMMMkk"))
|
|
64
|
+
AAAABBBBHHHHMMMkk
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
return Word(map(letter_from_str_a, raw))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def word_from_str(inp: str) -> Word:
|
|
71
|
+
"""
|
|
72
|
+
Parses a word from a string. Guesses the correct function to use from :py:func:`word_from_str_a` and :py:func:`word_from_str_b` based on whether there is a
|
|
73
|
+
caret ``^`` in the input. Recommended in most cases.
|
|
74
|
+
"""
|
|
75
|
+
if "^" in inp:
|
|
76
|
+
return word_from_str_b(inp)
|
|
77
|
+
else:
|
|
78
|
+
return word_from_str_a(inp)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
read = word_from_str
|
|
82
|
+
"""
|
|
83
|
+
Alias to :py:func:`word_from_str`. Recommended when brevity is desired.
|
|
84
|
+
"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: free_groups_26
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A Python Module for working with Free Groups
|
|
5
|
+
Project-URL: Documentation, https://www.sudhirkrisna.com/free_groups_26/
|
|
6
|
+
Project-URL: Repository, https://github.com/Sudhboi/free_groups_26
|
|
7
|
+
Project-URL: Issues, https://github.com/Sudhboi/free_groups_26/issues
|
|
8
|
+
Author-email: Sudhir Krisna <sudhcontent@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Requires-Dist: matplotlib>=3.10.8
|
|
15
|
+
Requires-Dist: networkx>=3.6.1
|
|
16
|
+
Requires-Dist: sortedcontainers>=2.4.0
|
|
17
|
+
Provides-Extra: docs
|
|
18
|
+
Requires-Dist: sphinx; extra == 'docs'
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# free_groups_26
|
|
25
|
+
|
|
26
|
+
### Install:
|
|
27
|
+
```
|
|
28
|
+
pip install free-groups-26
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Documentation can be found at the official website - [free_groups_26](https://www.sudhirkrisna.com/free_groups_26/index.html)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
free_groups_26/__init__.py,sha256=RZxnzzIuLS-86qCAsjyTyhgzpruJApxrP29Td6FTH8A,1008
|
|
2
|
+
free_groups_26/aut_orbit_bruteforce.py,sha256=vaBjMZClOgAEIjqT1su0iQN-um4mg1ZOwYkJedkRbmE,665
|
|
3
|
+
free_groups_26/free_group.py,sha256=l1RzAwPSxGWgYyxmQVHcy3-AwyNZVYeIWaWMNifW1n0,1612
|
|
4
|
+
free_groups_26/letter.py,sha256=PRPLmyI-9WSzNgu3GPJDxR6kMDSd-c8ZILjhnfFlHWE,3845
|
|
5
|
+
free_groups_26/log.py,sha256=oVphsZvyZCRrfmPzQ843dq81evxScKA7NXjU0ydZSYM,1080
|
|
6
|
+
free_groups_26/minimize_bruteforce.py,sha256=xC1TxmwcP4ClVnolt13HkrWffVHi5XEjC7lcNjAkDxk,3272
|
|
7
|
+
free_groups_26/morphism.py,sha256=RYF4QhW2UTeX8ZbfUkYl3F37UugDW073eJNMzt9oKfM,1947
|
|
8
|
+
free_groups_26/whitehead_automorphism.py,sha256=LV01dMiHpFMJhTolE8ychXUsjhUp9iaG7YxQnhKZVO0,3254
|
|
9
|
+
free_groups_26/whitehead_graph.py,sha256=F91De94GMDXGLB_yozFgLWcg8ZtedZoARVu2XmC94gY,1590
|
|
10
|
+
free_groups_26/whitehead_minimization.py,sha256=cAph5nU8cYLBMUWitWCj96oAQ6U52H0reKLEleQ1Ujk,4568
|
|
11
|
+
free_groups_26/word/__init__.py,sha256=qPI4Oyjd7fg9yw8eh99IRfYqtjnS4BUtXIuwzrOWP8I,349
|
|
12
|
+
free_groups_26/word/defn.py,sha256=-FDaL9VTkrTR7dFsq2SlMT8aw0dWElhhWIXs8irozIU,4781
|
|
13
|
+
free_groups_26/word/functions.py,sha256=r7lVTMLcQBXJBwQM6OROXlqQfPRT5N7gN9JWiE358N4,2844
|
|
14
|
+
free_groups_26-1.2.0.dist-info/METADATA,sha256=nWTQw4YTPbnwqwJP4SpDCJHW1jwYRREq3utBERci8lc,1040
|
|
15
|
+
free_groups_26-1.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
free_groups_26-1.2.0.dist-info/licenses/LICENSE,sha256=zdv9z3A6yiSjuvbDlFwNK8AnGLTpNx2Rb5h64f6Lubo,1070
|
|
17
|
+
free_groups_26-1.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sudhir Krisna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|