In this notebook we work with the infinite family of strongly invertible hyperbolic L-space knots K_n whose Watson invariant is thick. The first cell loads the necessary code.

In [1]:
import snappy
def FKP_bound(vol_surgery_link,lower_volume_bound):
    '''
    Returns the Futer-Kalfagianni-Purcell bound, that gives a lower bound on the normalized 
    length of a slope to ensure that a filling with such a slope on a link with volume = vol_surgery_link 
    has volume at least lower_volume_bound.
    '''
    return 2*3.14159/sqrt(1-(lower_volume_bound/vol_surgery_link)^(2/3))+0.1

def Bound(systole):
    '''
    Takes as input a systole and computes the bound from FPS19.
    '''
    B= math.sqrt(2*math.pi/systole +58)
    if 10.1>B:
        return 10.11
    else:
        return B+0.01

def better_systole(M,index=10):
    '''
    Computes the systole of a manifold. 
    It returns 'unclear' if SnapPy cannot compute the systole.
    '''
    w=False
    randomizeCount=0
    try:
        systole=M.dual_curves()[0].complete_length.real() 
        w=True
    except ValueError:
        pass
    except RuntimeError:
        pass
    except snappy.SnapPeaFatalError:
        pass
    except IndexError:
        pass
    if w==False:
        while randomizeCount<index and w==False:
            M.randomize()
            randomizeCount=randomizeCount+1
            try:
                systole=M.length_spectrum()[0].length.real() 
                w=True
            except ValueError:
                pass
            except RuntimeError:
                pass
            except snappy.SnapPeaFatalError:
                pass
            except IndexError:
                pass
    if w==True:
        return systole
    if w==False:
        return 'unclear'
    
#
# dunfield.py
#

import regina
import snappy
import re
import networkx as nx

# This code has its origins in Dunfield's paper on exceptional slopes:

# https://dataverse.harvard.edu/file.xhtml?persistentId=doi:10.7910/DVN/6WNVG0/0X6FYV&version=1.0

# Some hacking has taken place. Most significantly, we have updated a lot of commands
# to interact with Regina 7.0 rather than 6.0.

################

# Code to prove a manifold is hyperbolic.  


def all_positive(manifold):
    return manifold.solution_type() == 'all tetrahedra positively oriented'

def find_positive_triangulation(manifold, tries = 3, verbose = 2):

    M = snappy.Manifold(manifold)
    for i in range(tries):
        if all_positive(M):
            return M
        else:
            M.randomize()
    try: # this fails in verify_mt_action, sometimes
        for d in M.dual_curves():
            X = M.drill(d)
            X = X.filled_triangulation()
            X.dehn_fill((1,0),-1)
            for i in range(tries):
                if all_positive(X):
                    return X
                X.randomize()
    except snappy.SnapPeaFatalError: 
        print(M, "we failed to drill and fill... why?")
        raise
    
    # In the closed case, here is another trick.

    if all(not c for c in M.cusp_info('is_complete')):
        for i in range(tries):
            # Drills out a random edge
            X = M.__class__(M.filled_triangulation())
            if all_positive(X):
                return X
            M.randomize()

    # everything has failed

    return None
            
def verify_hyperbolic_basic(manifold, tries = 3, verbose = 2):

    M = find_positive_triangulation(manifold, tries = tries, verbose = verbose)
    if M is not None:
        for i in range(tries):
            prec = 53*(2**i) # this used to go to 1000.  Now it goes to 11. 
            try:
                
                if M.verify_hyperbolicity(bits_prec=prec)[0]:
                    
                    return True
            except RuntimeError:
                
                print(M, 'Treating exception in verify code as a failure')
    return False

def verify_hyperbolic_basic_with_volume(manifold, tries = 3, verbose = 2):
    M = find_positive_triangulation(manifold, tries = tries, verbose = verbose)
    if M is not None:
        for i in range(tries):
            prec = 53*(2**i) # this used to go to 1000.  Now it goes to 11. 
            try:
                if M.verify_hyperbolicity(bits_prec = prec)[0]:
                    return (True, M.volume(verified = True, bits_prec = prec))
            except RuntimeError:
                print(M, 'Treating exception in verify code as a failure')
    return (False, None)

def is_hyperbolic(manifold, tries = 10, verbose = 2):
    

    if verify_hyperbolic_basic(manifold, tries = tries, verbose=verbose):
        return True
    else:
        for d in range(2, min(tries, 8)):
            for C in manifold.covers(d):
                if verify_hyperbolic_basic(C, tries = tries, verbose=verbose):
                    return True
    return False

                
