コード例 #1
0
ファイル: moves.py プロジェクト: saraedum/flipper
    def __init__(self, source_triangulation, target_triangulation, label_map):
        ''' This represents an isometry from source_triangulation to target_triangulation.
        
        It is given by a map taking each edge label of source_triangulation to a label of target_triangulation. '''

        assert isinstance(label_map, dict)

        super(Isometry, self).__init__(source_triangulation,
                                       target_triangulation)
        self.label_map = dict(label_map)

        self.flip_length = 0  # The number of flips needed to realise this move.

        # If we are missing any labels then use a depth first search to find the missing ones.
        # Hmmm, should always we do this just to check consistency?
        for i in self.source_triangulation.labels:
            if i not in self.label_map:
                raise flipper.AssumptionError(
                    'This label_map not defined on edge %d.' % i)

        self.index_map = dict((i, flipper.kernel.norm(self.label_map[i]))
                              for i in self.source_triangulation.indices)
        # Store the inverses too while we're at it.
        self.inverse_label_map = dict(
            (self.label_map[i], i) for i in self.source_triangulation.labels)
        self.inverse_index_map = dict(
            (i, flipper.kernel.norm(self.inverse_label_map[i]))
            for i in self.source_triangulation.indices)
        self.inverse_signs = dict((i, +1 if self.inverse_index_map[i] ==
                                   self.inverse_label_map[i] else -1)
                                  for i in self.source_triangulation.indices)
コード例 #2
0
ファイル: encoding.py プロジェクト: saraedum/flipper
    def is_conjugate_to(self, other):
        ''' Return if this mapping class is conjugate to other.
        
        It would also be straightforward to check if self^i ~~ other^j
        for some i, j.
        
        Both encodings must be mapping classes.
        
        Currently assumes that at least one mapping class is pseudo-Anosov. '''

        assert isinstance(other, Encoding)

        # Nielsen-Thurston type is a conjugacy invariant.
        if self.nielsen_thurston_type() != other.nielsen_thurston_type():
            return False

        if self.nielsen_thurston_type() == NT_TYPE_PERIODIC:
            if self.order() != other.order():
                return False

            # We could also use action on H_1(S) as a conjugacy invaraiant.

            raise flipper.AssumptionError('Mapping class is periodic.')
        elif self.nielsen_thurston_type() == NT_TYPE_REDUCIBLE:
            # There's more to do here.

            raise flipper.AssumptionError('Mapping class is reducible.')
        else:  # if self.nielsen_thurston_type() == NT_TYPE_PSEUDO_ANOSOV:
            # Two pseudo-Anosov mapping classes are conjugate if and only if
            # there canonical forms are cyclically conjugate via an isometry.
            f = self.canonical()
            g = other.canonical()
            # We should start by quickly checking some invariants.
            # For example they should have the same dilatation.
            if self.dilatation() != other.dilatation():
                return False

            for i in range(len(f)):
                # Conjugate around.
                f_cycled = f[i:] * f[:i]
                # g_cycled = g[i:] * g[:i]  # Could cycle g instead.
                for isom in f_cycled.source_triangulation.isometries_to(
                        g.source_triangulation):
                    if isom.encode() * f_cycled == g * isom.encode():
                        return True

            return False
コード例 #3
0
 def splitting_sequences(self, take_roots=False):
     ''' Return a list of splitting sequences associated to this mapping class.
     
     Assumes (and checks) that the mapping class is pseudo-Anosov.
     
     This encoding must be a mapping class. '''
     
     if self.is_periodic():  # Actually this test is redundant but it is faster to test it now.
         raise flipper.AssumptionError('Mapping class is not pseudo-Anosov.')
     
     dilatation, lamination = self.pml_fixedpoint()
     try:
         splittings = lamination.splitting_sequences(dilatation=None if take_roots else dilatation)
     except flipper.AssumptionError as err:  # Lamination is not filling.
         raise flipper.AssumptionError('Mapping class is not pseudo-Anosov.') from err
     
     return splittings
