Source code for flipper.kernel.triangulation3


''' A module for representing triangulations of 3--manifolds.

Provides two classes: Tetrahedron and Triangulation3. '''

# We use right-handed tetrahedra, see SnapPy/headers/kernel_typedefs.h.
#
# We follow the orientation conventions in SnapPy/headers/kernel_typedefs.h L:154
# and SnapPy/kernel/peripheral_curves.c.

from itertools import combinations, product

import flipper

# Edge veerings:
VEERING_UNKNOWN, VEERING_LEFT, VEERING_RIGHT = 'Unknown', 'Left', 'Right'
# Peripheral curve types:
LONGITUDES, MERIDIANS, TEMPS = 0, 1, 2  # These are used for indexing so need to be integers.
PERIPHERAL_TYPES = [LONGITUDES, MERIDIANS, TEMPS]
# Tetrahedron geometry:
# This order was chosen so they appear ordered anti-clockwise from the cusp.
VERTICES_MEETING = {0: (1, 2, 3), 1: (0, 3, 2), 2: (0, 1, 3), 3: (0, 2, 1)}
# If you enter cusp a through side b then EXIT_CUSP_LEFT[(a, b)] is the side to your left.
EXIT_CUSP_LEFT = {
    (0, 1): 3, (0, 2): 1, (0, 3): 2,
    (1, 0): 2, (1, 2): 3, (1, 3): 0,
    (2, 0): 3, (2, 1): 0, (2, 3): 1,
    (3, 0): 1, (3, 1): 2, (3, 2): 0
    }
# Similarly EXIT_CUSP_RIGHT[(a, b)] is the side to your right.
EXIT_CUSP_RIGHT = {
    (0, 1): 2, (0, 2): 3, (0, 3): 1,
    (1, 0): 3, (1, 2): 0, (1, 3): 2,
    (2, 0): 1, (2, 1): 3, (2, 3): 0,
    (3, 0): 2, (3, 1): 0, (3, 2): 1
    }

