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.
@@ -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)
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.