コード例 #4
0
ファイル: encoding.py プロジェクト: saraedum/flipper
    def pml_fixedpoint(self):
        ''' Return a rescaling constant and projectively invariant lamination.
        
        Assumes that the mapping class is pseudo-Anosov.
        
        To find this we start with a curve on the surface and repeatedly apply the map.
        We then use :func:`~flipper.kernel.matrix.Matrix.directed_eigenvector()` to find the nearby projective fixed point.
        The work of Margalit--Strenner--Yurtas says that if we apply self too many times then self is not pseudo-Anosov.
        
        This encoding must be a mapping class. '''

        assert self.is_mapping_class()

        # We start with a fast test for periodicity.
        # This isn't needed but it means that if we ever discover that self is not pA then it must be reducible.
        if self.is_periodic():
            raise flipper.AssumptionError('Mapping class is periodic.')

        for curve in self.source_triangulation.key_curves():
            # The result of Margalit--Strenner--Yurtas say that this is a sufficient number of iterations to find a fixed point.
            # See https://www.youtube.com/watch?v=-GO0AvUGjH4
            for _ in range(36 *
                           self.source_triangulation.euler_characteristic**2):
                curve = self(curve)
                try:
                    action_matrix, condition_matrix = self.applied_geometric(
                        curve)
                    eigenvalue, eigenvector = action_matrix.directed_eigenvector(
                        condition_matrix)

                    invariant_lamination = self.source_triangulation(
                        eigenvector.tolist())
                    if invariant_lamination.is_empty(
                    ):  # But it might have been entirely peripheral.
                        raise flipper.ComputationError(
                            'No interesting eigenvectors in cell.')

                    return eigenvalue, invariant_lamination
                except flipper.ComputationError:
                    pass

        raise flipper.AssumptionError('Mapping class is reducible.')
コード例 #5
0
    def geometric_intersection(self, lamination):
        ''' Return the geometric intersection number between this lamination and the given one.
        
        Assumes (and checks) that this is a twistable lamination. '''

        assert isinstance(lamination, Lamination)
        assert lamination.triangulation == self.triangulation

        if not self.is_twistable():
            raise flipper.AssumptionError(
                'Can only compute geometric intersection number between a twistable curve and a lamination.'
            )

        conjugator = self.conjugate_short()

        short = conjugator(self)
        short_lamination = conjugator(lamination)

        triangulation = short.triangulation
        e1, e2 = [
            edge_index for edge_index in triangulation.indices
            if short(edge_index) > 0
        ]
        # We might need to swap these edge indices so we have a good frame of reference.
        if triangulation.corner_of_edge(e1).indices[2] != e2: e1, e2 = e2, e1

        a, b, c, d = triangulation.square_about_edge(e1)
        e = e1

        x = (short_lamination(a) + short_lamination(b) -
             short_lamination(e)) // 2
        y = (short_lamination(b) + short_lamination(e) -
             short_lamination(a)) // 2
        z = (short_lamination(e) + short_lamination(a) -
             short_lamination(b)) // 2
        x2 = (short_lamination(c) + short_lamination(d) -
              short_lamination(e)) // 2
        y2 = (short_lamination(d) + short_lamination(e) -
              short_lamination(c)) // 2
        z2 = (short_lamination(e) + short_lamination(c) -
              short_lamination(d)) // 2

        intersection_number = short_lamination(a) - 2 * min(x, y2, z)

        # Check that the other formula gives the same answer.
        assert intersection_number == short_lamination(c) - 2 * min(x2, y, z2)

        return intersection_number
