1256 lines
42 KiB
Python
1256 lines
42 KiB
Python
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 = {}
|
|
|
|
@property
|
|
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__
|
|
|
|
@property
|
|
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
|