def is_hyperbolic_with_volume(manifold, tries = 10, verbose = 2):
    if verbose > 12:
        print(manifold, "entering is_hyperbolic_with_volume")

    is_hyp, vol = verify_hyperbolic_basic_with_volume(manifold, tries = tries, verbose = verbose)
        
    if is_hyp:
        if verbose > 12:
            print(manifold, "verify_hyperbolic_basic_with_volume worked.")
        return (is_hyp, vol)
    else:
        for d in range(2, min(tries, 8)):
            for C in manifold.covers(d):
                if verbose > 12:
                    print("trying cover of degree", d)
                is_hyp, vol = verify_hyperbolic_basic_with_volume(C, tries = tries, verbose = verbose)
                if is_hyp:
                    return (is_hyp, vol/d)
    return (False, None)

                
###########################

# Provides functions for working with Regina (with a little
# help from SnapPy) to:

# 1. Give a standard name ("identify") manifolds, especially Seifert and
#    graph manifolds.

# 2. Find essential tori.

# 3. Try to compute the JSJ decomposition.

def appears_hyperbolic(M):
    acceptable = ['all tetrahedra positively oriented',
                  'contains negatively oriented tetrahedra']
    return M.solution_type() in acceptable and M.volume() > 0

'''
The following seems to be unnecessary now that Regina 7.0 returns lists

def children(packet):
    child = packet.firstChild()
    while child:
        yield child
        child = child.nextSibling()
'''

def to_regina(data):
    if hasattr(data, '_to_string'):
        data = data._to_string()
    if isinstance(data, str):
        if data.find('(') > -1:
            data = closed_isosigs(data)[0]
        return regina.Triangulation3(data)
    assert isinstance(data, regina.Triangulation3)
    return data

def extract_vector(surface):
    """
    Extract the raw vector of the (almost) normal surface in Regina's
    NS_STANDARD coordinate system.
    """
    S = surface
    T = S.triangulation()
    n = T.countTetrahedra()
    ans = []
    for i in range(n):
        for j in range(4):
            ans.append(S.triangles(i, j))
        for j in range(3):
            ans.append(S.quads(i, j))
    A = regina.NormalSurface(T, regina.NS_STANDARD, ans)
    assert A == S
    return ans

def haken_sum(S1, S2):
    T = S1.triangulation()
    assert S1.locallyCompatible(S2)
    v1, v2 = extract_vector(S1), extract_vector(S2)
    sum_vec = [x1 + x2 for x1, x2 in zip(v1, v2)]
    A = regina.NormalSurface(T, regina.NS_STANDARD, sum_vec)
    assert S1.locallyCompatible(A) and S2.locallyCompatible(A)
    assert S1.eulerChar() + S2.eulerChar() == A.eulerChar()
    return A

def census_lookup(regina_tri):
    """
    Should the input triangulation be in Regina's census, return the
    name of the manifold, dropping the triangulation number.
    """
    hits = regina.Census.lookup(regina_tri)
    # The following is a Regina 6.0 command, deprecated in Regina 7.0
    # hit = hits.first()
    if len(hits) == 0:
        return None
    hit = hits[0]
    if hit is not None:
        name = hit.name()
        match = re.search(r"(.*) : #\d+$", name)
        if match:
            return match.group(1)
        else:
            return match

def standard_lookup(regina_tri):
    match = regina.StandardTriangulation.recognise(regina_tri)
    if match:
        return match.manifold()

def closed_isosigs(snappy_manifold, tries = 20, max_tets = 50):
    """
    Generate a slew of 1-vertex triangulations of a closed manifold
    using SnapPy.
    
    >>> M = snappy.Manifold('m004(1,2)')
    >>> len(closed_isosigs(M, tries=5)) > 0
    True
    """
    M = snappy.Manifold(snappy_manifold)
    assert set(M.cusp_info('complete?')) == {False}
    surgery_descriptions = [snappy.Manifold(M)]

    try:
        for curve in M.dual_curves():
            N = M.drill(curve)
            N.dehn_fill((1,0), 1)
            surgery_descriptions.append(N.filled_triangulation([0]))
    except snappy.SnapPeaFatalError:
        pass

    if len(surgery_descriptions) == 1:
        # Try again, but unfill the cusp first to try to find more
        # dual curves.
        try:
            filling = M.cusp_info(0).filling
            N = snappy.Manifold(M)
            N.dehn_fill((0, 0), 0)
            N.randomize()
            for curve in N.dual_curves():
                D = N.drill(curve)
                D.dehn_fill([filling, (1,0)])
                surgery_descriptions.append(D.filled_triangulation([0]))
        except snappy.SnapPeaFatalError:
            pass

    ans = set()
    for N in surgery_descriptions:
        for i in range(tries):
            T = N.filled_triangulation()
            if T._num_fake_cusps() == 1:
                n = T.num_tetrahedra()
                if n <= max_tets:
                    ans.add((n, T.triangulation_isosig(decorated=False)))
            N.randomize()

    return [iso for (n, iso) in sorted(ans)]

