123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- 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):
- from collections import deque
- self.group = group
- self.alphabet = group.generators
- self._is_confluent = None
- # these values are taken from [2]
- self.maxeqns = 32767 # max rules
- self.tidyint = 100 # rules before tidying
- # _max_exceeded is True if maxeqns is exceeded
- # at any point
- self._max_exceeded = False
- # Reduction automaton
- self.reduction_automaton = None
- self._new_rules = {}
- # dictionary of reductions
- self.rules = {}
- self.rules_cache = deque([], 50)
- self._init_rules()
- # All the transition symbols in the automaton
- generators = list(self.alphabet)
- generators += [gen**-1 for gen in generators]
- # Create a finite state machine as an instance of the StateMachine object
- 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
- # Add the newly added rule to the `new_rules` dictionary.
- 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
- # The following is the equivalent of checking
- # s1 for overlaps with the implicit reductions
- # {g*g**-1 -> <identity>} and {g**-1*g -> <identity>}
- # for any generator g without installing the
- # redundant rules that would result from processing
- # the overlaps. See [1], Section 3 for details.
- 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)
- # overlaps on the right
- 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)
- # overlaps on the left
- 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:
- # system not confluent
- 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 could be i+1 to not
- # check each pair twice but lhs
- # is extended in the loop and the new
- # elements have to be checked with the
- # preceding ones. there is probably a better way
- # to handle this
- 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:
- # too many rules were added so the process
- # couldn't complete
- return self._is_confluent
- if added > self.tidyint and not check:
- # tidy up
- r, a = self._remove_redundancies(changes=True)
- added = 0
- if r:
- # reset i since some elements were removed
- 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:
- # r1 was removed as redundant
- 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}
- # the following is essentially `eliminate_words()` code from the
- # `FreeGroupElement` class, the only difference being the first
- # "if" statement
- 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 variables
- automaton_alphabet = []
- proper_prefixes = {}
- # compute the inverses of all the new rules added
- all_rules = rules
- inverse_rules = self._compute_inverse_rules(all_rules)
- all_rules.update(inverse_rules)
- # Keep track of the accept_states.
- accept_states = []
- for rule in all_rules:
- # The symbols present in the new rules are the symbols to be verified at each state.
- # computes the automaton_alphabet, as the transitions solely depend upon the new states.
- automaton_alphabet += rule.letter_form_elm
- # Compute the proper prefixes for every rule.
- proper_prefixes[rule] = []
- letter_word_array = [s for s in 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]
- # Add accept states.
- 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
- # Check for overlaps between dead and accept states.
- 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)
- # Add dead states
- 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)
- # Add new transitions for every state.
- for state in self.reduction_automaton.states:
- current_state_name = state
- current_state_type = self.reduction_automaton.states[state].state_type
- # Transitions will be modified only when suffixes of the current_state
- # belongs to the proper_prefixes of the new rules.
- # The rest are ignored if they cannot lead to a dead state after a finite number of transisitons.
- 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':
- # Check if the transition to any new state in possible.
- 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)
- # Add transitions for new states. All symbols used in the automaton are considered here.
- # Ignore this if `reduction_automaton.automaton_alphabet` = `automaton_alphabet`.
- 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.
- '''
- # Modify the automaton if new rules are found.
- 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']
- word_array = [s for s in word.letter_form_elm]
- for i in range (0, len(word_array)):
- next_state_name = current_state.transitions[word_array[i]]
- 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
|