#!/usr/bin/env python
# coding: utf-8

# In[ ]:


# Script in Sage to check that there are no admissible strings of length < 2pi
# used in the paper "A four-dimensional pseudo-Anosov homeomorphism"


# In[1]:


# We write some affine transformations of R^3 as 4x4 matrices in the usual way

# The identity 

I = Matrix([[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]])

# Reflections along lines that cut the faces of a cube in half

s1 = Matrix([[1,0,0,0],[0,-1,0,0],[0,0,1,2],[0,0,0,1]])
s2 = Matrix([[1,0,0,2],[0,1,0,0],[0,0,-1,0],[0,0,0,1]])
s3 = Matrix([[-1,0,0,0],[0,1,0,2],[0,0,1,0],[0,0,0,1]]) 

# Rotation of angle pi along the line generted by (1,1,1)

rot = Matrix([[0,0,1,0],[1,0,0,0],[0,1,0,0],[0,0,0,1]])

# Reflection along the origin
# (this exchanges red and green segments)

mI = Matrix([[-1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]])

# Reflection along a line that contains the green segment (1/2, 1/2, 1/2) + t(1,-1,0)
# (sends vertices of the cubes in the centers of the cubes)

f = Matrix([[0,-1,0,1], [-1,0,0,1], [0,0,-1,1], [0,0,0,1]])

s1, s2, s3, rot, mI, f


# In[3]:


direzioni = []

# The 8 basic vectors, of types (a), ..., (h)
# For each vector we write the vector, the departing sector, the arrival sector, the minimum length,
# the departing and arrival blue segment, oriented towards the endpoint that is closer to the other blue segment,
# and finally the type.

basic_vectors = [[[0,-2,2,0], [-1,-1,2,0], [-2,1,-1,0], 2/5, [-1,-1,-1,0], [1,1,-1,0], 'a'], 
                 [[1,-1,1,0], [1,-2,1,0], [-2,1,-1,0], 1/5, [1,1,1,0], [1,1,-1,0], 'b'], 
                 [[2,0,0,0], [2,-1,-1,0], [-2,1,-1,0], 1/5, [1,1,1,0], [-1,-1,1,0], 'c'], 
                 [[3,1,-1,0], [1,1,-2,0], [-2,1,-1,0], 1/2, [1,1,1,0], [-1,-1,1,0], 'd'],  
                 [[3,3,-1,0], [1,1,-2,0], [-1,-1,2,0], 3/5, [1,1,1,0], [-1,-1,-1,0], 'e'],    
                 [[-1,3,-1,0], [-1,2,-1,0], [1,-2,1,0], 3/5, [1,1,1,0], [-1,-1,-1,0], 'f'], 
                 [[1,3,1,0], [-1,2,-1,0], [-1,-1,-2,0], 1/2, [1,1,1,0], [-1,-1,1,0], 'g'], 
                 [[0,2,2,0], [-2,1,1,0], [-1,-1,-2,0], 2/5, [1,1,1,0], [-1,-1,1,0], 'h']]
                 # [[1,3,1,0], [-1,2,-1,0], [1,-2,-1,0], 1/2],  old versions probably mistaken
                 # [[0,2,2,0], [-2,1,1,0], [1,-2,-1,0], 2/5]]

# Transform the lists of numbers into vectors following sage        
        
basic_vectors_v = [vector([0,0,0,1]), [[vector(v[0]), vector(v[1]), vector(v[2]), v[3], vector(v[4]), vector(v[5]), v[6]] for v in basic_vectors]]

# Calculate the 48 vectors exiting from the origin by acting via isometries

dallorigine = [vector([0,0,0,1]), []]
for g in [I, rot, rot^2, mI, mI * rot, mI * rot^2]:
    for v in basic_vectors_v[1]:
        new_v = [g * v[0], g * v[1], g * v[2], v[3], g * v[4], g * v[5], v[6]]
        dallorigine[1].append(new_v)
        # print (new_v)

# Calculate the 4 * 2 * 48 vectors overall
# Here * 2 because we add the centers of the cubes
# And * 4 because there are four cube types
# Every other vector is obtained by translating one of these