def best_match(matches):
    """
    Prioritize the most concise description that Regina provides to
    try to avoid things like the Seifert fibered space of a node being
    a solid torus or having several nodes that can be condensed into a
    single Seifert fibered piece.
    """
    
    def score(m):
        if isinstance(m, regina.SFSpace):
            s = 0
        elif isinstance(m, regina.GraphLoop):
            s = 1
        elif isinstance(m, regina.GraphPair):
            s = 2
        elif isinstance(m, regina.GraphTriple):
            s = 3
        elif m is None:
            s = 10000
        else:
            s = 4
        return (s, str(m))
    return min(matches, key=score)


def identify_with_torus_boundary(regina_tri):
    """
    Use the combined power of Regina and SnapPy to try to give a name
    to the input manifold.
    """
    
    kind, name = "unknown", None
    
    P = regina.Triangulation3(regina_tri) # a clone of the triangulation
    P.finiteToIdeal()
    P.intelligentSimplify()
    M = snappy.Manifold(P.isoSig())
    M.simplify()
    if appears_hyperbolic(M):
        for i in range(100):
            if M.solution_type() == 'all tetrahedra positively oriented':
                break
            M.randomize()
        
        if not M.verify_hyperbolicity(bits_prec=100):
            raise RuntimeError('Cannot prove hyperbolicity for ' +
                               M.triangulation_isosig())
        kind = 'hyperbolic'
        ids = M.identify()
        if ids:
            name = ids[0].name()
    else:
        match = standard_lookup(regina_tri)
        if match is None:
            Q = regina.Triangulation3(P)
            Q.idealToFinite()
            Q.intelligentSimplify()
            match = standard_lookup(Q)
        if match is not None:
            kind = match.__class__.__name__
            name = str(match)
        else:
            name = P.isoSig()
    return kind, name


def is_toroidal(regina_tri):
    """
    Checks for essential tori and returns the pieces of the
    associated partial JSJ decomposition.
    
    >>> T = to_regina('hLALAkbccfefgglpkusufk')  # m004(4,1)
    >>> is_toroidal(T)[0]
    True
    >>> T = to_regina('hvLAQkcdfegfggjwajpmpw')  # m004(0,1)
    >>> is_toroidal(T)[0]
    True
    >>> T = to_regina('nLLLLMLPQkcdgfihjlmmlkmlhshnrvaqtpsfnf')  # 5_2(10,1)
    >>> T.isHaken()
    True
    >>> is_toroidal(T)[0]
    False

    Note: currently checks all fundamental normal tori; possibly
    the theory lets one just check *vertex* normal tori.
    """
    T = regina_tri
    assert T.isZeroEfficient()
    # got rid of regina.NormalSurfaces.enumerate
    surfaces = regina.NormalSurfaces(T, regina.NS_QUAD, regina.NS_FUNDAMENTAL)
    for i in range(surfaces.size()):
        S = surfaces.surface(i)
        if S.eulerChar() == 0:
            if not S.isOrientable():
                S = S.doubleSurface()
            assert S.isOrientable()
            X = S.cutAlong()
            X.intelligentSimplify()
            # The following is Regina 6.0 code, deprecated in version 7.0
            # X.splitIntoComponents()
            # pieces = list(children(X))
            pieces = X.triangulateComponents()
            if all(not C.hasCompressingDisc() for C in pieces):
                ids = [identify_with_torus_boundary(C) for C in pieces]
                return (True, sorted(ids))
                
    return (False, None)


def decompose_along_tori(regina_tri):
    """
    First, finds all essential normal tori in the manifold associated
    with fundamental normal surfaces.  Then takes a maximal disjoint
    collection of these tori, namely the one with the fewest tori
    involved, and cuts the manifold open along it.  It tries to
    identify the pieces, removing any (torus x I) components. 

    Returns: (has essential torus, list of pieces)

    Note: This may fail to be the true JSJ decomposition. There might be
    unrecognized (torus x I)'s in the list of pieces and it might well be
    possible to amalgamate some of the pieces into a single SFS.
    """
    
    T = regina_tri
    assert T.isZeroEfficient()
    assert T.isConnected() # We will be counting components below
    incompress_tori = []
    # got rid of regina.NormalSurfaces.enumerate
    surfaces = regina.NormalSurfaces(T, regina.NS_QUAD, regina.NS_FUNDAMENTAL)
    for i in range(surfaces.size()):
        S = surfaces.surface(i)
        if S.eulerChar() == 0:
            if not S.isOrientable():
                S = S.doubleSurface()
            assert S.isOrientable()
            X = S.cutAlong()
            X.intelligentSimplify()
            # The following is Regina 6.0 code, deprecated in version 7.0
            # X.splitIntoComponents()
            # pieces = list(children(X))
            pieces = X.triangulateComponents()
            if all(not C.hasCompressingDisc() for C in pieces):
                incompress_tori.append(S)

    if incompress_tori == []:
        return False, None
    
    # Compute disjointness graph.
    D = nx.Graph()
    for a, A in enumerate(incompress_tori):
        for b, B in enumerate(incompress_tori):
            if a < b:
                if A.disjoint(B):
                    D.add_edge(a, b)

    # Build A, the union of a clique of disjoint tori such that any torus disjoint from A
    # is parallel into A.
    cliques = list(nx.find_cliques(D))
    if len(cliques) == 0:
        clique = [0]
    else:
        clique = min(cliques, key=len)
    clique = [incompress_tori[c] for c in clique]
    num_tori = len(clique)
    A = clique[0]
    for B in clique[1:]:
        A = haken_sum(A, B)

    X = A.cutAlong()
    X.intelligentSimplify()
    # The following is Regina 6.0 code, deprecated in version 7.0
    # X.splitIntoComponents()
    # pieces = list(children(X))
    pieces = X.triangulateComponents()
    ids = [identify_with_torus_boundary(C) for C in pieces]

    # Count products. If this is less than the number of tori that we cut along,
    # then at least one torus is not boundary-parallel
    num_products = len([i for i in ids if i[1] in ('SFS [A: (1,1)]', 'A x S1')])
    # The following is Regina 6.0 code, deprecated in version 7.0
    # if len(T.boundaryComponents()) > 0 and (num_products >= num_tori):
    if T.boundaryComponents().size() > 0 and (num_products >= num_tori): 
        # All tori are boundary-parallel
        toroidal = False
    else:
        toroidal = True
    
    # Remove products
    pruned_ids = [i for i in ids if i[1] not in ('SFS [A: (1,1)]', 'A x S1')]
    # But if nothing is left, then keep one product
    if pruned_ids == []:
        pruned_ids = ids[:1]
    
    return (toroidal, sorted(pruned_ids))


