def __init__(self, free_group, relations): """ The Python constructor TESTS:: sage: G = FreeGroup('a, b') sage: H = G / (G([1]), G([2])^3) sage: H Finitely presented group < a, b | a, b^3 > sage: F = FreeGroup('a, b') sage: J = F / (F([1]), F([2, 2, 2])) sage: J is H True sage: TestSuite(H).run() sage: TestSuite(J).run() """ from sage.groups.free_group import is_FreeGroup assert is_FreeGroup(free_group) assert isinstance(relations, tuple) self._free_group = free_group self._relations = relations self._assign_names(free_group.variable_names()) parent_gap = free_group.gap() / libgap([ rel.gap() for rel in relations]) ParentLibGAP.__init__(self, parent_gap) Group.__init__(self)
def __init__(self, free_group, relations): """ The Python constructor. TESTS:: sage: G = FreeGroup('a, b') sage: H = G / (G([1]), G([2])^3) sage: H Finitely presented group < a, b | a, b^3 > sage: F = FreeGroup('a, b') sage: J = F / (F([1]), F([2, 2, 2])) sage: J is H True sage: TestSuite(H).run() sage: TestSuite(J).run() """ from sage.groups.free_group import is_FreeGroup assert is_FreeGroup(free_group) assert isinstance(relations, tuple) self._free_group = free_group self._relations = relations self._assign_names(free_group.variable_names()) parent_gap = free_group.gap() / libgap( [rel.gap() for rel in relations]) ParentLibGAP.__init__(self, parent_gap) Group.__init__(self)
def get_minword_wh_nbrs(F, word): """ Assuming word is (AutF)-minimial, find all words that can be obtained from word by exactly one Whitehead move. The assumption on minimality is not necessary, but we just find whitehead move that don't change the length. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert word in F, "word must be in F" letters = list(range(1, r + 1)) + list(range(-r, 0)) word_len = len(word.Tietze()) nbrs = set() for v in letters: for choices in product([(0, 0), (0, 1), (1, 0), (1, 1)], repeat=r - 1): phi = get_whitehead_move(F, v, choices) nbr = phi(word) if len(nbr.Tietze()) == word_len: nbrs.add(nbr) return list(nbrs)
def minimize_naive(F, word): """ Find a minimal automorphic representative for a word in F. Uses Whitehead minimization. This is a naive implementation, going over all possible moves instead of finding the most efficient one. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert word in F, "word must be in F" letters = list(range(1, r + 1)) + list(range(-r, 0)) curr_word = word curr_len = len(word.Tietze()) for v in letters: for choices in product([(0, 0), (0, 1), (1, 0), (1, 1)], repeat=r - 1): phi = get_whitehead_move(F, v, choices) new_word = phi(word) new_len = len(new_word.Tietze()) if new_len < curr_len: curr_word = new_word curr_len = new_len if curr_len < len(word.Tietze()): return minimize_naive(F, curr_word) else: return word
def _get_action_(self, S, op, self_on_left): """ Let the coercion system discover actions of the braid group on free groups. sage: B.<b0,b1,b2> = BraidGroup() sage: F.<f0,f1,f2,f3> = FreeGroup() sage: f1 * b1 f1*f2*f1^-1 sage: from sage.structure.all import get_coercion_model sage: cm = get_coercion_model() sage: cm.explain(f1, b1, operator.mul) Action discovered. Right action by Braid group on 4 strands on Free Group on generators {f0, f1, f2, f3} Result lives in Free Group on generators {f0, f1, f2, f3} Free Group on generators {f0, f1, f2, f3} sage: cm.explain(b1, f1, operator.mul) Will try _r_action and _l_action Unknown result parent. """ import operator if is_FreeGroup(S) and op == operator.mul and not self_on_left: return self.mapping_class_action(S) return None
def __init__(self, F, arg): """ If arg is a list: Generate the core graph corresponding to the group generated by group_gens. If arg is a graph: Check for validity and do all folds. """ assert is_FreeGroup(F), "F must be a free group" self.F = F self.F_rank = F.rank() self.letter_types = range(1, self.F_rank + 1) self.letters = list(range(-self.F_rank, 0)) + list(range(1, self.F_rank + 1)) # -r, ..., -1, 1, ..., r if isinstance(arg, list): group_gens = arg assert all([gen in F for gen in group_gens]), "The generators must be elements of F." self.group_gens = group_gens G = MultiDiGraph() G.add_node((0,)) # the marked vertex (id) for i, gen in enumerate(self.group_gens): word = gen.Tietze() word_len = len(word) # new_nodes = [(i, j) for j in range(1, word_len)] # G.add_nodes_from(new_nodes) get_node = lambda j: (0,) if (j % word_len == 0) else (i, j) for j in range(word_len): G.add_edge(get_node(j), get_node(j + 1), label=word[j]) G.add_edge(get_node(j + 1), get_node(j), label=-word[j]) elif isinstance(arg, MultiDiGraph): # We are going to copy the graph, add reverse edges when needed, # and sort the edges. # The reason we sort the edges is to get a "canonical" version # of the object, so subgroup_gens would be the same in different # objects with the same graph. G = MultiDiGraph() G.add_nodes_from(arg.nodes()) edges = arg.edges(data='label') G_edges = [e for e in edges] assert len(edges) == len(arg.edges()), "Every edge has to be labelled." for src, dst, letter in edges: assert letter in self.letters, \ f"The edge betwen {src} and {dst} has an invalid label" if (dst, src, -letter) not in G.edges(data='label'): G_edges.append((dst, src, -letter)) G.add_weighted_edges_from(sorted(G_edges), weight='label') else: raise ValueError("arg must be a list of words or a MultiDiGraph.") self.G = do_all_folds(G) # The subgraph of positive edges G_pos = MultiDiGraph() G_pos.add_edges_from([e for e in self.G.edges(data=True) if e[2]['label'] > 0]) self.G_pos = G_pos self.subgroup_gens = tuple(sorted(self.get_subgroup()))
def get_whitehead_move_of_cut(F, v, Y): """ Generate the whitehead move corresponding to v with and the cut Y. Recall that the whitehead move is defined by x -> v^(-1 if -x in Y else 0) * x * v^(1 if x in Y else 0) v -> v F - a free group. v - a letter of F. Y - a set containing v but not -v. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert v in range(1, r + 1) or v in range( -r, 0), "v must be a valid letter of F \ (a number in [-r,...,-1,1,...,r])" assert v in Y and -v not in Y, "The cut must seperate v and -v" gen_imgs = [] for x in range(1, r + 1): img = [] if -x in Y: img.append(-v) img.append(x) if x in Y: img.append(v) gen_imgs.append(img) gen_imgs[abs(v) - 1] = [abs(v)] return F.hom([F(img) for img in gen_imgs])
def get_whitehead_move(F, v, choices): """ Generate the whitehead move corresponding to v with the given choices of (s,t). Recall that the whitehead move is defined by v->v, x_i-> v^s * x * v^-t F - a free group. v - a letter of F. choices - a list of tuples of pair of binary numbers, of length rank(F) - 1. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert v in range(1, r + 1) or v in range( -r, 0), "v must be a valid letter of F \ (a number in [-r,...,-1,1,...,r])" assert len(choices) == r - 1, "choices must be of length rank(F) - 1" gen_imgs = [] for x in range(1, abs(v)): s, t = choices[x - 1] gen_imgs.append([c for c in [s * v, x, -t * v] if c]) gen_imgs.append([abs(v)]) for x in range(abs(v) + 1, r + 1): s, t = choices[x - 2] gen_imgs.append([c for c in [s * v, x, -t * v] if c]) return F.hom([F(img) for img in gen_imgs])
def minimize(F, word): """ Find the minimal form of a word. A minimal form is a representative of (Aut(F)w) of shortest length. This is a smart implementation - for each letter v, we find the best v-cuts. """ assert is_FreeGroup(F), "F must be a free group" assert word in F, "word must be in F" curr_word = word new_word = minimize_one_step(F, curr_word) while len(new_word.Tietze()) < len(curr_word.Tietze()): curr_word, new_word = new_word, minimize_one_step(F, new_word) return curr_word
def are_automorphic(F, w1, w2): assert is_FreeGroup(F), "F must be a free group" assert w1 in F and w2 in F, "Both words must be in F" w1 = canonical_letter_permute_form(F, minimize(F, w1)) w2 = canonical_letter_permute_form(F, minimize(F, w2)) if w1 == w2: return True if len(w1.Tietze()) != len(w2.Tietze()): return False G = Graph() G.add_nodes_from([w1, w2]) tested_words = set() while len(tested_words) < len(G.nodes()): edges_to_add = [] for word in G.nodes(): if word in tested_words: continue nbrs = [canonical_letter_permute_form(F, nbr) for nbr in get_minword_wh_nbrs(F, word)] edges_to_add += [(word, nbr) for nbr in nbrs] tested_words.add(word) G.add_edges_from(edges_to_add) return has_path(G, w1, w2)
def get_whitehead_graph(F, word): """ Construct the Whitehead graph of the word. This is the graph whose vertices are the letters of F, and for any (xy) appearing in the word there's an edge between x and y^-1. """ assert is_FreeGroup(F), "F must be a free group" assert word in F, "word must be in F" r = F.rank() G = Graph() G.add_nodes_from(list(range(1, r + 1)) + list(range(-r, 0))) word_repr = word.Tietze() edges = [(word_repr[i], -word_repr[i + 1]) for i in range(-1, len(word_repr) - 1)] for u, v in edges: if not G.has_edge(u, v): G.add_edge(u, v, capacity=1) else: G[u][v]['capacity'] += 1 return G
def minimize_one_step(F, word): """ Find the shortest word that can be obtained by applying a single Whitehead move to a word. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert word in F, "word must be in F" word = shortest_cyclic_shift(F, word) letters = list(range(1, r + 1)) + list(range(-r, 0)) G = get_whitehead_graph(F, word) best_change = 0 best_move = tuple() for v in letters: cap, cut = minimum_cut(G, v, -v) change = cap - sum([G[v][u]['capacity'] for u in G[v]]) if change < best_change: best_change = change best_move = (v, cut[0]) assert best_change <= 0, "Something is wrong" if best_change == 0: return word v, cut = best_move phi = get_whitehead_move_of_cut(F, v, cut) new_word = shortest_cyclic_shift(F, phi(word)) assert len(new_word.Tietze()) == len( word.Tietze()) + best_change, "Something is wrong" return new_word
def _get_action_(self, S, op, self_on_left): """ Let the coercion system discover actions of the braid group on free groups. sage: B.<b0,b1,b2> = BraidGroup() sage: F.<f0,f1,f2,f3> = FreeGroup() sage: f1 * b1 f1*f2*f1^-1 sage: from sage.structure.all import get_coercion_model sage: cm = get_coercion_model() sage: cm.explain(f1, b1, operator.mul) Action discovered. Right action by Braid group on 4 strands on Free Group on generators {f0, f1, f2, f3} Result lives in Free Group on generators {f0, f1, f2, f3} Free Group on generators {f0, f1, f2, f3} sage: cm.explain(b1, f1, operator.mul) Will try _r_action and _l_action Unknown result parent. """ import operator if is_FreeGroup(S) and op==operator.mul and not self_on_left: return self.mapping_class_action(S) return None
def canonical_letter_permute_form(F, word): """ Apply a permutation of the letters of F so that the different letters appearing in the word appear in order (1,2,3,...). """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() assert word in F, "word must be in F" word_rep = word.Tietze() letter_order = [] sgns = [] for letter in word_rep: if abs(letter) not in letter_order: letter_order.append(abs(letter)) sgns.append((1 if letter > 0 else -1)) gen_imgs = [i for i in range(1, r + 1)] for i, letter in enumerate(letter_order): gen_imgs[letter - 1] = (sgns[i] * (i + 1),) for i, v in enumerate([v for v in range(1, r + 1) if v not in letter_order]): gen_imgs[v - 1] = (len(letter_order) + i + 1,) phi = F.hom([F(img) for img in gen_imgs]) return phi(word)
def get_all_aut_classes(F, length, dirname="aut_classes_cache/", verbose=True): """ Get all automorphism classes of words in F_r with bounded length. Caches the result to dirname. """ assert is_FreeGroup(F), "F must be a free group" r = F.rank() cache_dir = os.fsencode(dirname) cache_file = os.fsencode(f"r{r}-len{length}.pkl") if not os.path.exists(cache_dir): os.mkdir(cache_dir) if os.path.exists(cache_dir + cache_file): aut_classes = pickle.load(open(cache_dir + cache_file, 'rb')) return [set([F(w.Tietze()) for w in cls]) for cls in aut_classes] # Maybe we computed something bigger before for file in os.listdir(cache_dir): filename = os.fsdecode(file) r_str, len_str = filename.split(".")[0].split("-") r_cached = int(r_str[1:]) len_cached = int(len_str[3:]) if r_cached >= r and len_cached >= length: cached_aut_classes = pickle.load(open(cache_dir + file, 'rb')) aut_classes = [] for cls in cached_aut_classes: word = cls.pop() word_rep = word.Tietze() if len(word_rep) == 0: aut_classes.append(set([F(1)])) elif len(word_rep) < length and max( set([abs(x) for x in word_rep])) < r: cls.add(word) aut_classes.append(set([F(w.Tietze()) for w in cls])) pickle.dump(aut_classes, open(f"aut_classes_cache/r{r}-len{length}.pkl", 'wb')) return aut_classes letters = list(range(1, r + 1)) + list(range(-r, 0)) minimal_words = set() all_words = set() # To avoid stuff like (1,-1,2,3) and (2,-2,2,3) # We only consider words that are in "canonical order" # We also assume we only check tuple starting with a # (They can still be a*a^-1*b*...) tuples = product(letters, repeat=length - 1) if verbose: print(f"Minimizing all words in {F} of length <={length}") print(f"{2*(len(letters)) ** (length - 1)} words to minimize.") tuples = tqdm(tuples) for tup in tuples: tup = [1] + list(tup) word = F(tup) if word not in all_words and word == canonical_letter_permute_form( F, word): all_words.add(word) minimal_words.add( canonical_letter_permute_form(F, minimize(F, word))) if length > 0: # Due to cancellations (e.g. (1,-1, ...)), we only # need to check words of length N, N - 1. tuples = product(letters, repeat=length - 1) if verbose: tuples = tqdm(tuples) for tup in tuples: word = F(tup) if word == canonical_letter_permute_form( F, word): # We only consider canonized orders minimal_words.add( canonical_letter_permute_form(F, minimize(F, word))) if verbose: print( f"Finished minimizing letters, found {len(minimal_words)} minimal words." ) print("Creating the Whitehead moves graph on minimal words.") G = Graph() G.add_nodes_from(minimal_words) for word in minimal_words: nbrs = get_minword_wh_nbrs(F, word) assert (len(nbrs[0].Tietze()) == len( word.Tietze())), "Something's wrong" for nbr in nbrs: nbr = canonical_letter_permute_form(F, nbr) assert nbr in minimal_words, f"Found a word ({nbr}) not in minimal_words" G.add_edge(word, nbr) aut_classes = list(connected_components(G)) pickle.dump(aut_classes, open(f"aut_classes_cache/r{r}-len{length}.pkl", 'wb')) return aut_classes