''' A module for representing triangulations along with laminations and mapping classes on them. '''
from collections.abc import Sequence
from queue import Queue
from random import choice
import re
import curver
IS_NAME = re.compile(r'[a-z]\w*$')
IS_INT = re.compile(r'[+-]?\d+')
TOKENS = re.compile(r'[+-]?\d+|\^|\(|\)|[\w_\.]+')
LEADING_DOTS = re.compile(r'^\.*')
[docs]class MappingClassGroup:
''' This represents a triangulation along with a collection of named mapping classes on it.
It can also be given a list of curves and arcs, in which case the twists and half-twists about
these are also added to the list of known mapping classes.
Most importantly this object can construct a mapping class from a string descriptor.
See self.mapping_class for additional information. '''
def __init__(self, pos_mapping_classes=None, curves=None, arcs=None):
if pos_mapping_classes is None: pos_mapping_classes = dict()
if curves is None: curves = dict()
if arcs is None: arcs = dict()
for name, arc in arcs.items():
assert name not in pos_mapping_classes
pos_mapping_classes[name] = arc.encode_halftwist()
for name, curve in curves.items():
assert name not in pos_mapping_classes
pos_mapping_classes[name] = curve.encode_twist()
assert pos_mapping_classes
self.triangulation = next(iter(pos_mapping_classes.values())).source_triangulation
self.zeta = self.triangulation.zeta
assert all(isinstance(key, str) for key in pos_mapping_classes)
assert all(IS_NAME.match(name) for name in pos_mapping_classes)
assert all(isinstance(pos_mapping_class, curver.kernel.MappingClass) for pos_mapping_class in pos_mapping_classes.values())
assert all(pos_mapping_class.source_triangulation == self.triangulation for pos_mapping_class in pos_mapping_classes.values())
self.pos_mapping_classes = pos_mapping_classes
self.neg_mapping_classes = dict((name.swapcase(), pos_mapping_class.inverse()) for name, pos_mapping_class in self.pos_mapping_classes.items())
self.mapping_classes = {**self.pos_mapping_classes, **self.neg_mapping_classes}
self.arcs = arcs
self.curves = curves
def __repr__(self):
return str(self)
def __str__(self):
pos_keys = sorted(self.pos_mapping_classes.keys(), key=curver.kernel.utilities.alphanum_key)
return f'Mapping class group < {", ".join(pos_keys)} > on {self.triangulation}'
def __eq__(self, other):
return self.triangulation == other.triangulation and self.mapping_classes == other.mapping_classes
def __getitem__(self, item):
return self.mapping_classes[item]
def __iter__(self):
return iter(self.mapping_classes)
[docs] def random_word(self, length, positive=True, negative=True, letters=None):
''' Return a random sequence of generators of the required length.
The letters to choose from can be specified or, alternatively, the set
of positive, negative or all (default) mapping classes can be used by using the
flags postive and negative. '''
if letters is None:
pos_keys = sorted(self.pos_mapping_classes.keys())
neg_keys = sorted(self.neg_mapping_classes.keys())
if positive and negative:
letters = pos_keys + neg_keys
elif positive and not negative:
letters = pos_keys
elif not positive and negative:
letters = neg_keys
else:
raise TypeError('At least one of positive and negative must be allowed')
return [choice(letters) for _ in range(length)]
[docs] def mapping_class(self, data, **kwargs):
''' Return a mapping class from data.
Data can either be:
* an iterable of mapping_class names,
* an integer specifying the word length of a random mapping class, or
* a string specifying the generators to be composed together.
The string supports '^' powers, parentheses and optional '.' separators.
Raises a ValueError if given a string that cannot be decomposed. '''
if isinstance(data, curver.IntegerType):
sequence = self.random_word(data, **kwargs)
elif isinstance(data, str):
SLP = curver.kernel.SLP # Shorter alias.
word = '(' + data + ')' # This ensures that the last token is a ')' and so avoids a special case.
word = re.sub(r'\s', '', word) # Remove whitespace.
MATCH_MCs = re.compile('|'.join(sorted(self.mapping_classes, key=len, reverse=True)))
def decompose(word):
''' Break a word into a list of words that match MATCH_MCs. '''
iterable = [x for subword in word.split('.') for x in MATCH_MCs.findall(subword)]
if sum(len(x) for x in iterable) + word.count('.') < len(word): # We were unable to decompose the entire word.
remaining = word
for index, item in enumerate(iterable):
remaining = LEADING_DOTS.sub('', remaining) # Remove leading dots.
if remaining.startswith(item):
remaining = remaining[len(item):]
else:
raise ValueError(f'After extracting {iterable[:index]}, the remaining {remaining} of {word} could not be decomposed')
remaining = LEADING_DOTS.sub('', remaining) # Remove leading dots.
raise ValueError(f'After extracting {iterable}, the remaining "{remaining}" of "{word}" could not be decomposed')
return iterable
stack = [[]]
tokens = TOKENS.findall(word) # Break word into tokens.
for index, token in enumerate(tokens):
if IS_INT.match(token):
pass
elif token == '^':
try:
power = int(tokens[index+1])
except (ValueError, IndexError) as err:
raise ValueError('^ not followed by a power') from err
stack[-1][-1] = stack[-1][-1] * abs(power)
if power < 0: stack[-1][-1] = stack[-1][-1].reverse().map(lambda x: x.swapcase())
elif token == '(':
stack[-1].append([])
stack.append(stack[-1][-1])
elif token == ')':
stack.pop()
if not stack:
raise ValueError('Unbalanced parentheses')
stack[-1][-1] = SLP.sum(stack[-1][-1])
else:
stack[-1].append(SLP(decompose(token)))
if len(stack) > 1:
raise ValueError('Unbalanced parentheses')
sequence = stack[-1][-1]
elif isinstance(data, Sequence):
sequence = data
else:
raise TypeError('No method for generating a Sequence from this type')
sequence = [item for letter in sequence for item in self.mapping_classes[letter]]
return curver.kernel.MappingClass(sequence) if sequence else self.triangulation.id_encoding()
def __call__(self, word, **kwargs):
''' A shortcut for self.mapping_class(...). '''
return self.mapping_class(word, **kwargs)
[docs] def lamination(self, geometric):
''' Return a new lamination on this surface assigning the specified weight to each edge. '''
return self.triangulation(geometric)
[docs] def cayley(self, generators, length):
''' Explore the Cayley graph for self with respect to the given generators.
Yield the canonical names for all elements with at most the given length as they are encountered. '''
identity = tuple()
id_mapping_class = self('')
convert = lambda X: (X[0], tuple(X[1].flatten())) # Since numpy.ndarrays are not hashable we need a converter.
elements = {convert((id_mapping_class.source_triangulation.as_lamination(), id_mapping_class.homology_matrix())): identity}
good = set([identity])
Q = Queue()
Q.put(((id_mapping_class.source_triangulation.as_lamination(), id_mapping_class.homology_matrix()), identity))
good = set([identity])
yield identity
while not Q.empty():
image, word = Q.get()
for generator in generators:
next_word = (generator,) + word
# Check all prefixes are good.
if any(next_word[:i] not in good for i in range(1, len(next_word))):
continue
lam, mat = image
action = self.mapping_classes[generator]
next_image = (action(lam), action.homology_matrix().dot(mat))
key = convert(next_image)
if key not in elements:
yield next_word
good.add(next_word)
elements[key] = next_word
if len(next_word) < length:
Q.put((next_image, next_word))