def identify_with_bdy_from_isosig(data):
    """
    Given the isosig of an ideal triangulation, use the combined 
    power of Regina and SnapPy to try to give a name to the input 
    manifold. Decompose along tori, if necessary.
    
    Also works if handed a snappy Manifold.
    """
    
    kind, name = "unknown", None

    # If handed an isosig, throw away the boundary framing
    if isinstance(data, str):
        parts = iso.split("_")
        data = parts[0]
    
    P = to_regina(data)
    P.intelligentSimplify()
    M = snappy.Manifold(data)
    M.simplify()
    if appears_hyperbolic(M):
        for i in range(100):
            if M.solution_type() == 'all tetrahedra positively oriented':
                break
            M.randomize()
        
        if not M.verify_hyperbolicity(bits_prec=100):
            raise RuntimeError('Cannot prove hyperbolicity for ' +
                               M.triangulation_isosig())
        kind = 'hyperbolic'
        ids = M.identify()
        if ids:
            name = ids[0].name()
            return kind, name
    else:
        match = standard_lookup(P)
        if match is None:
            P.idealToFinite()
            P.intelligentSimplify()
            match = standard_lookup(P)
        if match is not None:
            kind = match.__class__.__name__
            name = str(match)
            return kind, name
        if match is None:
            toroidal, pieces = decompose_along_tori(P)
            if toroidal:
                kind = 'toroidal'
                name = str(pieces)
            else:
                # If atoroidal, there should be only one piece
                assert len(pieces) == 1
                kind = pieces[0][0]
                name = pieces[0][1]
            return kind, name

    # At this point, we have failed. Return the only data available.
    name = P.isoSig()
    return kind, name
    

def regina_name(closed_snappy_manifold, tries = 100, max_tets = 25):
    """
    >>> regina_name('m004(1,0)')
    'S3'
    >>> regina_name('s006(-2, 1)')
    'SFS [A: (5,1)] / [ 0,-1 | -1,0 ]'
    >>> regina_name('m010(-1, 1)')
    'L(3,1) # RP3'
    >>> regina_name('m022(-1,1)')
    'SFS [S2: (3,2) (3,2) (4,-3)]'
    >>> regina_name('v0004(0, 1)')
    'SFS [S2: (2,1) (4,1) (15,-13)]'
    >>> regina_name('m305(1, 0)')
    'L(3,1) # RP3'
    """
    M = snappy.Manifold(closed_snappy_manifold)
    isosigs = closed_isosigs(M, tries = tries, max_tets = max_tets)
    if len(isosigs) == 0:
        return
    T = to_regina(isosigs[0])
    if T.isIrreducible():
        if T.countTetrahedra() <= max_tets: # This used to be 11 - a magic number.  We may regret changing it...
            for i in range(3):
                T.simplifyExhaustive(i)
                name = census_lookup(T)
                if name is not None:
                    return name
            
        matches = [standard_lookup(to_regina(iso)) for iso in isosigs]
        match = best_match(matches)
        if match is not None:
            return str(match)
    else:
        # The following is Regina 6.0 code, deprecated in version 7.0
        # T.connectedSumDecomposition()
        # pieces = list(children(T))
        pieces = T.summands()
        if len(pieces) == 1:
            return census_lookup(pieces[0])
        pieces = [regina_name(P) for P in pieces]
        if None not in pieces:
            return ' # '.join(sorted(pieces))