コード例 #6
0
    def splitting_sequences(self, dilatation=None, maxlen=None):
        ''' Return a list of splitting sequence associated to this lamination.
        
        This is the encoding obtained by flipping edges to repeatedly split
        the branches of the corresponding train track with maximal weight
        until you reach a projectively periodic sequence (with the required
        dilatation if given).
        
        Assumes that this lamination is projectively invariant under some mapping class.
        Assumes (and checks) that this lamination is filling.
        
        Each entry of self.geometric must be an Integer or a RealAlgebraic (over
        the same RealNumberField). '''

        # In this method we use Lamination.projective_hash to store the laminations
        # we encounter efficiently and so avoid a quadratic algorithm. This currently
        # only ever uses the default precision HASH_DENOMINATOR. At some point this
        # should change dynamically depending on the algebraic numbers involved in
        # this lamination.

        assert all(
            isinstance(entry, (flipper.IntegerType,
                               flipper.kernel.RealAlgebraic))
            for entry in self)
        assert len(
            set(entry.field for entry in self
                if isinstance(entry, flipper.kernel.RealAlgebraic))) <= 1

        # Check if the lamination is obviously non-filling.
        if all(isinstance(entry, flipper.IntegerType) for entry in self):
            raise flipper.AssumptionError('Lamination is not filling.')

        # This assumes that no vertices are filled. We can probably get rid of this
        # if we get the collapse_trivial_weights preprocessing below working.
        if any(entry == 0 for entry in self):
            raise flipper.AssumptionError('Lamination is not filling.')

        # We should call lamination.collapse_trivial_weight(flip_index) on each
        # weight 0 edge and rely on it to either collapse the edge or raise the
        # approprate error.

        lamination = self
        encodings = []
        # Puncture all the triangles where the lamination is a tripod.
        E = lamination.puncture_tripods()
        lamination = E(lamination)
        encodings.append(E)

        # This is a dict taking the hash of each lamination to the index where we saw it.
        target = lamination.projective_hash()
        seen = dict()
        # We then want a second dictionary taking indices where laminations occur back to
        # the lamination. This can use a lot of memory however as Tao's K(S) can grow very
        # large when the surface has high genus. To get around this we note that we will only
        # ever lookup laminations that occur in the last maxlen steps, which is at least the
        # periodic length of this sequence. So we create a bunch of dictionaries that we
        # cycle through. This way their union will always contain the last maxlen indices
        # and will only contain at most maxlen * NUM_LAM_BLOCKS / (NUM_LAM_BLOCKS - 1) laminations.
        NUM_LAM_BLOCKS = 5
        laminations = [dict() for i in range(NUM_LAM_BLOCKS)]
        current_block = 0  # This records which dictionary we are currently filling.

        seen[target] = [1]
        laminations[current_block][1] = lamination

        # We'll store the edge weights in a maximal heap using heapq. This allows us to quickly find the maximal weight edges.
        flip_first = lambda x: (
            -x[0], x[1]
        )  # A small function allowing us to use Pythons defaul min heap as a max heap.
        weights_heap = [
            flip_first((weight, index))
            for index, weight in enumerate(lamination)
        ]
        heapq.heapify(weights_heap)
        # Get the index of the largest weight edge.
        flip_weight, flip_index = flip_first(heapq.heappop(weights_heap))
        while True:
            max_weight = flip_weight
            # Flip all edges weight max_weight. As the heap outputs these in sorted order,
            # we do this by popping an element and flipping it until we reach one of
            # weight < max_weight.
            while flip_weight == max_weight:
                # Do the flip.
                E = lamination.triangulation.encode_flip(flip_index)
                lamination = E(lamination)
                # Record information about the flip.
                encodings.append(E)

                # Check if we have created any edges of weight 0. Of course it is enough to just check flip_index.
                if lamination(flip_index) == 0:
                    try:
                        # If this fails it's because the lamination isn't filling.
                        lamination, E = lamination.collapse_trivial_weight(
                            flip_index)
                        encodings.append(E)
                        # Need to rebuild the heap as indices no longer correspond.
                        weights_heap = [
                            flip_first((weight, index))
                            for index, weight in enumerate(lamination)
                        ]
                        heapq.heapify(weights_heap)
                    except flipper.AssumptionError:
                        raise flipper.AssumptionError(
                            'Lamination is not filling.')
                else:
                    # Add the new edge weight back into the heap.
                    heapq.heappush(
                        weights_heap,
                        flip_first((lamination(flip_index), flip_index)))

                # Get the next largest edge.
                flip_weight, flip_index = flip_first(
                    heapq.heappop(weights_heap))

            # print(len(encodings))

            # Record this lamination in the dictionary of seen laminations.
            # To cut down on memory usage we will only retain the last maxlen laminations
            # at any given point.
            if maxlen is not None and len(laminations[current_block]
                                          ) > maxlen // (NUM_LAM_BLOCKS - 1):
                # Move to the (cyclically) next block and reset it with just this lamination.
                current_block = (current_block + 1) % NUM_LAM_BLOCKS
                laminations[current_block] = {len(encodings): lamination}
            else:
                laminations[current_block][len(encodings)] = lamination

            # Check if lamination now (projectively) matches a lamination we've already seen.
            target = lamination.projective_hash()
            if target in seen:
                # print(seen[target])
                for index in seen[target]:
                    for i in range(NUM_LAM_BLOCKS):
                        if index in laminations[i]:
                            old_lamination = laminations[i][index]
                            break
                    else:
                        # This index has moved out of the lamination dictionaries and so is
                        # too old to be the start point of the periodic cycle.
                        continue

                    # In the next block we have a lot of tests to do. We'll do these in
                    # order of difficulty of computation. For example, computing
                    # projective_isometries is slow; so we'll leave that to last to give
                    # us the best chance that a faster test failing will allow us to
                    # skip it.
                    if dilatation is None or old_lamination.weight(
                    ) >= dilatation * lamination.weight():
                        isometries = lamination.all_projective_isometries(
                            old_lamination)
                        if isometries:
                            assert dilatation is None or old_lamination.weight(
                            ) == dilatation * lamination.weight()
                            # print('!!', index)

                            encoding = flipper.kernel.Encoding([
                                move for item in reversed(encodings)
                                for move in item
                            ])
                            return flipper.kernel.SplittingSequences(
                                encoding, isometries, index, dilatation,
                                old_lamination)
                    else:
                        # dilatation is not None and:
                        #   old_lamination.weight() < dilatation * lamination.weight():
                        # Note that the weight of laminations is strictly decreasing and the
                        # indices of seen[target] are increasing. Thus if we are in this case
                        # then the same inequality holds for every later index in seen[target].
                        # Hence we may break out.
                        break
                seen[target].append(len(encodings))
            else:
                # Start a new class containing this lamination.
                seen[target] = [len(encodings)]

        raise RuntimeError('Unreachable code.')
