switching to high quality piper tts and added label translations
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
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
|
||||
|
||||
# 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 = 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]
|
||||
# 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']
|
||||
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
|
||||
Reference in New Issue
Block a user