def better_symmetry_group(M,index=100,num_pos_triang=1):
    '''
    This function computes the symmetry group of the input manifold. 
    If the second entry is True it is proven to be the symmetry group.
    '''
    full=False
    S='unclear'
    try:
        S=M.symmetry_group()
        full=S.is_full_group()
        if full:
            return (S,full)
    except (ValueError, RuntimeError, snappy.SnapPeaFatalError):
        pass
    pos_triang=find_positive_triangulations(M,tries=index,number=num_pos_triang)
    if pos_triang==[]:
        randomizeCount=0
        while randomizeCount<index and full==False:
            try:
                S=M.symmetry_group()
                full=S.is_full_group()
                if full:
                    return (S,full)
                M.randomize()
                randomizeCount=randomizeCount+1
            except (ValueError, RuntimeError, snappy.SnapPeaFatalError):
                M.randomize()
                randomizeCount=randomizeCount+1
    for X in pos_triang:
        randomizeCount=0
        while randomizeCount<index and full==False:
            try:
                S=X.symmetry_group()
                full=S.is_full_group()
                if full:
                    return (S,full)
                X.randomize()
                randomizeCount=randomizeCount+1
            except (ValueError, RuntimeError, snappy.SnapPeaFatalError):
                X.randomize()
                randomizeCount=randomizeCount+1
    return (S,full)

def all_positive(manifold):
    '''
    Checks if the solution type of a triangulation is positive.
    '''
    return manifold.solution_type() == 'all tetrahedra positively oriented'

def find_positive_triangulations(manifold,number=1,tries=100):
    '''
    Searches for one triangulation with a positive solution type.
    (Or if number is set to a different value also for different such triangulations.)
    '''
    M = manifold.copy()
    pos_triangulations=[]
    for i in range(tries):
        if all_positive(M):
            pos_triangulations.append(M)
            if len(pos_triangulations)==number:
                return pos_triangulations
            break
        M.randomize()
    for d in M.dual_curves(max_segments=500):
        X = M.drill(d)
        X = X.filled_triangulation()
        X.dehn_fill((1,0),-1)
        for i in range(tries):
            if all_positive(X):
                pos_triangulations.append(X)
                if len(pos_triangulations)==number:
                    return pos_triangulations
                break
            X.randomize()

    # In the closed case, here is another trick.
    if all(not c for c in M.cusp_info('is_complete')):
        for i in range(tries):
            # Drills out a random edge
            X = M.__class__(M.filled_triangulation())
            if all_positive(X):
                pos_triangulations.append(X)
                if len(pos_triangulations)==number:
                    return pos_triangulations
            break
            M.randomize()
    return pos_triangulations

def Seifert_name(M, tries = 30):
    """
    original version: identify_with_torus_boundary by Nathan Dunfield

    Input: M - snappy.Manifold(); tries - int()
    Output: None or regina name of M

    returns Regina's canonical Seifert/standard name of a cusped (non-hyperbolic) exterior given by snappy. If not detected gives None.
    Torus knot complements may need multiple tries. 
    """
    Mc = M.copy(); Mc.simplify()
    i = tries
    while i > 0:
        Mc.randomize(); Mc.simplify()
        T = regina.Triangulation3(Mc.triangulation_isosig(decorated=False))
        T.idealToFinite()            # ideal cusp -> real torus boundary (needed for SFS recognition)
        T.intelligentSimplify()
        T.intelligentSimplify()
        s = regina.StandardTriangulation.recognise(T)
        i += -1
        if not (s is None): 
            return str(s.manifold())
    return None 

First we demostrate that these knots have a unique strong inversion. For that we consider their surgery description compute the symmetry group (of orientation-preserving diffeomorphism) and then check the possible symmetry-exceptional slopes.

In [2]:
L=snappy.Manifold('L12n1739')
L.symmetry_group().isometries()
Out[2]:
[0 -> 0  1 -> 1  2 -> 2
 [1 0]   [1 0]   [1 0]
 [0 1]   [0 1]   [0 1]
 Extends to link,
 0 -> 0   1 -> 1   2 -> 2
 [ 1  0]  [-1 -4]  [1  0]
 [-1 -1]  [ 0  1]  [0 -1]
 Does not extend to link,
 0 -> 0   1 -> 1   2 -> 2
 [-1  0]  [-1  0]  [-1  0]
 [ 0 -1]  [ 0 -1]  [ 0 -1]
 Extends to link,
 0 -> 0  1 -> 1  2 -> 2
 [-1 0]  [1  4]  [-1 0]
 [ 1 1]  [0 -1]  [ 0 1]
 Does not extend to link]

So this implies that there exist exactly one non-trivial orientation-preserving symmetry of the link which is given by a strong inversion. This strong inversion survives to a strong inversion after Dehn filling to K_n. But it could be that there exist for small values of the slope another exceptional symmetry. We exclude that by computing all short slopes and checking them directly.