for g in [I, s1, s2, s3, f, s1*f, s2*f, s3*f]:
    new_directions = [g * dallorigine[0], []]
    for v in dallorigine[1]:
        new_v = [g * v[0], g * v[1], g * v[2], v[3], g * v[4], g * v[5], v[6]]
        new_directions[1].append(new_v)
    new_directions[1].sort()
    direzioni.append(new_directions)  
direzioni


# In[4]:


# Sanity check: the vectors departing from the origin are the vectors arriving to the origin, 
# with opposite signs

zero = vector([0,0,0,1])
due = vector([2,2,2,0])

counter = 0
for v in direzioni:
    for w in v[1]:
        u = v[0] + w[0]
        if u % 4 == (zero) % 4 or (u + due) % 4 == (zero) % 4:
            # print (v[0], w)
            counter = counter + 1
            for z in direzioni[0][1]:
                if z[0] == - w[0]:
                    # print ("ie", z)
                    if w[1] != z[2] or w[2] != z[1]:
                        print ("ACHTUNG: ")
                        print (v[0], w)
                        print (z)        
print (counter)


# In[5]:


# Sanity check: every departing and arrival vector must be orthogonal to the corresponding blue line
#
# If sometimes goes wrong it writes ACHTUNG!

due = vector([2,2,2,0])

blue_lines = [[vector([0,0,0,1]), vector([1,1,1,0])], [vector([1,1,1,1]), vector([1,1,1,0])],
              [vector([2,0,0,1]), vector([1,1,-1,0])], [vector([1,-1,1,1]), vector([1,1,-1,0])],
              [vector([0,2,0,1]), vector([-1,1,1,0])], [vector([1,1,-1,1]), vector([-1,1,1,0])],
              [vector([0,0,2,1]), vector([1,-1,1,0])], [vector([-1,1,1,1]), vector([1,-1,1,0])]]

for v in direzioni:
    u = v[0]
    bl_initial = []
    for l in blue_lines:
        if l[0] % 4 == u % 4 or (l[0] + due) % 4 == u % 4:
            bl_initial = l[1]
    for w in v[1]:
        if w[1] * bl_initial != 0:
            print ("ACHTUNG! ", v[0], w)
        u = v[0] + w[0]
        bl_final = []
        for l in blue_lines:
            if l[0] % 4 == u % 4 or (l[0] + due) % 4 == u % 4:
                bl_final = l[1]
        if w[2] * bl_final != 0:
            print ("ACHTUNG! ", v[0], w)


# In[6]:


# Main iterative routines

zero = vector([0,0,0,1])
due = vector([2,2,2,0])

short_geodesics = []

# Determines the bonus length
#
# We refer to the conditions (1), ..., (5) listed at the end of the paper (page 74 in the first version on the arXiv)