コード例 #7
0
    def collapse_trivial_weight(self, edge_index):
        ''' Return this lamination on the triangulation obtained by collapsing edge edge_index
        and an encoding which is at least algebraically correct.
        
        Assumes (and checks) that:
            - edge_index is a flippable edge,
            - self.triangulation is not S_{0,3},
            - the given edge does not connect between two unfilled vertices, and
            - edge_index is the only edge of weight 0. '''

        if self(edge_index) != 0:
            raise flipper.AssumptionError(
                'Lamination does not have weight 0 on edge to collapse.')

        # This relies on knowing how squares are returned.
        a, b, c, d = self.triangulation.square_about_edge(
            edge_index)  # Get the square about it.
        e = self.triangulation.edge_lookup[edge_index]

        # We'll first deal with some bad cases that con occur when some of the sides of the square are in fact the same.
        if a == ~b or c == ~d:
            # This implies that self(a) (respectively self(c)) == 0.
            raise flipper.AssumptionError('Additional weightless edge.')

        # There is at most one duplicated pair.
        if a == ~d and b == ~c:
            # This implies that the underlying surface is S_{0,3}.
            raise flipper.AssumptionError('Underlying surface is S_{0,3}.')

        if a == ~c and a == ~d:
            # This implies the underlying surface is S_{1,1}. As there is
            # only one vertex, both endpoints of this edge must be labelled 0.
            raise flipper.AssumptionError('Lamination is not filling.')

        # Now the only remaining possibilities are:
        #   a == ~c, b == ~d, a == ~d, b == ~c, or no relations.

        # Get the two vertices. We will ensure that if there is a filled vertex then bad_vertex is filled.
        bad_vertex, good_vertex = [
            e.source_vertex, e.target_vertex
        ] if e.source_vertex.filled else [e.target_vertex, e.source_vertex]

        # We will quickly check some assumptions. If we collapse together two
        # unfilled vertices (or a vertex with itself) then there is a loop
        # disjoint to this lamination and so this is not filling.
        if (not good_vertex.filled
                and not bad_vertex.filled) or good_vertex == bad_vertex:
            raise flipper.AssumptionError('Lamination is not filling.')

        # Figure out how the vertices should be mapped.
        vertex_map = dict()
        # Every vertex, except the bad_vertex, goes to an exact copy of itself.
        for vertex in self.triangulation.vertices:
            if vertex != bad_vertex:
                # Although we will tweak the vertex labels slightly to ensure that they are still labelled 0, 1, ...
                new_label = vertex.label if vertex.label < bad_vertex.label else vertex.label - 1
                vertex_map[vertex] = flipper.kernel.Vertex(
                    new_label, filled=vertex.filled)
        # The bad_vertex goes to the same place as the good_vertex.
        vertex_map[bad_vertex] = vertex_map[good_vertex]

        # Now figure out how the edges should be mapped.
        edge_count = 0
        edge_map = dict()
        # Far away edges should go to an exact copy of themselves.
        for edge in self.triangulation.positive_edges:
            if edge not in [a, b, c, d, e, ~a, ~b, ~c, ~d, ~e]:
                edge_map[edge] = flipper.kernel.Edge(
                    vertex_map[edge.source_vertex],
                    vertex_map[edge.target_vertex], edge_count)
                edge_map[~edge] = ~edge_map[edge]
                edge_count += 1

        if a == ~c:  # Collapsing an annulus.
            edge_map[~b] = flipper.kernel.Edge(vertex_map[b.target_vertex],
                                               vertex_map[b.source_vertex],
                                               edge_count)
            edge_map[~d] = ~edge_map[~b]
            edge_count += 1
        elif b == ~d:  # An annulus in the other direction.
            edge_map[~a] = flipper.kernel.Edge(vertex_map[a.target_vertex],
                                               vertex_map[a.source_vertex],
                                               edge_count)
            edge_map[~c] = ~edge_map[~a]
            edge_count += 1
        elif a == ~d:  # Collapsing a bigon.
            edge_map[~b] = flipper.kernel.Edge(vertex_map[b.target_vertex],
                                               vertex_map[b.source_vertex],
                                               edge_count)
            edge_map[~c] = ~edge_map[~b]
            edge_count += 1
        elif b == ~c:  # A bigon in the other direction.
            edge_map[~a] = flipper.kernel.Edge(vertex_map[a.target_vertex],
                                               vertex_map[a.source_vertex],
                                               edge_count)
            edge_map[~d] = ~edge_map[~a]
            edge_count += 1
        else:  # No identification.
            edge_map[~a] = flipper.kernel.Edge(vertex_map[a.target_vertex],
                                               vertex_map[a.source_vertex],
                                               edge_count)
            edge_map[~b] = ~edge_map[~a]
            edge_count += 1
            edge_map[~c] = flipper.kernel.Edge(vertex_map[c.target_vertex],
                                               vertex_map[c.source_vertex],
                                               edge_count)
            edge_map[~d] = ~edge_map[~c]
            edge_count += 1

        triples = [[edge_map[edge] for edge in triangle]
                   for triangle in self.triangulation
                   if e not in triangle and ~e not in triangle]
        new_triangles = [flipper.kernel.Triangle(triple) for triple in triples]
        T = flipper.kernel.Triangulation(new_triangles)

        bad_edges = [
            a, b, c, d, e, ~e
        ]  # These are the edges for which edge_map is not defined.
        geometric = [[
            self(edge) for edge in self.triangulation.edges
            if edge not in bad_edges and edge_map[edge].index == i
        ][0] for i in range(edge_count)]
        algebraic = [0] * edge_count
        lamination = Lamination(T, geometric, algebraic)

        # Now compute the encoding describing this. We will only bother to get the algebraic part correct
        # as the geometric part is not a PL function with only finitely many pieces.
        matrix = flipper.kernel.id_matrix(self.zeta)
        for edge in self.triangulation.edges:
            if edge != e and edge != ~e and edge.source_vertex == bad_vertex and edge.target_vertex != bad_vertex:
                matrix = matrix.elementary(
                    edge.index, edge_index, +1 if edge.is_positive() !=
                    (e.source_vertex == bad_vertex) else -1)

        matrix2 = flipper.kernel.zero_matrix(self.zeta, edge_count)
        for edge in self.triangulation.edges:
            if edge not in bad_edges:
                target_edge = edge_map[edge]
                if not any(matrix2[target_edge.index]):
                    matrix2[target_edge.index][
                        edge.index] = +1 if edge.is_positive(
                        ) == target_edge.is_positive() else -1

        algebraic_matrix = matrix2 * matrix
        geometric_matrix = flipper.kernel.zero_matrix(self.zeta, edge_count)

        encoding = flipper.kernel.LinearTransformation(
            self.triangulation, T, geometric_matrix,
            algebraic_matrix).encode()

        return lamination, encoding
コード例 #8
0
 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]