[docs]class Tetrahedron: ''' This represents a tetrahedron. ''' def __init__(self, label): assert isinstance(label, flipper.IntegerType) self.label = label self.glued_to = [None] * 4 # Each entry is either: None or (Tetrahedron, permutation). self.cusp_indices = [-1, -1, -1, -1] self.peripheral_curves = [[[0, 0, 0, 0] for _ in range(4)] for _ in PERIPHERAL_TYPES] self.edge_labels = dict((vertex_pair, VEERING_UNKNOWN) for vertex_pair in combinations(range(4), r=2)) self.vertex_labels = [None, None, None, None] def __repr__(self): return str(self) def __str__(self): return '%s --> %s' % (self.label, ', '.join('%d: %s' % (x[0].label, x[1]) if x is not None else 'X' for x in self.glued_to)) # pylint: disable=unsubscriptable-object
[docs] def glue(self, side, target, permutation): ''' Glue the given side of this tetrahedron to target via the given permutation. You are not supposed to unglue tetrahedra as they automatically install a lot of additional structure.''' if self.glued_to[side] is None: assert self.glued_to[side] is None assert target.glued_to[permutation(side)] is None assert not permutation.is_even() self.glued_to[side] = (target, permutation) target.glued_to[permutation(side)] = (self, permutation.inverse()) # Move across the edge veerings too, is possible. for a, b in combinations(VERTICES_MEETING[side], 2): x, y = permutation(a), permutation(b) my_edge_veering = self.get_edge_label(a, b) his_edge_veering = target.get_edge_label(x, y) if my_edge_veering == VEERING_UNKNOWN and his_edge_veering != VEERING_UNKNOWN: self.set_edge_label(a, b, his_edge_veering) elif my_edge_veering != VEERING_UNKNOWN and his_edge_veering == VEERING_UNKNOWN: target.set_edge_label(x, y, my_edge_veering) else: assert (target, permutation) == self.glued_to[side]
[docs] def get_edge_label(self, a, b): ''' Return the label on edge (a) -- (b) of this tetrahedron. ''' a, b = min(a, b), max(a, b) return self.edge_labels[(a, b)]
[docs] def set_edge_label(self, a, b, value): ''' Set the label on edge (a) -- (b) of this tetrahedron. ''' a, b = min(a, b), max(a, b) self.edge_labels[(a, b)] = value
[docs] def snappy_string(self): ''' Return the SnapPy string describing this tetrahedron. ''' strn = '' strn += '%4d %4d %4d %4d \n' % tuple(tetrahedra.label for tetrahedra, gluing in self.glued_to) strn += ' %4s %4s %4s %4s\n' % tuple(gluing.compressed_string() for tetrahedra, gluing in self.glued_to) strn += '%4d %4d %4d %4d \n' % tuple(self.cusp_indices) strn += ' %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d\n' % tuple(cusp for meridian in self.peripheral_curves[MERIDIANS] for cusp in meridian) strn += ' 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n' strn += ' %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d %2d\n' % tuple(cusp for longitude in self.peripheral_curves[LONGITUDES] for cusp in longitude) strn += ' 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n' strn += ' 0.000000000000 0.000000000000\n' return strn
def __snappy__(self): return self.snappy_string()
[docs]class Triangulation3: ''' This represents triangulation, that is a collection of tetrahedra. ''' def __init__(self, num_tetrahedra): assert isinstance(num_tetrahedra, flipper.IntegerType) self.num_tetrahedra = num_tetrahedra self.tetrahedra = [Tetrahedron(i) for i in range(self.num_tetrahedra)] self.cusps = None self.num_cusps = -1 def __repr__(self): return str(self) def __str__(self): return '\n'.join(str(tetrahedron) for tetrahedron in self) def __iter__(self): return iter(self.tetrahedra)
[docs] def clear_temp_peripheral_structure(self): ''' Remove all TEMP peripheral curves. ''' for tetrahedron in self: tetrahedron.peripheral_curves[TEMPS] = [[0, 0, 0, 0] for _ in range(4)]
[docs] def is_closed(self): ''' Return if this triangulation is closed. ''' return all(tetrahedron.glued_to[side] is not None for tetrahedron in self for side in range(4))
[docs] def is_veering(self): ''' Return if this triangulation is veering. ''' # Hmmm, this only checks the local condition. This test appears to be # assuming that there are no edges labelled VEERING_UNKNOWN. for tetrahedron in self: for side in range(4): if tetrahedron.glued_to[side] is not None: target, permutation = tetrahedron.glued_to[side] for a, b in combinations(VERTICES_MEETING[side], 2): if tetrahedron.get_edge_label(a, b) != target.get_edge_label(permutation(a), permutation(b)): return False return True
[docs] def cusp_identification_map(self): ''' Return a dictionary pairing the sides of the peripheral triangles. ''' cusp_pairing = dict() for tetrahedron in self.tetrahedra: for side in range(4): for other in VERTICES_MEETING[side]: neighbour_tetrahedron, permutation = tetrahedron.glued_to[other] neighbour_side, neighbour_other = permutation(side), permutation(other) cusp_pairing[(tetrahedron, side, other)] = (neighbour_tetrahedron, neighbour_side, neighbour_other) return cusp_pairing
[docs] def assign_cusp_indices(self): ''' Assign the tetrahedra in this triangulation their cusp indices and return the list of corners in the same cusp. This triangulation must be closed. ''' assert self.is_closed() cusp_pairing = self.cusp_identification_map() self.cusps = [] remaining_vertices = list(product(self, range(4))) while remaining_vertices: # We do a depth first search to find all vertices in this cusp. new_vertex = remaining_vertices.pop() new_vertex_class = [new_vertex] # This is a stack of triangles that may still have consequences. to_explore = [new_vertex] while to_explore: current_tetrahedron, current_side = to_explore.pop() for side in VERTICES_MEETING[current_side]: neighbour_tetrahedron, neighbour_side, _ = cusp_pairing[(current_tetrahedron, current_side, side)] neighbour_vertex = neighbour_tetrahedron, neighbour_side if neighbour_vertex in remaining_vertices: to_explore.append(neighbour_vertex) new_vertex_class.append(neighbour_vertex) remaining_vertices.remove(neighbour_vertex) self.cusps.append(new_vertex_class) # Then iterate through each one assigning cusp indices. for index, vertex_class in enumerate(self.cusps): for tetrahedron, side in vertex_class: tetrahedron.cusp_indices[side] = index self.num_cusps = len(self.cusps) return self.cusps
[docs] def intersection_number(self, peripheral_type_a, peripheral_type_b, cusp=None): ''' Return the algebraic intersection number between the specified peripheral types. See SnapPy/kernel_code/intersection_numbers.c for more information. Convention:: ^ B | ---+---> A | | has intersection <A, B> := +1. ''' # This is the number of strands flowing from A to B. It is negative if they go in the opposite direction. flow = lambda A, B: 0 if (A < 0) == (B < 0) else (A if (A < 0) != (A < -B) else -B) if cusp is None: cusp = list(product(self.tetrahedra, range(4))) intersection_number = 0 for tetrahedron, side in cusp: periph_a = tetrahedron.peripheral_curves[peripheral_type_a] periph_b = tetrahedron.peripheral_curves[peripheral_type_b] # Count intersection numbers along edges. for other in VERTICES_MEETING[side]: # Only count crossings where periph_a is entering to avoid double counting. if periph_a[side][other] > 0: intersection_number -= periph_a[side][other] * periph_b[side][other] # and then within each face. for other in VERTICES_MEETING[side]: left = EXIT_CUSP_LEFT[(side, other)] right = EXIT_CUSP_RIGHT[(side, other)] # Even though this looks weird it is the correct pattern. intersection_number += flow(periph_a[side][other], periph_a[side][left]) * flow(periph_b[side][other], periph_b[side][left]) intersection_number += flow(periph_a[side][other], periph_a[side][right]) * flow(periph_b[side][other], periph_b[side][left]) return intersection_number
[docs] def install_peripheral_curves(self): ''' Assign a longitude and meridian to each cusp. Assumes (and checks) the link of each vertex is a torus. ''' # Install the cusp indices. self.assign_cusp_indices() cusp_pairing = self.cusp_identification_map() # Blank out the longitudes and meridians. for peripheral_type in [LONGITUDES, MERIDIANS]: for tetrahedron in self.tetrahedra: for side in range(4): for other in range(4): tetrahedron.peripheral_curves[peripheral_type][side][other] = 0 # Install a longitude and meridian on each cusp one at a time. for cusp in self.cusps: # Build a triangulation of the cusp neighbourhood. label = 0 edge_label_map = dict() for tetrahedron, side in cusp: for other in VERTICES_MEETING[side]: key = (tetrahedron, side, other) if key not in edge_label_map: edge_label_map[key] = label edge_label_map[cusp_pairing[key]] = ~label label += 1 edge_labels = [[edge_label_map[(tetrahedron, side, other)] for other in VERTICES_MEETING[side]] for tetrahedron, side in cusp] T = flipper.kernel.create_triangulation(edge_labels) if T.genus != 1: raise flipper.AssumptionError('Non torus vertex link.') # Get a basis for H_1. homology_basis_paths = T.homology_basis() # Install the longitude and meridian. # !?! Double check and optimise this. for peripheral_type in [LONGITUDES, MERIDIANS]: last = homology_basis_paths[peripheral_type][-1] # Find a starting point. for tetrahedron, side in cusp: for x in VERTICES_MEETING[side]: if edge_label_map[(tetrahedron, side, x)] == last: current_tetrahedron, current_side, arrive = tetrahedron, side, x break for other in homology_basis_paths[peripheral_type]: for a in VERTICES_MEETING[current_side]: if edge_label_map[(current_tetrahedron, current_side, a)] == ~other: leave = a current_tetrahedron.peripheral_curves[peripheral_type][current_side][arrive] += 1 current_tetrahedron.peripheral_curves[peripheral_type][current_side][leave] -= 1 next_tetrahedron, perm = current_tetrahedron.glued_to[leave] current_tetrahedron, current_side, arrive = next_tetrahedron, perm(current_side), perm(leave) break # Compute the algebraic intersection number between the longitude and meridian we just installed. # If the it is -1 then we need to reverse the direction of the meridian. # See SnapPy/kernel_code/intersection_numbers.c for how to do this. intersection_number = self.intersection_number(LONGITUDES, MERIDIANS, cusp) assert abs(intersection_number) == 1 # We might need to reverse the orientation of one of these to get the right sign. # If the intersection number is -1 then we need to reverse the direction of one of them (we choose the meridian). if intersection_number < 0: for tetrahedron, side in cusp: for other in VERTICES_MEETING[side]: tetrahedron.peripheral_curves[MERIDIANS][side][other] = -tetrahedron.peripheral_curves[MERIDIANS][side][other]
[docs] def slope(self): ''' Return the slope of the peripheral curve in TEMPS relative to the set meridians and longitudes. Assumes that the meridian and longitude on this cusp have been set. ''' longitude_intersection = self.intersection_number(LONGITUDES, TEMPS) meridian_intersection = self.intersection_number(MERIDIANS, TEMPS) # So the number of copies of the longitude and the meridian that we need to represent the path is: longitude_copies = -meridian_intersection meridian_copies = longitude_intersection return (meridian_copies, longitude_copies)
[docs] def snappy_string(self, name='flipper triangulation', fillings=None): ''' Return the SnapPy string describing this triangulation. If given, fillings is a list of slopes to perform Dehn filling along on each of the cusps. This triangulation must be closed. ''' assert fillings is None or isinstance(fillings, (list, tuple)) assert self.is_closed() if fillings is None: fillings = [(0, 0)] * self.num_cusps strn = '' strn += '% Triangulation\n' strn += '%s\n' % name strn += 'not_attempted 0.0\n' strn += 'oriented_manifold\n' strn += 'CS_unknown\n' strn += '\n' strn += '%d 0\n' % self.num_cusps for slope in fillings: strn += ' torus %16.12f %16.12f\n' % slope strn += '\n' strn += '%d\n' % self.num_tetrahedra for tetrahedra in self: strn += tetrahedra.snappy_string() + '\n' return strn