Spaces:
Sleeping
Sleeping
| from sympy.combinatorics.free_groups import free_group | |
| from sympy.printing.defaults import DefaultPrinting | |
| from itertools import chain, product | |
| from bisect import bisect_left | |
| ############################################################################### | |
| # COSET TABLE # | |
| ############################################################################### | |
| class CosetTable(DefaultPrinting): | |
| # coset_table: Mathematically a coset table | |
| # represented using a list of lists | |
| # alpha: Mathematically a coset (precisely, a live coset) | |
| # represented by an integer between i with 1 <= i <= n | |
| # alpha in c | |
| # x: Mathematically an element of "A" (set of generators and | |
| # their inverses), represented using "FpGroupElement" | |
| # fp_grp: Finitely Presented Group with < X|R > as presentation. | |
| # H: subgroup of fp_grp. | |
| # NOTE: We start with H as being only a list of words in generators | |
| # of "fp_grp". Since `.subgroup` method has not been implemented. | |
| r""" | |
| Properties | |
| ========== | |
| [1] `0 \in \Omega` and `\tau(1) = \epsilon` | |
| [2] `\alpha^x = \beta \Leftrightarrow \beta^{x^{-1}} = \alpha` | |
| [3] If `\alpha^x = \beta`, then `H \tau(\alpha)x = H \tau(\beta)` | |
| [4] `\forall \alpha \in \Omega, 1^{\tau(\alpha)} = \alpha` | |
| References | |
| ========== | |
| .. [1] Holt, D., Eick, B., O'Brien, E. | |
| "Handbook of Computational Group Theory" | |
| .. [2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson | |
| Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490. | |
| "Implementation and Analysis of the Todd-Coxeter Algorithm" | |
| """ | |
| # default limit for the number of cosets allowed in a | |
| # coset enumeration. | |
| coset_table_max_limit = 4096000 | |
| # limit for the current instance | |
| coset_table_limit = None | |
| # maximum size of deduction stack above or equal to | |
| # which it is emptied | |
| max_stack_size = 100 | |
| def __init__(self, fp_grp, subgroup, max_cosets=None): | |
| if not max_cosets: | |
| max_cosets = CosetTable.coset_table_max_limit | |
| self.fp_group = fp_grp | |
| self.subgroup = subgroup | |
| self.coset_table_limit = max_cosets | |
| # "p" is setup independent of Omega and n | |
| self.p = [0] | |
| # a list of the form `[gen_1, gen_1^{-1}, ... , gen_k, gen_k^{-1}]` | |
| self.A = list(chain.from_iterable((gen, gen**-1) \ | |
| for gen in self.fp_group.generators)) | |
| #P[alpha, x] Only defined when alpha^x is defined. | |
| self.P = [[None]*len(self.A)] | |
| # the mathematical coset table which is a list of lists | |
| self.table = [[None]*len(self.A)] | |
| self.A_dict = {x: self.A.index(x) for x in self.A} | |
| self.A_dict_inv = {} | |
| for x, index in self.A_dict.items(): | |
| if index % 2 == 0: | |
| self.A_dict_inv[x] = self.A_dict[x] + 1 | |
| else: | |
| self.A_dict_inv[x] = self.A_dict[x] - 1 | |
| # used in the coset-table based method of coset enumeration. Each of | |
| # the element is called a "deduction" which is the form (alpha, x) whenever | |
| # a value is assigned to alpha^x during a definition or "deduction process" | |
| self.deduction_stack = [] | |
| # Attributes for modified methods. | |
| H = self.subgroup | |
| self._grp = free_group(', ' .join(["a_%d" % i for i in range(len(H))]))[0] | |
| self.P = [[None]*len(self.A)] | |
| self.p_p = {} | |
| def omega(self): | |
| """Set of live cosets. """ | |
| return [coset for coset in range(len(self.p)) if self.p[coset] == coset] | |
| def copy(self): | |
| """ | |
| Return a shallow copy of Coset Table instance ``self``. | |
| """ | |
| self_copy = self.__class__(self.fp_group, self.subgroup) | |
| self_copy.table = [list(perm_rep) for perm_rep in self.table] | |
| self_copy.p = list(self.p) | |
| self_copy.deduction_stack = list(self.deduction_stack) | |
| return self_copy | |
| def __str__(self): | |
| return "Coset Table on %s with %s as subgroup generators" \ | |
| % (self.fp_group, self.subgroup) | |
| __repr__ = __str__ | |
| def n(self): | |
| """The number `n` represents the length of the sublist containing the | |
| live cosets. | |
| """ | |
| if not self.table: | |
| return 0 | |
| return max(self.omega) + 1 | |
| # Pg. 152 [1] | |
| def is_complete(self): | |
| r""" | |
| The coset table is called complete if it has no undefined entries | |
| on the live cosets; that is, `\alpha^x` is defined for all | |
| `\alpha \in \Omega` and `x \in A`. | |
| """ | |
| return not any(None in self.table[coset] for coset in self.omega) | |
| # Pg. 153 [1] | |
| def define(self, alpha, x, modified=False): | |
| r""" | |
| This routine is used in the relator-based strategy of Todd-Coxeter | |
| algorithm if some `\alpha^x` is undefined. We check whether there is | |
| space available for defining a new coset. If there is enough space | |
| then we remedy this by adjoining a new coset `\beta` to `\Omega` | |
| (i.e to set of live cosets) and put that equal to `\alpha^x`, then | |
| make an assignment satisfying Property[1]. If there is not enough space | |
| then we halt the Coset Table creation. The maximum amount of space that | |
| can be used by Coset Table can be manipulated using the class variable | |
| ``CosetTable.coset_table_max_limit``. | |
| See Also | |
| ======== | |
| define_c | |
| """ | |
| A = self.A | |
| table = self.table | |
| len_table = len(table) | |
| if len_table >= self.coset_table_limit: | |
| # abort the further generation of cosets | |
| raise ValueError("the coset enumeration has defined more than " | |
| "%s cosets. Try with a greater value max number of cosets " | |
| % self.coset_table_limit) | |
| table.append([None]*len(A)) | |
| self.P.append([None]*len(self.A)) | |
| # beta is the new coset generated | |
| beta = len_table | |
| self.p.append(beta) | |
| table[alpha][self.A_dict[x]] = beta | |
| table[beta][self.A_dict_inv[x]] = alpha | |
| # P[alpha][x] = epsilon, P[beta][x**-1] = epsilon | |
| if modified: | |
| self.P[alpha][self.A_dict[x]] = self._grp.identity | |
| self.P[beta][self.A_dict_inv[x]] = self._grp.identity | |
| self.p_p[beta] = self._grp.identity | |
| def define_c(self, alpha, x): | |
| r""" | |
| A variation of ``define`` routine, described on Pg. 165 [1], used in | |
| the coset table-based strategy of Todd-Coxeter algorithm. It differs | |
| from ``define`` routine in that for each definition it also adds the | |
| tuple `(\alpha, x)` to the deduction stack. | |
| See Also | |
| ======== | |
| define | |
| """ | |
| A = self.A | |
| table = self.table | |
| len_table = len(table) | |
| if len_table >= self.coset_table_limit: | |
| # abort the further generation of cosets | |
| raise ValueError("the coset enumeration has defined more than " | |
| "%s cosets. Try with a greater value max number of cosets " | |
| % self.coset_table_limit) | |
| table.append([None]*len(A)) | |
| # beta is the new coset generated | |
| beta = len_table | |
| self.p.append(beta) | |
| table[alpha][self.A_dict[x]] = beta | |
| table[beta][self.A_dict_inv[x]] = alpha | |
| # append to deduction stack | |
| self.deduction_stack.append((alpha, x)) | |
| def scan_c(self, alpha, word): | |
| """ | |
| A variation of ``scan`` routine, described on pg. 165 of [1], which | |
| puts at tuple, whenever a deduction occurs, to deduction stack. | |
| See Also | |
| ======== | |
| scan, scan_check, scan_and_fill, scan_and_fill_c | |
| """ | |
| # alpha is an integer representing a "coset" | |
| # since scanning can be in two cases | |
| # 1. for alpha=0 and w in Y (i.e generating set of H) | |
| # 2. alpha in Omega (set of live cosets), w in R (relators) | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| f = alpha | |
| i = 0 | |
| r = len(word) | |
| b = alpha | |
| j = r - 1 | |
| # list of union of generators and their inverses | |
| while i <= j and table[f][A_dict[word[i]]] is not None: | |
| f = table[f][A_dict[word[i]]] | |
| i += 1 | |
| if i > j: | |
| if f != b: | |
| self.coincidence_c(f, b) | |
| return | |
| while j >= i and table[b][A_dict_inv[word[j]]] is not None: | |
| b = table[b][A_dict_inv[word[j]]] | |
| j -= 1 | |
| if j < i: | |
| # we have an incorrect completed scan with coincidence f ~ b | |
| # run the "coincidence" routine | |
| self.coincidence_c(f, b) | |
| elif j == i: | |
| # deduction process | |
| table[f][A_dict[word[i]]] = b | |
| table[b][A_dict_inv[word[i]]] = f | |
| self.deduction_stack.append((f, word[i])) | |
| # otherwise scan is incomplete and yields no information | |
| # alpha, beta coincide, i.e. alpha, beta represent the pair of cosets where | |
| # coincidence occurs | |
| def coincidence_c(self, alpha, beta): | |
| """ | |
| A variation of ``coincidence`` routine used in the coset-table based | |
| method of coset enumeration. The only difference being on addition of | |
| a new coset in coset table(i.e new coset introduction), then it is | |
| appended to ``deduction_stack``. | |
| See Also | |
| ======== | |
| coincidence | |
| """ | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| # behaves as a queue | |
| q = [] | |
| self.merge(alpha, beta, q) | |
| while len(q) > 0: | |
| gamma = q.pop(0) | |
| for x in A_dict: | |
| delta = table[gamma][A_dict[x]] | |
| if delta is not None: | |
| table[delta][A_dict_inv[x]] = None | |
| # only line of difference from ``coincidence`` routine | |
| self.deduction_stack.append((delta, x**-1)) | |
| mu = self.rep(gamma) | |
| nu = self.rep(delta) | |
| if table[mu][A_dict[x]] is not None: | |
| self.merge(nu, table[mu][A_dict[x]], q) | |
| elif table[nu][A_dict_inv[x]] is not None: | |
| self.merge(mu, table[nu][A_dict_inv[x]], q) | |
| else: | |
| table[mu][A_dict[x]] = nu | |
| table[nu][A_dict_inv[x]] = mu | |
| def scan(self, alpha, word, y=None, fill=False, modified=False): | |
| r""" | |
| ``scan`` performs a scanning process on the input ``word``. | |
| It first locates the largest prefix ``s`` of ``word`` for which | |
| `\alpha^s` is defined (i.e is not ``None``), ``s`` may be empty. Let | |
| ``word=sv``, let ``t`` be the longest suffix of ``v`` for which | |
| `\alpha^{t^{-1}}` is defined, and let ``v=ut``. Then three | |
| possibilities are there: | |
| 1. If ``t=v``, then we say that the scan completes, and if, in addition | |
| `\alpha^s = \alpha^{t^{-1}}`, then we say that the scan completes | |
| correctly. | |
| 2. It can also happen that scan does not complete, but `|u|=1`; that | |
| is, the word ``u`` consists of a single generator `x \in A`. In that | |
| case, if `\alpha^s = \beta` and `\alpha^{t^{-1}} = \gamma`, then we can | |
| set `\beta^x = \gamma` and `\gamma^{x^{-1}} = \beta`. These assignments | |
| are known as deductions and enable the scan to complete correctly. | |
| 3. See ``coicidence`` routine for explanation of third condition. | |
| Notes | |
| ===== | |
| The code for the procedure of scanning `\alpha \in \Omega` | |
| under `w \in A*` is defined on pg. 155 [1] | |
| See Also | |
| ======== | |
| scan_c, scan_check, scan_and_fill, scan_and_fill_c | |
| Scan and Fill | |
| ============= | |
| Performed when the default argument fill=True. | |
| Modified Scan | |
| ============= | |
| Performed when the default argument modified=True | |
| """ | |
| # alpha is an integer representing a "coset" | |
| # since scanning can be in two cases | |
| # 1. for alpha=0 and w in Y (i.e generating set of H) | |
| # 2. alpha in Omega (set of live cosets), w in R (relators) | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| f = alpha | |
| i = 0 | |
| r = len(word) | |
| b = alpha | |
| j = r - 1 | |
| b_p = y | |
| if modified: | |
| f_p = self._grp.identity | |
| flag = 0 | |
| while fill or flag == 0: | |
| flag = 1 | |
| while i <= j and table[f][A_dict[word[i]]] is not None: | |
| if modified: | |
| f_p = f_p*self.P[f][A_dict[word[i]]] | |
| f = table[f][A_dict[word[i]]] | |
| i += 1 | |
| if i > j: | |
| if f != b: | |
| if modified: | |
| self.modified_coincidence(f, b, f_p**-1*y) | |
| else: | |
| self.coincidence(f, b) | |
| return | |
| while j >= i and table[b][A_dict_inv[word[j]]] is not None: | |
| if modified: | |
| b_p = b_p*self.P[b][self.A_dict_inv[word[j]]] | |
| b = table[b][A_dict_inv[word[j]]] | |
| j -= 1 | |
| if j < i: | |
| # we have an incorrect completed scan with coincidence f ~ b | |
| # run the "coincidence" routine | |
| if modified: | |
| self.modified_coincidence(f, b, f_p**-1*b_p) | |
| else: | |
| self.coincidence(f, b) | |
| elif j == i: | |
| # deduction process | |
| table[f][A_dict[word[i]]] = b | |
| table[b][A_dict_inv[word[i]]] = f | |
| if modified: | |
| self.P[f][self.A_dict[word[i]]] = f_p**-1*b_p | |
| self.P[b][self.A_dict_inv[word[i]]] = b_p**-1*f_p | |
| return | |
| elif fill: | |
| self.define(f, word[i], modified=modified) | |
| # otherwise scan is incomplete and yields no information | |
| # used in the low-index subgroups algorithm | |
| def scan_check(self, alpha, word): | |
| r""" | |
| Another version of ``scan`` routine, described on, it checks whether | |
| `\alpha` scans correctly under `word`, it is a straightforward | |
| modification of ``scan``. ``scan_check`` returns ``False`` (rather than | |
| calling ``coincidence``) if the scan completes incorrectly; otherwise | |
| it returns ``True``. | |
| See Also | |
| ======== | |
| scan, scan_c, scan_and_fill, scan_and_fill_c | |
| """ | |
| # alpha is an integer representing a "coset" | |
| # since scanning can be in two cases | |
| # 1. for alpha=0 and w in Y (i.e generating set of H) | |
| # 2. alpha in Omega (set of live cosets), w in R (relators) | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| f = alpha | |
| i = 0 | |
| r = len(word) | |
| b = alpha | |
| j = r - 1 | |
| while i <= j and table[f][A_dict[word[i]]] is not None: | |
| f = table[f][A_dict[word[i]]] | |
| i += 1 | |
| if i > j: | |
| return f == b | |
| while j >= i and table[b][A_dict_inv[word[j]]] is not None: | |
| b = table[b][A_dict_inv[word[j]]] | |
| j -= 1 | |
| if j < i: | |
| # we have an incorrect completed scan with coincidence f ~ b | |
| # return False, instead of calling coincidence routine | |
| return False | |
| elif j == i: | |
| # deduction process | |
| table[f][A_dict[word[i]]] = b | |
| table[b][A_dict_inv[word[i]]] = f | |
| return True | |
| def merge(self, k, lamda, q, w=None, modified=False): | |
| """ | |
| Merge two classes with representatives ``k`` and ``lamda``, described | |
| on Pg. 157 [1] (for pseudocode), start by putting ``p[k] = lamda``. | |
| It is more efficient to choose the new representative from the larger | |
| of the two classes being merged, i.e larger among ``k`` and ``lamda``. | |
| procedure ``merge`` performs the merging operation, adds the deleted | |
| class representative to the queue ``q``. | |
| Parameters | |
| ========== | |
| 'k', 'lamda' being the two class representatives to be merged. | |
| Notes | |
| ===== | |
| Pg. 86-87 [1] contains a description of this method. | |
| See Also | |
| ======== | |
| coincidence, rep | |
| """ | |
| p = self.p | |
| rep = self.rep | |
| phi = rep(k, modified=modified) | |
| psi = rep(lamda, modified=modified) | |
| if phi != psi: | |
| mu = min(phi, psi) | |
| v = max(phi, psi) | |
| p[v] = mu | |
| if modified: | |
| if v == phi: | |
| self.p_p[phi] = self.p_p[k]**-1*w*self.p_p[lamda] | |
| else: | |
| self.p_p[psi] = self.p_p[lamda]**-1*w**-1*self.p_p[k] | |
| q.append(v) | |
| def rep(self, k, modified=False): | |
| r""" | |
| Parameters | |
| ========== | |
| `k \in [0 \ldots n-1]`, as for ``self`` only array ``p`` is used | |
| Returns | |
| ======= | |
| Representative of the class containing ``k``. | |
| Returns the representative of `\sim` class containing ``k``, it also | |
| makes some modification to array ``p`` of ``self`` to ease further | |
| computations, described on Pg. 157 [1]. | |
| The information on classes under `\sim` is stored in array `p` of | |
| ``self`` argument, which will always satisfy the property: | |
| `p[\alpha] \sim \alpha` and `p[\alpha]=\alpha \iff \alpha=rep(\alpha)` | |
| `\forall \in [0 \ldots n-1]`. | |
| So, for `\alpha \in [0 \ldots n-1]`, we find `rep(self, \alpha)` by | |
| continually replacing `\alpha` by `p[\alpha]` until it becomes | |
| constant (i.e satisfies `p[\alpha] = \alpha`):w | |
| To increase the efficiency of later ``rep`` calculations, whenever we | |
| find `rep(self, \alpha)=\beta`, we set | |
| `p[\gamma] = \beta \forall \gamma \in p-chain` from `\alpha` to `\beta` | |
| Notes | |
| ===== | |
| ``rep`` routine is also described on Pg. 85-87 [1] in Atkinson's | |
| algorithm, this results from the fact that ``coincidence`` routine | |
| introduces functionality similar to that introduced by the | |
| ``minimal_block`` routine on Pg. 85-87 [1]. | |
| See Also | |
| ======== | |
| coincidence, merge | |
| """ | |
| p = self.p | |
| lamda = k | |
| rho = p[lamda] | |
| if modified: | |
| s = p[:] | |
| while rho != lamda: | |
| if modified: | |
| s[rho] = lamda | |
| lamda = rho | |
| rho = p[lamda] | |
| if modified: | |
| rho = s[lamda] | |
| while rho != k: | |
| mu = rho | |
| rho = s[mu] | |
| p[rho] = lamda | |
| self.p_p[rho] = self.p_p[rho]*self.p_p[mu] | |
| else: | |
| mu = k | |
| rho = p[mu] | |
| while rho != lamda: | |
| p[mu] = lamda | |
| mu = rho | |
| rho = p[mu] | |
| return lamda | |
| # alpha, beta coincide, i.e. alpha, beta represent the pair of cosets | |
| # where coincidence occurs | |
| def coincidence(self, alpha, beta, w=None, modified=False): | |
| r""" | |
| The third situation described in ``scan`` routine is handled by this | |
| routine, described on Pg. 156-161 [1]. | |
| The unfortunate situation when the scan completes but not correctly, | |
| then ``coincidence`` routine is run. i.e when for some `i` with | |
| `1 \le i \le r+1`, we have `w=st` with `s = x_1 x_2 \dots x_{i-1}`, | |
| `t = x_i x_{i+1} \dots x_r`, and `\beta = \alpha^s` and | |
| `\gamma = \alpha^{t-1}` are defined but unequal. This means that | |
| `\beta` and `\gamma` represent the same coset of `H` in `G`. Described | |
| on Pg. 156 [1]. ``rep`` | |
| See Also | |
| ======== | |
| scan | |
| """ | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| # behaves as a queue | |
| q = [] | |
| if modified: | |
| self.modified_merge(alpha, beta, w, q) | |
| else: | |
| self.merge(alpha, beta, q) | |
| while len(q) > 0: | |
| gamma = q.pop(0) | |
| for x in A_dict: | |
| delta = table[gamma][A_dict[x]] | |
| if delta is not None: | |
| table[delta][A_dict_inv[x]] = None | |
| mu = self.rep(gamma, modified=modified) | |
| nu = self.rep(delta, modified=modified) | |
| if table[mu][A_dict[x]] is not None: | |
| if modified: | |
| v = self.p_p[delta]**-1*self.P[gamma][self.A_dict[x]]**-1 | |
| v = v*self.p_p[gamma]*self.P[mu][self.A_dict[x]] | |
| self.modified_merge(nu, table[mu][self.A_dict[x]], v, q) | |
| else: | |
| self.merge(nu, table[mu][A_dict[x]], q) | |
| elif table[nu][A_dict_inv[x]] is not None: | |
| if modified: | |
| v = self.p_p[gamma]**-1*self.P[gamma][self.A_dict[x]] | |
| v = v*self.p_p[delta]*self.P[mu][self.A_dict_inv[x]] | |
| self.modified_merge(mu, table[nu][self.A_dict_inv[x]], v, q) | |
| else: | |
| self.merge(mu, table[nu][A_dict_inv[x]], q) | |
| else: | |
| table[mu][A_dict[x]] = nu | |
| table[nu][A_dict_inv[x]] = mu | |
| if modified: | |
| v = self.p_p[gamma]**-1*self.P[gamma][self.A_dict[x]]*self.p_p[delta] | |
| self.P[mu][self.A_dict[x]] = v | |
| self.P[nu][self.A_dict_inv[x]] = v**-1 | |
| # method used in the HLT strategy | |
| def scan_and_fill(self, alpha, word): | |
| """ | |
| A modified version of ``scan`` routine used in the relator-based | |
| method of coset enumeration, described on pg. 162-163 [1], which | |
| follows the idea that whenever the procedure is called and the scan | |
| is incomplete then it makes new definitions to enable the scan to | |
| complete; i.e it fills in the gaps in the scan of the relator or | |
| subgroup generator. | |
| """ | |
| self.scan(alpha, word, fill=True) | |
| def scan_and_fill_c(self, alpha, word): | |
| """ | |
| A modified version of ``scan`` routine, described on Pg. 165 second | |
| para. [1], with modification similar to that of ``scan_anf_fill`` the | |
| only difference being it calls the coincidence procedure used in the | |
| coset-table based method i.e. the routine ``coincidence_c`` is used. | |
| See Also | |
| ======== | |
| scan, scan_and_fill | |
| """ | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| r = len(word) | |
| f = alpha | |
| i = 0 | |
| b = alpha | |
| j = r - 1 | |
| # loop until it has filled the alpha row in the table. | |
| while True: | |
| # do the forward scanning | |
| while i <= j and table[f][A_dict[word[i]]] is not None: | |
| f = table[f][A_dict[word[i]]] | |
| i += 1 | |
| if i > j: | |
| if f != b: | |
| self.coincidence_c(f, b) | |
| return | |
| # forward scan was incomplete, scan backwards | |
| while j >= i and table[b][A_dict_inv[word[j]]] is not None: | |
| b = table[b][A_dict_inv[word[j]]] | |
| j -= 1 | |
| if j < i: | |
| self.coincidence_c(f, b) | |
| elif j == i: | |
| table[f][A_dict[word[i]]] = b | |
| table[b][A_dict_inv[word[i]]] = f | |
| self.deduction_stack.append((f, word[i])) | |
| else: | |
| self.define_c(f, word[i]) | |
| # method used in the HLT strategy | |
| def look_ahead(self): | |
| """ | |
| When combined with the HLT method this is known as HLT+Lookahead | |
| method of coset enumeration, described on pg. 164 [1]. Whenever | |
| ``define`` aborts due to lack of space available this procedure is | |
| executed. This routine helps in recovering space resulting from | |
| "coincidence" of cosets. | |
| """ | |
| R = self.fp_group.relators | |
| p = self.p | |
| # complete scan all relators under all cosets(obviously live) | |
| # without making new definitions | |
| for beta in self.omega: | |
| for w in R: | |
| self.scan(beta, w) | |
| if p[beta] < beta: | |
| break | |
| # Pg. 166 | |
| def process_deductions(self, R_c_x, R_c_x_inv): | |
| """ | |
| Processes the deductions that have been pushed onto ``deduction_stack``, | |
| described on Pg. 166 [1] and is used in coset-table based enumeration. | |
| See Also | |
| ======== | |
| deduction_stack | |
| """ | |
| p = self.p | |
| table = self.table | |
| while len(self.deduction_stack) > 0: | |
| if len(self.deduction_stack) >= CosetTable.max_stack_size: | |
| self.look_ahead() | |
| del self.deduction_stack[:] | |
| continue | |
| else: | |
| alpha, x = self.deduction_stack.pop() | |
| if p[alpha] == alpha: | |
| for w in R_c_x: | |
| self.scan_c(alpha, w) | |
| if p[alpha] < alpha: | |
| break | |
| beta = table[alpha][self.A_dict[x]] | |
| if beta is not None and p[beta] == beta: | |
| for w in R_c_x_inv: | |
| self.scan_c(beta, w) | |
| if p[beta] < beta: | |
| break | |
| def process_deductions_check(self, R_c_x, R_c_x_inv): | |
| """ | |
| A variation of ``process_deductions``, this calls ``scan_check`` | |
| wherever ``process_deductions`` calls ``scan``, described on Pg. [1]. | |
| See Also | |
| ======== | |
| process_deductions | |
| """ | |
| table = self.table | |
| while len(self.deduction_stack) > 0: | |
| alpha, x = self.deduction_stack.pop() | |
| for w in R_c_x: | |
| if not self.scan_check(alpha, w): | |
| return False | |
| beta = table[alpha][self.A_dict[x]] | |
| if beta is not None: | |
| for w in R_c_x_inv: | |
| if not self.scan_check(beta, w): | |
| return False | |
| return True | |
| def switch(self, beta, gamma): | |
| r"""Switch the elements `\beta, \gamma \in \Omega` of ``self``, used | |
| by the ``standardize`` procedure, described on Pg. 167 [1]. | |
| See Also | |
| ======== | |
| standardize | |
| """ | |
| A = self.A | |
| A_dict = self.A_dict | |
| table = self.table | |
| for x in A: | |
| z = table[gamma][A_dict[x]] | |
| table[gamma][A_dict[x]] = table[beta][A_dict[x]] | |
| table[beta][A_dict[x]] = z | |
| for alpha in range(len(self.p)): | |
| if self.p[alpha] == alpha: | |
| if table[alpha][A_dict[x]] == beta: | |
| table[alpha][A_dict[x]] = gamma | |
| elif table[alpha][A_dict[x]] == gamma: | |
| table[alpha][A_dict[x]] = beta | |
| def standardize(self): | |
| r""" | |
| A coset table is standardized if when running through the cosets and | |
| within each coset through the generator images (ignoring generator | |
| inverses), the cosets appear in order of the integers | |
| `0, 1, \dots, n`. "Standardize" reorders the elements of `\Omega` | |
| such that, if we scan the coset table first by elements of `\Omega` | |
| and then by elements of A, then the cosets occur in ascending order. | |
| ``standardize()`` is used at the end of an enumeration to permute the | |
| cosets so that they occur in some sort of standard order. | |
| Notes | |
| ===== | |
| procedure is described on pg. 167-168 [1], it also makes use of the | |
| ``switch`` routine to replace by smaller integer value. | |
| Examples | |
| ======== | |
| >>> from sympy.combinatorics import free_group | |
| >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r | |
| >>> F, x, y = free_group("x, y") | |
| # Example 5.3 from [1] | |
| >>> f = FpGroup(F, [x**2*y**2, x**3*y**5]) | |
| >>> C = coset_enumeration_r(f, []) | |
| >>> C.compress() | |
| >>> C.table | |
| [[1, 3, 1, 3], [2, 0, 2, 0], [3, 1, 3, 1], [0, 2, 0, 2]] | |
| >>> C.standardize() | |
| >>> C.table | |
| [[1, 2, 1, 2], [3, 0, 3, 0], [0, 3, 0, 3], [2, 1, 2, 1]] | |
| """ | |
| A = self.A | |
| A_dict = self.A_dict | |
| gamma = 1 | |
| for alpha, x in product(range(self.n), A): | |
| beta = self.table[alpha][A_dict[x]] | |
| if beta >= gamma: | |
| if beta > gamma: | |
| self.switch(gamma, beta) | |
| gamma += 1 | |
| if gamma == self.n: | |
| return | |
| # Compression of a Coset Table | |
| def compress(self): | |
| """Removes the non-live cosets from the coset table, described on | |
| pg. 167 [1]. | |
| """ | |
| gamma = -1 | |
| A = self.A | |
| A_dict = self.A_dict | |
| A_dict_inv = self.A_dict_inv | |
| table = self.table | |
| chi = tuple([i for i in range(len(self.p)) if self.p[i] != i]) | |
| for alpha in self.omega: | |
| gamma += 1 | |
| if gamma != alpha: | |
| # replace alpha by gamma in coset table | |
| for x in A: | |
| beta = table[alpha][A_dict[x]] | |
| table[gamma][A_dict[x]] = beta | |
| table[beta][A_dict_inv[x]] == gamma | |
| # all the cosets in the table are live cosets | |
| self.p = list(range(gamma + 1)) | |
| # delete the useless columns | |
| del table[len(self.p):] | |
| # re-define values | |
| for row in table: | |
| for j in range(len(self.A)): | |
| row[j] -= bisect_left(chi, row[j]) | |
| def conjugates(self, R): | |
| R_c = list(chain.from_iterable((rel.cyclic_conjugates(), \ | |
| (rel**-1).cyclic_conjugates()) for rel in R)) | |
| R_set = set() | |
| for conjugate in R_c: | |
| R_set = R_set.union(conjugate) | |
| R_c_list = [] | |
| for x in self.A: | |
| r = {word for word in R_set if word[0] == x} | |
| R_c_list.append(r) | |
| R_set.difference_update(r) | |
| return R_c_list | |
| def coset_representative(self, coset): | |
| ''' | |
| Compute the coset representative of a given coset. | |
| Examples | |
| ======== | |
| >>> from sympy.combinatorics import free_group | |
| >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r | |
| >>> F, x, y = free_group("x, y") | |
| >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) | |
| >>> C = coset_enumeration_r(f, [x]) | |
| >>> C.compress() | |
| >>> C.table | |
| [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]] | |
| >>> C.coset_representative(0) | |
| <identity> | |
| >>> C.coset_representative(1) | |
| y | |
| >>> C.coset_representative(2) | |
| y**-1 | |
| ''' | |
| for x in self.A: | |
| gamma = self.table[coset][self.A_dict[x]] | |
| if coset == 0: | |
| return self.fp_group.identity | |
| if gamma < coset: | |
| return self.coset_representative(gamma)*x**-1 | |
| ############################## | |
| # Modified Methods # | |
| ############################## | |
| def modified_define(self, alpha, x): | |
| r""" | |
| Define a function p_p from from [1..n] to A* as | |
| an additional component of the modified coset table. | |
| Parameters | |
| ========== | |
| \alpha \in \Omega | |
| x \in A* | |
| See Also | |
| ======== | |
| define | |
| """ | |
| self.define(alpha, x, modified=True) | |
| def modified_scan(self, alpha, w, y, fill=False): | |
| r""" | |
| Parameters | |
| ========== | |
| \alpha \in \Omega | |
| w \in A* | |
| y \in (YUY^-1) | |
| fill -- `modified_scan_and_fill` when set to True. | |
| See Also | |
| ======== | |
| scan | |
| """ | |
| self.scan(alpha, w, y=y, fill=fill, modified=True) | |
| def modified_scan_and_fill(self, alpha, w, y): | |
| self.modified_scan(alpha, w, y, fill=True) | |
| def modified_merge(self, k, lamda, w, q): | |
| r""" | |
| Parameters | |
| ========== | |
| 'k', 'lamda' -- the two class representatives to be merged. | |
| q -- queue of length l of elements to be deleted from `\Omega` *. | |
| w -- Word in (YUY^-1) | |
| See Also | |
| ======== | |
| merge | |
| """ | |
| self.merge(k, lamda, q, w=w, modified=True) | |
| def modified_rep(self, k): | |
| r""" | |
| Parameters | |
| ========== | |
| `k \in [0 \ldots n-1]` | |
| See Also | |
| ======== | |
| rep | |
| """ | |
| self.rep(k, modified=True) | |
| def modified_coincidence(self, alpha, beta, w): | |
| r""" | |
| Parameters | |
| ========== | |
| A coincident pair `\alpha, \beta \in \Omega, w \in Y \cup Y^{-1}` | |
| See Also | |
| ======== | |
| coincidence | |
| """ | |
| self.coincidence(alpha, beta, w=w, modified=True) | |
| ############################################################################### | |
| # COSET ENUMERATION # | |
| ############################################################################### | |
| # relator-based method | |
| def coset_enumeration_r(fp_grp, Y, max_cosets=None, draft=None, | |
| incomplete=False, modified=False): | |
| """ | |
| This is easier of the two implemented methods of coset enumeration. | |
| and is often called the HLT method, after Hazelgrove, Leech, Trotter | |
| The idea is that we make use of ``scan_and_fill`` makes new definitions | |
| whenever the scan is incomplete to enable the scan to complete; this way | |
| we fill in the gaps in the scan of the relator or subgroup generator, | |
| that's why the name relator-based method. | |
| An instance of `CosetTable` for `fp_grp` can be passed as the keyword | |
| argument `draft` in which case the coset enumeration will start with | |
| that instance and attempt to complete it. | |
| When `incomplete` is `True` and the function is unable to complete for | |
| some reason, the partially complete table will be returned. | |
| # TODO: complete the docstring | |
| See Also | |
| ======== | |
| scan_and_fill, | |
| Examples | |
| ======== | |
| >>> from sympy.combinatorics.free_groups import free_group | |
| >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r | |
| >>> F, x, y = free_group("x, y") | |
| # Example 5.1 from [1] | |
| >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) | |
| >>> C = coset_enumeration_r(f, [x]) | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... print(C.table[i]) | |
| [0, 0, 1, 2] | |
| [1, 1, 2, 0] | |
| [2, 2, 0, 1] | |
| >>> C.p | |
| [0, 1, 2, 1, 1] | |
| # Example from exercises Q2 [1] | |
| >>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) | |
| >>> C = coset_enumeration_r(f, []) | |
| >>> C.compress(); C.standardize() | |
| >>> C.table | |
| [[1, 2, 3, 4], | |
| [5, 0, 6, 7], | |
| [0, 5, 7, 6], | |
| [7, 6, 5, 0], | |
| [6, 7, 0, 5], | |
| [2, 1, 4, 3], | |
| [3, 4, 2, 1], | |
| [4, 3, 1, 2]] | |
| # Example 5.2 | |
| >>> f = FpGroup(F, [x**2, y**3, (x*y)**3]) | |
| >>> Y = [x*y] | |
| >>> C = coset_enumeration_r(f, Y) | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... print(C.table[i]) | |
| [1, 1, 2, 1] | |
| [0, 0, 0, 2] | |
| [3, 3, 1, 0] | |
| [2, 2, 3, 3] | |
| # Example 5.3 | |
| >>> f = FpGroup(F, [x**2*y**2, x**3*y**5]) | |
| >>> Y = [] | |
| >>> C = coset_enumeration_r(f, Y) | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... print(C.table[i]) | |
| [1, 3, 1, 3] | |
| [2, 0, 2, 0] | |
| [3, 1, 3, 1] | |
| [0, 2, 0, 2] | |
| # Example 5.4 | |
| >>> F, a, b, c, d, e = free_group("a, b, c, d, e") | |
| >>> f = FpGroup(F, [a*b*c**-1, b*c*d**-1, c*d*e**-1, d*e*a**-1, e*a*b**-1]) | |
| >>> Y = [a] | |
| >>> C = coset_enumeration_r(f, Y) | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... print(C.table[i]) | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | |
| # example of "compress" method | |
| >>> C.compress() | |
| >>> C.table | |
| [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] | |
| # Exercises Pg. 161, Q2. | |
| >>> F, x, y = free_group("x, y") | |
| >>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3]) | |
| >>> Y = [] | |
| >>> C = coset_enumeration_r(f, Y) | |
| >>> C.compress() | |
| >>> C.standardize() | |
| >>> C.table | |
| [[1, 2, 3, 4], | |
| [5, 0, 6, 7], | |
| [0, 5, 7, 6], | |
| [7, 6, 5, 0], | |
| [6, 7, 0, 5], | |
| [2, 1, 4, 3], | |
| [3, 4, 2, 1], | |
| [4, 3, 1, 2]] | |
| # John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson | |
| # Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490 | |
| # from 1973chwd.pdf | |
| # Table 1. Ex. 1 | |
| >>> F, r, s, t = free_group("r, s, t") | |
| >>> E1 = FpGroup(F, [t**-1*r*t*r**-2, r**-1*s*r*s**-2, s**-1*t*s*t**-2]) | |
| >>> C = coset_enumeration_r(E1, [r]) | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... print(C.table[i]) | |
| [0, 0, 0, 0, 0, 0] | |
| Ex. 2 | |
| >>> F, a, b = free_group("a, b") | |
| >>> Cox = FpGroup(F, [a**6, b**6, (a*b)**2, (a**2*b**2)**2, (a**3*b**3)**5]) | |
| >>> C = coset_enumeration_r(Cox, [a]) | |
| >>> index = 0 | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... index += 1 | |
| >>> index | |
| 500 | |
| # Ex. 3 | |
| >>> F, a, b = free_group("a, b") | |
| >>> B_2_4 = FpGroup(F, [a**4, b**4, (a*b)**4, (a**-1*b)**4, (a**2*b)**4, \ | |
| (a*b**2)**4, (a**2*b**2)**4, (a**-1*b*a*b)**4, (a*b**-1*a*b)**4]) | |
| >>> C = coset_enumeration_r(B_2_4, [a]) | |
| >>> index = 0 | |
| >>> for i in range(len(C.p)): | |
| ... if C.p[i] == i: | |
| ... index += 1 | |
| >>> index | |
| 1024 | |
| References | |
| ========== | |
| .. [1] Holt, D., Eick, B., O'Brien, E. | |
| "Handbook of computational group theory" | |
| """ | |
| # 1. Initialize a coset table C for < X|R > | |
| C = CosetTable(fp_grp, Y, max_cosets=max_cosets) | |
| # Define coset table methods. | |
| if modified: | |
| _scan_and_fill = C.modified_scan_and_fill | |
| _define = C.modified_define | |
| else: | |
| _scan_and_fill = C.scan_and_fill | |
| _define = C.define | |
| if draft: | |
| C.table = draft.table[:] | |
| C.p = draft.p[:] | |
| R = fp_grp.relators | |
| A_dict = C.A_dict | |
| p = C.p | |
| for i in range(len(Y)): | |
| if modified: | |
| _scan_and_fill(0, Y[i], C._grp.generators[i]) | |
| else: | |
| _scan_and_fill(0, Y[i]) | |
| alpha = 0 | |
| while alpha < C.n: | |
| if p[alpha] == alpha: | |
| try: | |
| for w in R: | |
| if modified: | |
| _scan_and_fill(alpha, w, C._grp.identity) | |
| else: | |
| _scan_and_fill(alpha, w) | |
| # if alpha was eliminated during the scan then break | |
| if p[alpha] < alpha: | |
| break | |
| if p[alpha] == alpha: | |
| for x in A_dict: | |
| if C.table[alpha][A_dict[x]] is None: | |
| _define(alpha, x) | |
| except ValueError as e: | |
| if incomplete: | |
| return C | |
| raise e | |
| alpha += 1 | |
| return C | |
| def modified_coset_enumeration_r(fp_grp, Y, max_cosets=None, draft=None, | |
| incomplete=False): | |
| r""" | |
| Introduce a new set of symbols y \in Y that correspond to the | |
| generators of the subgroup. Store the elements of Y as a | |
| word P[\alpha, x] and compute the coset table similar to that of | |
| the regular coset enumeration methods. | |
| Examples | |
| ======== | |
| >>> from sympy.combinatorics.free_groups import free_group | |
| >>> from sympy.combinatorics.fp_groups import FpGroup | |
| >>> from sympy.combinatorics.coset_table import modified_coset_enumeration_r | |
| >>> F, x, y = free_group("x, y") | |
| >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) | |
| >>> C = modified_coset_enumeration_r(f, [x]) | |
| >>> C.table | |
| [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1], [None, 1, None, None], [1, 3, None, None]] | |
| See Also | |
| ======== | |
| coset_enumertation_r | |
| References | |
| ========== | |
| .. [1] Holt, D., Eick, B., O'Brien, E., | |
| "Handbook of Computational Group Theory", | |
| Section 5.3.2 | |
| """ | |
| return coset_enumeration_r(fp_grp, Y, max_cosets=max_cosets, draft=draft, | |
| incomplete=incomplete, modified=True) | |
| # Pg. 166 | |
| # coset-table based method | |
| def coset_enumeration_c(fp_grp, Y, max_cosets=None, draft=None, | |
| incomplete=False): | |
| """ | |
| >>> from sympy.combinatorics.free_groups import free_group | |
| >>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_c | |
| >>> F, x, y = free_group("x, y") | |
| >>> f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y]) | |
| >>> C = coset_enumeration_c(f, [x]) | |
| >>> C.table | |
| [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]] | |
| """ | |
| # Initialize a coset table C for < X|R > | |
| X = fp_grp.generators | |
| R = fp_grp.relators | |
| C = CosetTable(fp_grp, Y, max_cosets=max_cosets) | |
| if draft: | |
| C.table = draft.table[:] | |
| C.p = draft.p[:] | |
| C.deduction_stack = draft.deduction_stack | |
| for alpha, x in product(range(len(C.table)), X): | |
| if C.table[alpha][C.A_dict[x]] is not None: | |
| C.deduction_stack.append((alpha, x)) | |
| A = C.A | |
| # replace all the elements by cyclic reductions | |
| R_cyc_red = [rel.identity_cyclic_reduction() for rel in R] | |
| R_c = list(chain.from_iterable((rel.cyclic_conjugates(), (rel**-1).cyclic_conjugates()) \ | |
| for rel in R_cyc_red)) | |
| R_set = set() | |
| for conjugate in R_c: | |
| R_set = R_set.union(conjugate) | |
| # a list of subsets of R_c whose words start with "x". | |
| R_c_list = [] | |
| for x in C.A: | |
| r = {word for word in R_set if word[0] == x} | |
| R_c_list.append(r) | |
| R_set.difference_update(r) | |
| for w in Y: | |
| C.scan_and_fill_c(0, w) | |
| for x in A: | |
| C.process_deductions(R_c_list[C.A_dict[x]], R_c_list[C.A_dict_inv[x]]) | |
| alpha = 0 | |
| while alpha < len(C.table): | |
| if C.p[alpha] == alpha: | |
| try: | |
| for x in C.A: | |
| if C.p[alpha] != alpha: | |
| break | |
| if C.table[alpha][C.A_dict[x]] is None: | |
| C.define_c(alpha, x) | |
| C.process_deductions(R_c_list[C.A_dict[x]], R_c_list[C.A_dict_inv[x]]) | |
| except ValueError as e: | |
| if incomplete: | |
| return C | |
| raise e | |
| alpha += 1 | |
| return C | |