In [3]:
systole=better_systole(L,10)
B=Bound(systole)
slopes=L.short_slopes(length=B*math.sqrt(L.cusp_areas()[0]))
for n in range(1,100):
    if (1,2*n+2) in slopes[0]:
        print(n)
    if (-1,n+1) in slopes[2]:
        print(n)
1
1
2
2
3
3
4
4
5
5
6
7
8

So the only possibilities of K_n that might have more than one symmetry are the ones for n<9. We check these directly.

In [4]:
for n in range(1,11):
    L.dehn_fill([(1,2*n+3),(0,0),(-1,n+1)])
    F=L.filled_triangulation()
    print(n,F.symmetry_group())
1 Z/2
2 Z/2
3 Z/2
4 Z/2
5 Z/2
6 Z/2
7 Z/2
8 Z/2
9 Z/2
10 Z/2

Next, we compute the exceptional slopes of these. For that we first check which of the fillings on cusp 0 and 2 might be short.

In [5]:
L=snappy.Manifold('L12n1739')
short_slopes=L.short_slopes()
for n in range(1,100):
    if (1,2*n+3) in short_slopes[0]:
        print(n)
    if (-1,n+1) in short_slopes[2]:
        print(n)
1

So this might only occure for K_1 which is handled already in Baker-Kegel. The other ones we know to lie in short_slopes[1].

In [6]:
short_slopes[1]
Out[6]:
[(1, 0), (-5, 1), (-4, 1), (-3, 1), (-2, 1), (-1, 1), (0, 1), (1, 1)]

Next, we compute for small values of n, the short slopes and check if the slope is exceptional for K_n.

In [7]:
for n in range(1,6):
    L.dehn_fill([(1,2*n+3),(0,0),(-1,n+1)])
    print('K_n:',n)
    for x in short_slopes[1][1:]:
        L.dehn_fill(x,1)
        print(L)
        F=L.filled_triangulation(cusps_to_fill=[0,2])
        if is_hyperbolic(F)==False:
            print(regina_name(F))
    print('-------')
K_n: 1
L12n1739(1,5)(-5,1)(-1,2)
L12n1739(1,5)(-4,1)(-1,2)
L12n1739(1,5)(-3,1)(-1,2)
L12n1739(1,5)(-2,1)(-1,2)
SFS [S2: (2,1) (8,3) (11,-9)]
L12n1739(1,5)(-1,1)(-1,2)
L12n1739(1,5)(0,1)(-1,2)
L12n1739(1,5)(1,1)(-1,2)
-------
K_n: 2
L12n1739(1,7)(-5,1)(-1,3)
L12n1739(1,7)(-4,1)(-1,3)
L12n1739(1,7)(-3,1)(-1,3)
L12n1739(1,7)(-2,1)(-1,3)
SFS [S2: (2,1) (12,5) (15,-13)]
L12n1739(1,7)(-1,1)(-1,3)
L12n1739(1,7)(0,1)(-1,3)
L12n1739(1,7)(1,1)(-1,3)
-------
K_n: 3
L12n1739(1,9)(-5,1)(-1,4)
L12n1739(1,9)(-4,1)(-1,4)
L12n1739(1,9)(-3,1)(-1,4)
L12n1739(1,9)(-2,1)(-1,4)
SFS [S2: (2,1) (16,7) (19,-17)]
L12n1739(1,9)(-1,1)(-1,4)
L12n1739(1,9)(0,1)(-1,4)
L12n1739(1,9)(1,1)(-1,4)
-------
K_n: 4
L12n1739(1,11)(-5,1)(-1,5)
L12n1739(1,11)(-4,1)(-1,5)
L12n1739(1,11)(-3,1)(-1,5)
L12n1739(1,11)(-2,1)(-1,5)
SFS [S2: (2,1) (20,9) (23,-21)]
L12n1739(1,11)(-1,1)(-1,5)
L12n1739(1,11)(0,1)(-1,5)
L12n1739(1,11)(1,1)(-1,5)
-------
K_n: 5
L12n1739(1,13)(-5,1)(-1,6)
L12n1739(1,13)(-4,1)(-1,6)
L12n1739(1,13)(-3,1)(-1,6)
L12n1739(1,13)(-2,1)(-1,6)
SFS [S2: (2,1) (24,11) (27,-25)]
L12n1739(1,13)(-1,1)(-1,6)
L12n1739(1,13)(0,1)(-1,6)
L12n1739(1,13)(1,1)(-1,6)
-------

From Baker-Kegel we know already that the image of (-2,1) is a SFS slope for each knot. These calculations suggest that this is the only exceptional slope for each knot. (Excluding the (1,0) slope which gives S3.)