def bonus_length(path, num_nodes):
    bonus = 0
    used = [False for i in range(num_nodes)]
    for i in range(num_nodes):
        i1 = (i+1) % num_nodes
        i2 = (i+2) % num_nodes
        
        # Condition (1): (b,b,c) or (c,b,b)
        
        if (used[i], used[i1], used[i2]) == (False, False, False):  
            types = [path[i][6], path[i1][6], path[i2][6]]            
            if types == ['b', 'b', 'c'] or types == ['c', 'b', 'b']:
                somma = path[i][0] + path[i1][0] + path[i2][0]
                if path[i][4] == - path[i2][5] and (somma == 2 * path[i][4] or somma == - 2 * path[i][4]):
                    bonus = bonus + 2/5
                    used[i], used[i1], used[i2] = True, True, True

        # Condition (2): (b,f,b)
                    
        if (used[i], used[i1], used[i2]) == (False, False, False):  
            types = [path[i][6], path[i1][6], path[i2][6]]            
            if types == ['b', 'f', 'b']:
                if path[i][5] != path[i1][4] and path[i1][5] != path[i2][4]:
                    bonus = bonus + 1/5
                    used[i], used[i1], used[i2] = True, True, True

    for i in range(num_nodes):
        i1 = (i+1) % num_nodes        
        if (used[i], used[i1]) == (False, False):
            types = [path[i][6], path[i1][6]]

            # Condition (3): (h,c), (c,a), (g,b), (b,d), (h,d), (g,a)
            
            L = [['h', 'c'], ['c', 'a'], ['g', 'b'], ['b', 'd'], ['h', 'd'], ['g', 'a']]
            if types in L: 
                somma = path[i][0] + path[i1][0]
                if path[i][4] == - path[i1][5] and (somma == 2 * path[i][4] or somma == - 2 * path[i][4] or somma == 3 * path[i][4] or somma == - 3 * path[i][4]):
                    if types == ['h', 'c'] or types == ['c', 'a']:
                        bonus = bonus + 2/5
                    if types == ['g', 'b'] or types == ['b', 'd']:
                        bonus = bonus + 3/10
                    if types == ['h', 'd'] or types == ['g', 'a']:
                        bonus = bonus + 1/10
                    used[i], used[i1] = True, True                            
            
            # Condition (4): (b,c), (c,b)
            
            if types == ['b', 'c'] or types == ['c', 'b']:
                if path[i][5] != path[i1][4]:
                    if path[i][4] != path[i1][5] and path[i][4] != - path[i1][5]:
                        bonus = bonus + 1/5 
                        used[i], used[i1] = True, True
                        
            # Condition (5): (b,d), (g,b), (c,d), (g,c)

            J = [['b', 'd'], ['g', 'b'], ['c', 'd'], ['g', 'c']]
            if types in J:
                if path[i][5] != path[i1][4]:
                    bonus = bonus + 1/10 
                    used[i], used[i1] = True, True                    

    return bonus

# Main iterative routine to search the short closed geodesics

def search_closed_paths(level = 0, path = [], length = 0, point = zero):
    if level == 0:
        path = [[] for i in range(10)]
        for v in direzioni[0][1]:
            path[0] = v
            point = zero + v[0]
            search_closed_paths(1, path, v[3], point)
        return
    
    for P in direzioni:
        if P[0] % 4 == point % 4 or (P[0] + due) % 4 == point % 4:
            for v in P[1]:
                is_ok = True
                
                # Exclude coincident or adjacent sectors
                
                if path[level-1][2] * v[1] >= 0: is_ok = False 
                    
                # Excludes pairs of consecutive chords that are cooriented, both of type different from 'b'
                
                S = ['a', 'c', 'd', 'e', 'f', 'g', 'h']
                if v[6] in S and path[level-1][6] in S:
                    if v[4] == path[level-1][5]: is_ok = False
                        
                # Exclude triples of consecutive chords that are cooriented, the second being 'b'
                                        
                if level >= 2 and v[6] in S and path[level-1][6] == 'b' and path[level-2][6] in S:
                    if v[4] == path[level-1][5] and path[level-1][4] == path[level-2][5]: is_ok = False
                                                                    
                if is_ok:  
                    additional_length = v[3]                        
                    if length + additional_length <= 2:
                        path[level] = v
                        new_point = point + v[0]
                        new_length = length + additional_length

                        if new_point == zero:
                            is_ok = True
                            
                            # Test the same conditions written above cyclically 
                            
                            if v[2] * path[0][1] >= 0: is_ok = False
                            if v[6] in S and path[0][6] in S:
                                if v[5] == path[0][4]: is_ok = False
                            if v[6] in S and path[0][6] == 'b' and path[1][6] in S:
                                if v[5] == path[0][4] and path[0][5] == path[1][4]: is_ok = False
                            if path[level-1][6] in S and v[6] == 'b' and path[0][6] in S:
                                if v[5] == path[0][4] and path[level-1][5] == v[4]: is_ok = False
                                                            
                            if is_ok:
                                bonus = bonus_length(path, level+1)
                                if new_length < 2:   # This can be set to either < or <= to get some output
                                    print (level+1, new_length)
                                    for p in path:
                                        print (p)
                                    print ("Bonus length: ", bonus)
                                    print ("Total length: ", new_length + bonus)
                                if new_length + bonus < 2:
                                    print ("ACHTUNG!")
                                    short_geodesics.append(path[:])   # Attenzione: copiare tutto
                            return
                        search_closed_paths(level + 1, path, new_length, new_point)
                    


# In[8]:


# This is the main command. It starts the search process.

search_closed_paths()

