|
from collections import deque |
|
from sympy.combinatorics.rewritingsystem_fsm import StateMachine |
|
|
|
class RewritingSystem: |
|
''' |
|
A class implementing rewriting systems for `FpGroup`s. |
|
|
|
References |
|
========== |
|
.. [1] Epstein, D., Holt, D. and Rees, S. (1991). |
|
The use of Knuth-Bendix methods to solve the word problem in automatic groups. |
|
Journal of Symbolic Computation, 12(4-5), pp.397-414. |
|
|
|
.. [2] GAP's Manual on its KBMAG package |
|
https://www.gap-system.org/Manuals/pkg/kbmag-1.5.3/doc/manual.pdf |
|
|
|
''' |
|
def __init__(self, group): |
|
self.group = group |
|
self.alphabet = group.generators |
|
self._is_confluent = None |
|
|
|
|
|
self.maxeqns = 32767 |
|
self.tidyint = 100 |
|
|
|
|
|
|
|
self._max_exceeded = False |
|
|
|
|
|
self.reduction_automaton = None |
|
self._new_rules = {} |
|
|
|
|
|
self.rules = {} |
|
self.rules_cache = deque([], 50) |
|
self._init_rules() |
|
|
|
|
|
|
|
generators = list(self.alphabet) |
|
generators += [gen**-1 for gen in generators] |
|
|
|
self.reduction_automaton = StateMachine('Reduction automaton for '+ repr(self.group), generators) |
|
self.construct_automaton() |
|
|
|
def set_max(self, n): |
|
''' |
|
Set the maximum number of rules that can be defined |
|
|
|
''' |
|
if n > self.maxeqns: |
|
self._max_exceeded = False |
|
self.maxeqns = n |
|
return |
|
|
|
@property |
|
def is_confluent(self): |
|
''' |
|
Return `True` if the system is confluent |
|
|
|
''' |
|
if self._is_confluent is None: |
|
self._is_confluent = self._check_confluence() |
|
return self._is_confluent |
|
|
|
def _init_rules(self): |
|
identity = self.group.free_group.identity |
|
for r in self.group.relators: |
|
self.add_rule(r, identity) |
|
self._remove_redundancies() |
|
return |
|
|
|
def _add_rule(self, r1, r2): |
|
''' |
|
Add the rule r1 -> r2 with no checking or further |
|
deductions |
|
|
|
''' |
|
if len(self.rules) + 1 > self.maxeqns: |
|
self._is_confluent = self._check_confluence() |
|
self._max_exceeded = True |
|
raise RuntimeError("Too many rules were defined.") |
|
self.rules[r1] = r2 |
|
|
|
if self.reduction_automaton: |
|
self._new_rules[r1] = r2 |
|
|
|
def add_rule(self, w1, w2, check=False): |
|
new_keys = set() |
|
|
|
if w1 == w2: |
|
return new_keys |
|
|
|
if w1 < w2: |
|
w1, w2 = w2, w1 |
|
|
|
if (w1, w2) in self.rules_cache: |
|
return new_keys |
|
self.rules_cache.append((w1, w2)) |
|
|
|
s1, s2 = w1, w2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(s1) - len(s2) < 3: |
|
if s1 not in self.rules: |
|
new_keys.add(s1) |
|
if not check: |
|
self._add_rule(s1, s2) |
|
if s2**-1 > s1**-1 and s2**-1 not in self.rules: |
|
new_keys.add(s2**-1) |
|
if not check: |
|
self._add_rule(s2**-1, s1**-1) |
|
|
|
|
|
while len(s1) - len(s2) > -1: |
|
g = s1[len(s1)-1] |
|
s1 = s1.subword(0, len(s1)-1) |
|
s2 = s2*g**-1 |
|
if len(s1) - len(s2) < 0: |
|
if s2 not in self.rules: |
|
if not check: |
|
self._add_rule(s2, s1) |
|
new_keys.add(s2) |
|
elif len(s1) - len(s2) < 3: |
|
new = self.add_rule(s1, s2, check) |
|
new_keys.update(new) |
|
|
|
|
|
while len(w1) - len(w2) > -1: |
|
g = w1[0] |
|
w1 = w1.subword(1, len(w1)) |
|
w2 = g**-1*w2 |
|
if len(w1) - len(w2) < 0: |
|
if w2 not in self.rules: |
|
if not check: |
|
self._add_rule(w2, w1) |
|
new_keys.add(w2) |
|
elif len(w1) - len(w2) < 3: |
|
new = self.add_rule(w1, w2, check) |
|
new_keys.update(new) |
|
|
|
return new_keys |
|
|
|
def _remove_redundancies(self, changes=False): |
|
''' |
|
Reduce left- and right-hand sides of reduction rules |
|
and remove redundant equations (i.e. those for which |
|
lhs == rhs). If `changes` is `True`, return a set |
|
containing the removed keys and a set containing the |
|
added keys |
|
|
|
''' |
|
removed = set() |
|
added = set() |
|
rules = self.rules.copy() |
|
for r in rules: |
|
v = self.reduce(r, exclude=r) |
|
w = self.reduce(rules[r]) |
|
if v != r: |
|
del self.rules[r] |
|
removed.add(r) |
|
if v > w: |
|
added.add(v) |
|
self.rules[v] = w |
|
elif v < w: |
|
added.add(w) |
|
self.rules[w] = v |
|
else: |
|
self.rules[v] = w |
|
if changes: |
|
return removed, added |
|
return |
|
|
|
def make_confluent(self, check=False): |
|
''' |
|
Try to make the system confluent using the Knuth-Bendix |
|
completion algorithm |
|
|
|
''' |
|
if self._max_exceeded: |
|
return self._is_confluent |
|
lhs = list(self.rules.keys()) |
|
|
|
def _overlaps(r1, r2): |
|
len1 = len(r1) |
|
len2 = len(r2) |
|
result = [] |
|
for j in range(1, len1 + len2): |
|
if (r1.subword(len1 - j, len1 + len2 - j, strict=False) |
|
== r2.subword(j - len1, j, strict=False)): |
|
a = r1.subword(0, len1-j, strict=False) |
|
a = a*r2.subword(0, j-len1, strict=False) |
|
b = r2.subword(j-len1, j, strict=False) |
|
c = r2.subword(j, len2, strict=False) |
|
c = c*r1.subword(len1 + len2 - j, len1, strict=False) |
|
result.append(a*b*c) |
|
return result |
|
|
|
def _process_overlap(w, r1, r2, check): |
|
s = w.eliminate_word(r1, self.rules[r1]) |
|
s = self.reduce(s) |
|
t = w.eliminate_word(r2, self.rules[r2]) |
|
t = self.reduce(t) |
|
if s != t: |
|
if check: |
|
|
|
return [0] |
|
try: |
|
new_keys = self.add_rule(t, s, check) |
|
return new_keys |
|
except RuntimeError: |
|
return False |
|
return |
|
|
|
added = 0 |
|
i = 0 |
|
while i < len(lhs): |
|
r1 = lhs[i] |
|
i += 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
j = 0 |
|
while j < len(lhs): |
|
r2 = lhs[j] |
|
j += 1 |
|
if r1 == r2: |
|
continue |
|
overlaps = _overlaps(r1, r2) |
|
overlaps.extend(_overlaps(r1**-1, r2)) |
|
if not overlaps: |
|
continue |
|
for w in overlaps: |
|
new_keys = _process_overlap(w, r1, r2, check) |
|
if new_keys: |
|
if check: |
|
return False |
|
lhs.extend(new_keys) |
|
added += len(new_keys) |
|
elif new_keys == False: |
|
|
|
|
|
return self._is_confluent |
|
|
|
if added > self.tidyint and not check: |
|
|
|
r, a = self._remove_redundancies(changes=True) |
|
added = 0 |
|
if r: |
|
|
|
i = min(lhs.index(s) for s in r) |
|
lhs = [l for l in lhs if l not in r] |
|
lhs.extend(a) |
|
if r1 in r: |
|
|
|
break |
|
|
|
self._is_confluent = True |
|
if not check: |
|
self._remove_redundancies() |
|
return True |
|
|
|
def _check_confluence(self): |
|
return self.make_confluent(check=True) |
|
|
|
def reduce(self, word, exclude=None): |
|
''' |
|
Apply reduction rules to `word` excluding the reduction rule |
|
for the lhs equal to `exclude` |
|
|
|
''' |
|
rules = {r: self.rules[r] for r in self.rules if r != exclude} |
|
|
|
|
|
|
|
again = True |
|
new = word |
|
while again: |
|
again = False |
|
for r in rules: |
|
prev = new |
|
if rules[r]**-1 > r**-1: |
|
new = new.eliminate_word(r, rules[r], _all=True, inverse=False) |
|
else: |
|
new = new.eliminate_word(r, rules[r], _all=True) |
|
if new != prev: |
|
again = True |
|
return new |
|
|
|
def _compute_inverse_rules(self, rules): |
|
''' |
|
Compute the inverse rules for a given set of rules. |
|
The inverse rules are used in the automaton for word reduction. |
|
|
|
Arguments: |
|
rules (dictionary): Rules for which the inverse rules are to computed. |
|
|
|
Returns: |
|
Dictionary of inverse_rules. |
|
|
|
''' |
|
inverse_rules = {} |
|
for r in rules: |
|
rule_key_inverse = r**-1 |
|
rule_value_inverse = (rules[r])**-1 |
|
if (rule_value_inverse < rule_key_inverse): |
|
inverse_rules[rule_key_inverse] = rule_value_inverse |
|
else: |
|
inverse_rules[rule_value_inverse] = rule_key_inverse |
|
return inverse_rules |
|
|
|
def construct_automaton(self): |
|
''' |
|
Construct the automaton based on the set of reduction rules of the system. |
|
|
|
Automata Design: |
|
The accept states of the automaton are the proper prefixes of the left hand side of the rules. |
|
The complete left hand side of the rules are the dead states of the automaton. |
|
|
|
''' |
|
self._add_to_automaton(self.rules) |
|
|
|
def _add_to_automaton(self, rules): |
|
''' |
|
Add new states and transitions to the automaton. |
|
|
|
Summary: |
|
States corresponding to the new rules added to the system are computed and added to the automaton. |
|
Transitions in the previously added states are also modified if necessary. |
|
|
|
Arguments: |
|
rules (dictionary) -- Dictionary of the newly added rules. |
|
|
|
''' |
|
|
|
automaton_alphabet = [] |
|
proper_prefixes = {} |
|
|
|
|
|
all_rules = rules |
|
inverse_rules = self._compute_inverse_rules(all_rules) |
|
all_rules.update(inverse_rules) |
|
|
|
|
|
accept_states = [] |
|
|
|
for rule in all_rules: |
|
|
|
|
|
automaton_alphabet += rule.letter_form_elm |
|
|
|
proper_prefixes[rule] = [] |
|
letter_word_array = list(rule.letter_form_elm) |
|
len_letter_word_array = len(letter_word_array) |
|
for i in range (1, len_letter_word_array): |
|
letter_word_array[i] = letter_word_array[i-1]*letter_word_array[i] |
|
|
|
elem = letter_word_array[i-1] |
|
if elem not in self.reduction_automaton.states: |
|
self.reduction_automaton.add_state(elem, state_type='a') |
|
accept_states.append(elem) |
|
proper_prefixes[rule] = letter_word_array |
|
|
|
if rule in accept_states: |
|
self.reduction_automaton.states[rule].state_type = 'd' |
|
self.reduction_automaton.states[rule].rh_rule = all_rules[rule] |
|
accept_states.remove(rule) |
|
|
|
if rule not in self.reduction_automaton.states: |
|
self.reduction_automaton.add_state(rule, state_type='d', rh_rule=all_rules[rule]) |
|
|
|
automaton_alphabet = set(automaton_alphabet) |
|
|
|
|
|
for state in self.reduction_automaton.states: |
|
current_state_name = state |
|
current_state_type = self.reduction_automaton.states[state].state_type |
|
|
|
|
|
|
|
if current_state_type == 's': |
|
for letter in automaton_alphabet: |
|
if letter in self.reduction_automaton.states: |
|
self.reduction_automaton.states[state].add_transition(letter, letter) |
|
else: |
|
self.reduction_automaton.states[state].add_transition(letter, current_state_name) |
|
elif current_state_type == 'a': |
|
|
|
for letter in automaton_alphabet: |
|
_next = current_state_name*letter |
|
while len(_next) and _next not in self.reduction_automaton.states: |
|
_next = _next.subword(1, len(_next)) |
|
if not len(_next): |
|
_next = 'start' |
|
self.reduction_automaton.states[state].add_transition(letter, _next) |
|
|
|
|
|
|
|
if len(self.reduction_automaton.automaton_alphabet) != len(automaton_alphabet): |
|
for state in accept_states: |
|
current_state_name = state |
|
for letter in self.reduction_automaton.automaton_alphabet: |
|
_next = current_state_name*letter |
|
while len(_next) and _next not in self.reduction_automaton.states: |
|
_next = _next.subword(1, len(_next)) |
|
if not len(_next): |
|
_next = 'start' |
|
self.reduction_automaton.states[state].add_transition(letter, _next) |
|
|
|
def reduce_using_automaton(self, word): |
|
''' |
|
Reduce a word using an automaton. |
|
|
|
Summary: |
|
All the symbols of the word are stored in an array and are given as the input to the automaton. |
|
If the automaton reaches a dead state that subword is replaced and the automaton is run from the beginning. |
|
The complete word has to be replaced when the word is read and the automaton reaches a dead state. |
|
So, this process is repeated until the word is read completely and the automaton reaches the accept state. |
|
|
|
Arguments: |
|
word (instance of FreeGroupElement) -- Word that needs to be reduced. |
|
|
|
''' |
|
|
|
if self._new_rules: |
|
self._add_to_automaton(self._new_rules) |
|
self._new_rules = {} |
|
|
|
flag = 1 |
|
while flag: |
|
flag = 0 |
|
current_state = self.reduction_automaton.states['start'] |
|
for i, s in enumerate(word.letter_form_elm): |
|
next_state_name = current_state.transitions[s] |
|
next_state = self.reduction_automaton.states[next_state_name] |
|
if next_state.state_type == 'd': |
|
subst = next_state.rh_rule |
|
word = word.substituted_word(i - len(next_state_name) + 1, i+1, subst) |
|
flag = 1 |
|
break |
|
current_state = next_state |
|
return word |
|
|