Below we first confirm that the image of (-2,1) is always exceptional for K_n, for that we show that L([(0,0),(-2,1),(0,0)] is a manifold obtained by gluing three Seifert fibered spaces over an annulus and thus each Dehn filling gives a non-hyperbolic manifolds.

Then we show that every other filling is hyperbolic.

In [12]:
L=snappy.Manifold('L12n1739')
L.dehn_fill((-2,1),1)
F=L.filled_triangulation()
F.splitting_surfaces()
Out[12]:
[Orientable two-sided with euler = 0,
 Orientable two-sided with euler = 0]
In [13]:
A,B=F.split(1)
A.randomize()
B.randomize()
print(Seifert_name(A))
print(Seifert_name(B))
SFS [A: (2,1)]
None
In [14]:
B.splitting_surfaces()
Out[14]:
[Orientable two-sided with euler = 0]
In [15]:
C,D=B.split(0)
print(Seifert_name(C))
print(Seifert_name(D))
SFS [A: (2,1)]
SFS [A: (2,1)]

To demonstrate that the other fillings are all hyperbolic, we show that L([(0,0),s,(0,0)] is hyperbolic for all other slopes s. For that it is enough to just check the short slopes. Since we fill along long slopes of the other cusps, it follows that all other slopes are hyperbolic.

In [16]:
for s in short_slopes[1]:
    if s not in [(1,0),(-2,1)]:
        L.dehn_fill(s,1)
        H=L.filled_triangulation()
        print(s,is_hyperbolic(H))
(-5, 1) True
(-4, 1) True
(-3, 1) True
(-1, 1) True
(0, 1) True
(1, 1) True

Next, we classify the symmetry-exceptional slopes. We use the same strategy. First we compute the symmetry exceptional slopes of the K_n for n<9:

In [19]:
for n in range(1,9):
    L.dehn_fill([(1,2*n+3),(0,0),(-1,n+1)])
    print('K_n:',n)
    for x in slopes[1][1:]:
        if x!=(-2,1):
            L.dehn_fill(x,1)
            F=L.filled_triangulation(cusps_to_fill=[0,2])
            F.randomize()
            (S,w)=better_symmetry_group(F)
            if w==False:
                print(L)
                print(S)
            else:
                if S.order()>2:
                    print(L)
                    print(S)
    print('-------')
K_n: 1
L12n1739(1,5)(0,0)(-1,2)
Z/2 + Z/2
L12n1739(1,5)(0,0)(-1,2)
Z/2 + Z/2
-------
K_n: 2
L12n1739(1,7)(0,0)(-1,3)
Z/2 + Z/2
L12n1739(1,7)(0,0)(-1,3)
Z/2 + Z/2
-------
K_n: 3
L12n1739(1,9)(0,0)(-1,4)
Z/2 + Z/2
L12n1739(1,9)(0,0)(-1,4)
Z/2 + Z/2
-------
K_n: 4
L12n1739(1,11)(0,0)(-1,5)
Z/2 + Z/2
L12n1739(1,11)(0,0)(-1,5)
Z/2 + Z/2
-------
K_n: 5
L12n1739(1,13)(0,0)(-1,6)
Z/2 + Z/2
L12n1739(1,13)(0,0)(-1,6)
Z/2 + Z/2
-------
K_n: 6
L12n1739(1,15)(0,0)(-1,7)
Z/2 + Z/2
L12n1739(1,15)(0,0)(-1,7)
Z/2 + Z/2
-------
K_n: 7
L12n1739(1,17)(0,0)(-1,8)
Z/2 + Z/2
L12n1739(1,17)(0,0)(-1,8)
Z/2 + Z/2
-------
K_n: 8
L12n1739(1,19)(0,0)(-1,9)
Z/2 + Z/2
L12n1739(1,19)(0,0)(-1,9)
Z/2 + Z/2
-------

This suggest that the only symmetry-exceptional slopes are the images of -4 and 0. Both have two additional symmetries.

The slopes on the first cusp that are shorter than the FPS bound are:

In [20]:
print(slopes[1])
[(1, 0), (-15, 1), (-14, 1), (-13, 1), (-12, 1), (-11, 1), (-10, 1), (-9, 1), (-8, 1), (-7, 1), (-6, 1), (-5, 1), (-4, 1), (-3, 1), (-2, 1), (-1, 1), (0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1), (10, 1), (11, 1), (-15, 2), (-13, 2), (-11, 2), (-9, 2), (-7, 2), (-5, 2), (-3, 2), (-1, 2), (1, 2), (3, 2), (5, 2), (7, 2), (-17, 3), (-16, 3), (-14, 3), (-13, 3), (-11, 3), (-10, 3), (-8, 3), (-7, 3), (-5, 3), (-4, 3), (-2, 3), (-1, 3), (1, 3), (2, 3), (4, 3), (5, 3), (-19, 4), (-17, 4), (-15, 4), (-13, 4), (-11, 4), (-9, 4), (-7, 4), (-5, 4), (-3, 4), (-1, 4), (1, 4), (3, 4), (-19, 5), (-18, 5), (-17, 5), (-16, 5), (-14, 5), (-13, 5), (-12, 5), (-11, 5), (-9, 5), (-8, 5), (-7, 5), (-6, 5), (-4, 5), (-3, 5), (-2, 5), (-1, 5), (-19, 6), (-17, 6), (-13, 6), (-11, 6), (-7, 6), (-5, 6), (-19, 7), (-18, 7), (-17, 7), (-16, 7), (-15, 7), (-13, 7), (-12, 7), (-11, 7), (-10, 7), (-9, 7)]

We verify that (-4,1) and (0,1) fillings of L yield manifolds with symmetry group Z_2^2 and that all other slopes with length less than the FPS bound yield a manifold with symmetry group Z_2. Then we can conclude as in the case of the exceptional slopes.

In [21]:
L=snappy.Manifold('L12n1739')
for s in slopes[1]:
    if s not in [(1,0),(-2,1),(-4,1),(0,1)]:
        L.dehn_fill(s,1)
        H=L.filled_triangulation()
        print(s,better_symmetry_group(H))
(-15, 1) (Z/2, True)
(-14, 1) (Z/2, True)
(-13, 1) (Z/2, True)
(-12, 1) (Z/2, True)
(-11, 1) (Z/2, True)
(-10, 1) (Z/2, True)
(-9, 1) (Z/2, True)
(-8, 1) (Z/2, True)
(-7, 1) (Z/2, True)
(-6, 1) (Z/2, True)
(-5, 1) (Z/2, True)
(-3, 1) (Z/2, True)
(-1, 1) (Z/2, True)
(1, 1) (Z/2, True)
(2, 1) (Z/2, True)
(3, 1) (Z/2, True)
(4, 1) (Z/2, True)
(5, 1) (Z/2, True)
(6, 1) (Z/2, True)
(7, 1) (Z/2, True)
(8, 1) (Z/2, True)
(9, 1) (Z/2, True)
(10, 1) (Z/2, True)
(11, 1) (Z/2, True)
(-15, 2) (Z/2, True)
(-13, 2) (Z/2, True)
(-11, 2) (Z/2, True)
(-9, 2) (Z/2, True)
(-7, 2) (Z/2, True)
(-5, 2) (Z/2, True)
(-3, 2) (Z/2, True)
(-1, 2) (Z/2, True)
(1, 2) (Z/2, True)
(3, 2) (Z/2, True)
(5, 2) (Z/2, True)
(7, 2) (Z/2, True)
(-17, 3) (Z/2, True)
(-16, 3) (Z/2, True)
(-14, 3) (Z/2, True)
(-13, 3) (Z/2, True)
(-11, 3) (Z/2, True)
(-10, 3) (Z/2, True)
(-8, 3) (Z/2, True)
(-7, 3) (Z/2, True)
(-5, 3) (Z/2, True)
(-4, 3) (Z/2, True)
(-2, 3) (Z/2, True)
(-1, 3) (Z/2, True)
(1, 3) (Z/2, True)
(2, 3) (Z/2, True)
(4, 3) (Z/2, True)
(5, 3) (Z/2, True)
(-19, 4) (Z/2, True)
(-17, 4) (Z/2, True)
(-15, 4) (Z/2, True)
(-13, 4) (Z/2, True)
(-11, 4) (Z/2, True)
(-9, 4) (Z/2, True)
(-7, 4) (Z/2, True)
(-5, 4) (Z/2, True)
(-3, 4) (Z/2, True)
(-1, 4) (Z/2, True)
(1, 4) (Z/2, True)
(3, 4) (Z/2, True)
(-19, 5) (Z/2, True)
(-18, 5) (Z/2, True)
(-17, 5) (Z/2, True)
(-16, 5) (Z/2, True)
(-14, 5) (Z/2, True)
(-13, 5) (Z/2, True)
(-12, 5) (Z/2, True)
(-11, 5) (Z/2, True)
(-9, 5) (Z/2, True)
(-8, 5) (Z/2, True)
(-7, 5) (Z/2, True)
(-6, 5) (Z/2, True)
(-4, 5) (Z/2, True)
(-3, 5) (Z/2, True)
(-2, 5) (Z/2, True)
(-1, 5) (Z/2, True)
(-19, 6) (Z/2, True)
(-17, 6) (Z/2, True)
(-13, 6) (Z/2, True)
(-11, 6) (Z/2, True)
(-7, 6) (Z/2, True)
(-5, 6) (Z/2, True)
(-19, 7) (Z/2, True)
(-18, 7) (Z/2, True)
(-17, 7) (Z/2, True)
(-16, 7) (Z/2, True)
(-15, 7) (Z/2, True)
(-13, 7) (Z/2, True)
(-12, 7) (Z/2, True)
(-11, 7) (Z/2, True)
(-10, 7) (Z/2, True)
(-9, 7) (Z/2, True)
In [22]:
L.dehn_fill((-4,1),1)
H=L.filled_triangulation()
better_symmetry_group(H)
Out[22]:
(Z/2 + Z/2, True)
In [23]:
L.dehn_fill((0,1),1)
H=L.filled_triangulation()
better_symmetry_group(H)
Out[23]:
(Z/2 + Z/2, True)