From 7668adcbd7ecd33cceb1e020b817be8b142361e5 Mon Sep 17 00:00:00 2001 From: Josh C Date: Mon, 2 Oct 2023 15:50:05 +1100 Subject: [PATCH] symplectic basis for triangulated 3 manifolds --- cython/SnapPy.pxi | 3 + cython/core/triangulation.pyx | 87 + dev/symplectic_basis/CMakeLists.txt | 63 + .../CuspedCensusData/terse5.bin | Bin 0 -> 7470 bytes .../CuspedCensusData/terse6n.bin | Bin 0 -> 4662 bytes .../CuspedCensusData/terse6o.bin | Bin 0 -> 17316 bytes .../CuspedCensusData/terse7n.bin | Bin 0 -> 15966 bytes .../CuspedCensusData/terse7o.bin | Bin 0 -> 63936 bytes .../file_formats/GeneratorsFileFormat | 111 + .../file_formats/LinkProjectionFileFormat | 152 + dev/symplectic_basis/file_formats/ReadMe | 21 + .../file_formats/TriangulationFileFormat | 94 + dev/symplectic_basis/graph.py | 57 + dev/symplectic_basis/large-db | 145 + dev/symplectic_basis/small-db | 1000 ++++++ dev/symplectic_basis/symplectic_basis_main.c | 109 + dev/symplectic_basis/symplectic_basis_old.h | 244 ++ dev/symplectic_basis/symplectic_basis_test.c | 112 + dev/symplectic_basis/test_base.py | 161 + dev/symplectic_basis/test_linux.py | 69 + dev/symplectic_basis/test_windows.py | 82 + kernel/headers/SnapPea.h | 16 + kernel/kernel_code/symplectic_basis.c | 3035 +++++++++++++++++ quad_double/kernel_code/symplectic_basis.cpp | 1 + 24 files changed, 5562 insertions(+) create mode 100644 dev/symplectic_basis/CMakeLists.txt create mode 100644 dev/symplectic_basis/CuspedCensusData/terse5.bin create mode 100644 dev/symplectic_basis/CuspedCensusData/terse6n.bin create mode 100644 dev/symplectic_basis/CuspedCensusData/terse6o.bin create mode 100644 dev/symplectic_basis/CuspedCensusData/terse7n.bin create mode 100644 dev/symplectic_basis/CuspedCensusData/terse7o.bin create mode 100644 dev/symplectic_basis/file_formats/GeneratorsFileFormat create mode 100644 dev/symplectic_basis/file_formats/LinkProjectionFileFormat create mode 100644 dev/symplectic_basis/file_formats/ReadMe create mode 100644 dev/symplectic_basis/file_formats/TriangulationFileFormat create mode 100644 dev/symplectic_basis/graph.py create mode 100644 dev/symplectic_basis/large-db create mode 100644 dev/symplectic_basis/small-db create mode 100644 dev/symplectic_basis/symplectic_basis_main.c create mode 100644 dev/symplectic_basis/symplectic_basis_old.h create mode 100644 dev/symplectic_basis/symplectic_basis_test.c create mode 100644 dev/symplectic_basis/test_base.py create mode 100644 dev/symplectic_basis/test_linux.py create mode 100644 dev/symplectic_basis/test_windows.py create mode 100644 kernel/kernel_code/symplectic_basis.c create mode 100644 quad_double/kernel_code/symplectic_basis.cpp diff --git a/cython/SnapPy.pxi b/cython/SnapPy.pxi index 4bffcd221..a4612c11b 100644 --- a/cython/SnapPy.pxi +++ b/cython/SnapPy.pxi @@ -705,6 +705,9 @@ cdef extern from "SnapPea.h": extern void two_bridge(c_Triangulation *manifold, Boolean *is_two_bridge, long int *p, long int *q) except * extern Real volume(c_Triangulation *manifold, int *precision) except * extern Boolean mark_fake_cusps(c_Triangulation *manifold) except * + + extern int** get_symplectic_basis(c_Triangulation *manifold, int *, int *, int) except * + extern void free_symplectic_basis(int **, int) except * extern void register_callbacks(void (*begin_callback)(), void (*middle_callback)(), void (*end_callback)()) diff --git a/cython/core/triangulation.pyx b/cython/core/triangulation.pyx index 1f1245999..23d0e266a 100644 --- a/cython/core/triangulation.pyx +++ b/cython/core/triangulation.pyx @@ -3031,3 +3031,90 @@ cdef class Triangulation(): ignore_curve_orientations = ignore_curve_orientations, ignore_orientation = ignore_orientation) return self._cache.save(result, 'triangulation_isosig', *args) + + def symplectic_basis(self, verify=False): + """ + Extend the Neumann-Zagier Matrix to one which is symplectic (up to factors of 2) + using oscillating curves. Verify parameter explicitly tests if the resulting matrix is symplectic. + Only accepts triangulations with 1 cusp. + + >>> M = Manifold("4_1") + >>> M.symplectic_basis() + [-1 0 -1 -1] + [ 2 0 -2 0] + [-2 -1 -2 -1] + [ 0 -1 -2 -1] + + + """ + def is_symplectic(M): + """ + Test if the matrix M is symplectic + :param M: square matrix + :return: true or false + """ + n = len(M) + + for i in range(n): + for j in range(i, n): + omega = abs(symplectic_form(M[i], M[j])) + + if i % 2 == 0 and j % 2 == 1 and j == i + 1: + if omega != 2: + return False + elif omega: + return False + + return True + + def symplectic_form(u, v): + return sum([u[2 * i] * v[2 * i + 1] - u[2 * i + 1] * v[2 * i] for i in range(len(u) // 2)]) + + cdef int **c_eqns; + cdef int **g_eqns; + cdef int num_rows, num_cols, dual_rows; + cdef int* eqn; + + if self.c_triangulation is NULL: + raise ValueError('The Triangulation is empty.') + + # current get_symplectic_eqns() implementation requires 1 cusp + if self.num_cusps() > 1: + raise ValueError('Triangulation contains {} cusps, only accepts triangulations with 1 cusp'.format(self.num_cusps())) + + eqns = [] + + peripheral_curves(self.c_triangulation) + + # Cusp Equations + for i in range(self.num_cusps()): + cusp_info = self.cusp_info(i) + if cusp_info.is_complete: + to_do = [(1,0), (0,1)] + else: + to_do = [cusp_info.filling] + for (m, l) in to_do: + eqn = get_cusp_equation(self.c_triangulation, + i, int(m), int(l), &num_rows) + eqns.append([eqn[j] for j in range(num_rows)]) + free_cusp_equation(eqn) + + # Dual Curve Equations + g_eqns = get_symplectic_basis(self.c_triangulation, &dual_rows, &num_cols, 0) + + for i in range(dual_rows): + eqns.append([g_eqns[i][j] for j in range(num_cols)]) + + free_symplectic_basis(g_eqns, dual_rows) + + # Convert to Neumann Zagier Matrix + rows = len(eqns) + retval = [[eqns[i][3 * (j // 2) + j % 2] - eqns[i][3 * (j // 2) + 2] for j in range(rows)] for i in range(rows)] + + if verify: + if is_symplectic(retval): + print("Result is symplectic (up to factors of 2)") + else: + print("Warning: Result is not symplectic") + + return matrix(retval) diff --git a/dev/symplectic_basis/CMakeLists.txt b/dev/symplectic_basis/CMakeLists.txt new file mode 100644 index 000000000..a13b9bb6e --- /dev/null +++ b/dev/symplectic_basis/CMakeLists.txt @@ -0,0 +1,63 @@ +# To compile project +# - run 'cmake -S . -B build/' in the dev/symplectic_basis dir +# - run 'cmake --build build/' + +# For clangd copy 'dev/symplectic_basis/build/compile_commands.json' to 'build/' + +cmake_minimum_required(VERSION 3.12) +set(CMAKE_C_COMPILER "gcc") +set(CMAKE_CXX_COMPILER "g++") + +project(SnapPea) +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +# Set the name of your program +set(YOUR_PROGRAM symplectic_basis) + +# Set the location of the SnapPea kernel code +set(SNAPPEA_KERNEL ${PROJECT_SOURCE_DIR}/../../kernel) + +# Compiler options +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -Wall -fanalyzer") + +# Add include directories +include_directories(${SNAPPEA_KERNEL}/addl_code ${SNAPPEA_KERNEL}/headers ${SNAPPEA_KERNEL}/real_type ${SNAPPEA_KERNEL}/unix_kit ${SNAPPEA_KERNEL}/) + +# Add source files +file(GLOB KERNEL_SOURCES ${SNAPPEA_KERNEL}/*/*.c) +file(GLOB HEADER_FILES ${SNAPPEA_KERNEL}/*/*.h) +file(COPY ${CMAKE_SOURCE_DIR}/CuspedCensusData DESTINATION ${CMAKE_SOURCE_DIR}/build) # Copy census data to be used by program + +# Create executable target +add_executable(${YOUR_PROGRAM} ${YOUR_PROGRAM}_main.c ${KERNEL_SOURCES} ${HEADER_FILES} ${CUSPED_CENSUS}) + +# Link math library +target_link_libraries(${YOUR_PROGRAM} m) + +# Define custom target to create BuildDate file +#add_custom_target(BuildDate ALL +# COMMAND date > ${CMAKE_SOURCE_DIR}/BuildDate +# DEPENDS ${KERNEL_SOURCES} ${HEADER_FILES} +# WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +# ) + +# testing binary +add_executable(${YOUR_PROGRAM}_test ${YOUR_PROGRAM}_test.c ${KERNEL_SOURCES} ${HEADER_FILES} ${CUSPED_CENSUS}) +target_link_libraries(${YOUR_PROGRAM}_test m) + +# enable testing functionality +enable_testing() + +# define tests +add_test( + NAME symplectic_test + COMMAND $ +) + +# Clean target +add_custom_target(clean-all + COMMAND ${CMAKE_COMMAND} -E remove ${YOUR_PROGRAM} ${YOUR_PROGRAM}.o + COMMAND ${CMAKE_COMMAND} -E remove_directory KernelObjects + COMMAND ${CMAKE_COMMAND} -E remove *.pyc + ) + diff --git a/dev/symplectic_basis/CuspedCensusData/terse5.bin b/dev/symplectic_basis/CuspedCensusData/terse5.bin new file mode 100644 index 0000000000000000000000000000000000000000..ad947f788356f3402b0358d62f40480ff07a5810 GIT binary patch literal 7470 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 Rq*eo)5e|qXLmILl>;JwTGd=(S literal 0 HcmV?d00001 diff --git a/dev/symplectic_basis/CuspedCensusData/terse6o.bin b/dev/symplectic_basis/CuspedCensusData/terse6o.bin new file mode 100644 index 0000000000000000000000000000000000000000..81ecd5c924d839bd32afe998a5ee9ecfb37c9273 GIT binary patch literal 17316 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 literal 0 HcmV?d00001 diff --git a/dev/symplectic_basis/CuspedCensusData/terse7o.bin b/dev/symplectic_basis/CuspedCensusData/terse7o.bin new file mode 100644 index 0000000000000000000000000000000000000000..ef876bb98f88b3fc0a4476dcc500a818a1aba7eb GIT binary patch literal 63936 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 bAY`uUDsgY`!$Vf1FbYA+A+xnciM{s!%}UE( literal 0 HcmV?d00001 diff --git a/dev/symplectic_basis/file_formats/GeneratorsFileFormat b/dev/symplectic_basis/file_formats/GeneratorsFileFormat new file mode 100644 index 000000000..3b70419b6 --- /dev/null +++ b/dev/symplectic_basis/file_formats/GeneratorsFileFormat @@ -0,0 +1,111 @@ + SnapPea Matrix Generators File Format + +A generators file must begin with the line + + % Generators + +to distinguish it from a triangulation file, which begins with +"% Triangulation", or a link projection file which begins with +"% Link Projection". Next comes an integer telling how many matrices +are present. The matrices may by in either O(3,1) or SL(2,C). +Orientation-reversing generators are allowed in O(3,1) but not in +PSL(2,C). If the matrices are in SL(2,C), read_generators() will +convert them to O(3,1). read_generators() can tell which format +you are using by comparing the total number of matrix entries to +the total number of matrices. (SL(2,C) matrices contain 8 real entries +each, while O(3,1) matrices contain 16 real entries each.) + +In PSL(2,C), the entries of a matrix + + a b + c d + +should be written as + + Re(a) Im(a) + Re(b) Im(b) + Re(c) Im(c) + Re(d) Im(d). + +Actually, the arrangement of the white space (blanks, tabs and returns) +is irrelevant, so if you prefer you may write a PSL(2,C) matrix as, say, + + Re(a) Im(a) Re(b) Im(b) + Re(c) Im(c) Re(d) Im(d). + +In O(3,1) the entries of each matrix should be written as + + m00 m01 m02 m03 + m10 m11 m12 m13 + m20 m21 m22 m23 + m30 m31 m32 m33 + +where the 0-th coordinate is the timelike one. Again, the arrangement +of the white space is irrelevant. + +Here are two sample files. + +Sample #1. PSL(2,C) generators for the Borromean rings complement. + +% Generators +6 + 0.000000000000000 0.000000000000000 + 0.000000000000000 -1.000000000000000 + 0.000000000000000 -1.000000000000000 + 2.000000000000000 0.000000000000000 + + 0.000000000000000 0.000000000000000 + 0.000000000000000 1.000000000000000 + 0.000000000000000 1.000000000000000 + 2.000000000000000 0.000000000000000 + + 1.000000000000000 -1.000000000000000 + 0.000000000000000 -1.000000000000000 + 0.000000000000000 1.000000000000000 + 1.000000000000000 1.000000000000000 + + 1.000000000000000 -1.000000000000000 + 0.000000000000000 1.000000000000000 + 0.000000000000000 -1.000000000000000 + 1.000000000000000 1.000000000000000 + + 1.000000000000000 0.000000000000000 + -2.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + 1.000000000000000 0.000000000000000 + + 1.000000000000000 0.000000000000000 + 0.000000000000000 0.000000000000000 + -2.000000000000000 0.000000000000000 + 1.000000000000000 0.000000000000000 + +Sample #2. O(3,1) generators for a mirrored regular ideal tetrahedron. + +% Generators + +4 + + 1.25 -0.433012 -0.433012 -0.433012 + 0.433012 0.25 -0.75 -0.75 + 0.433012 -0.75 0.25 -0.75 + 0.433012 -0.75 -0.75 0.25 + + 1.25 -0.433012 +0.433012 +0.433012 + 0.433012 0.25 +0.75 +0.75 +-0.433012 +0.75 0.25 -0.75 +-0.433012 +0.75 -0.75 0.25 + + 1.25 +0.433012 -0.433012 +0.433012 +-0.433012 0.25 +0.75 -0.75 + 0.433012 +0.75 0.25 +0.75 +-0.433012 -0.75 +0.75 0.25 + + 1.25 +0.433012 +0.433012 -0.433012 +-0.433012 0.25 -0.75 +0.75 +-0.433012 -0.75 0.25 +0.75 + 0.433012 +0.75 +0.75 0.25 + +(Note: I truncated sqrt(3)/4 = 0.433012701892219323... to 0.433012 +to fit the above matrices within the width of this window. +If you want to try out this example, please restore the +high-precision value.) diff --git a/dev/symplectic_basis/file_formats/LinkProjectionFileFormat b/dev/symplectic_basis/file_formats/LinkProjectionFileFormat new file mode 100644 index 000000000..91805ac87 --- /dev/null +++ b/dev/symplectic_basis/file_formats/LinkProjectionFileFormat @@ -0,0 +1,152 @@ +// SnapPea Link Projection File Format +// +// This document explains SnapPea's file format for storing +// link projections. The link projections are written as text- +// only files, to allow easy cross-platform file transfers, and +// also to make it easy for other computer programs to create +// link projection files. +// +// This document contains an annotated link projection file. +// If you remove all comments (i.e. remove all text preceded by +// a double slash) you'll be left with a SnapPea-readable link +// projection file. +// +// This sample file describes the Hopf link. +// +// --------- +// | | +// | ----|---- +// | | | | +// ----|---- | +// | | +// --------- +// +// The horizontal axis runs left-to-right (as you would expect), +// but the vertical axis runs top-to-bottom (as you may or may +// not expect, depending on what window system you're used to). +// The square's corner coordinates are at multiples of 50. +// +// Although this simple example doesn't illustrate it, the +// file format allow "unfinished" link components. That is, +// some or all of the link components may be topological +// intervals instead of topological circles. Indeed, a link +// component may consist of a single vertex. Morever, the +// format allows for a "hot vertex". The hot vertex is the +// vertex (if any) which the link editor will join to the next +// point the user clicks. +// +// The file format doesn't require general position, but SnapPea's +// triangulation algorithm does. To keep it happy, please don't +// let edges pass through other vertices (they can come as close +// as they want) and don't let crossings get too close to one +// another (their coordinates will be rounded to integer values, +// and in any case human viewers will appreciate having crossings +// stay at least a half dozen pixels away from one another). +// (As you may have guessed, the coordinates are pixel coordinates.) +// +// Now on to a line-by-line description of the file . . . + +// Every link projection file begins with the following line. +// This tells SnapPea what file type to expect (link projection, +// triangulation, or generators). + +% Link Projection + +// Components +// +// The '2' (see below) means this link has two components. +// +// The components are numbered implicitly, beginning at 0. +// For each component, there is a line giving the indices of +// the component's first and last vertices. For a circular +// component the first and last vertex will be the same, and +// may be any vertex in the component. For a linear component +// the first and last vertices will be the endpoints. +// So, in this example, we see that +// +// component 0 begins and ends at vertex 0, and +// component 1 begins and ends at vertex 4. + +2 + 0 0 + 4 4 + +// Vertices +// +// The '8' means that this example has 8 vertices. +// +// The vertices are numbered implicitly, beginning at 0. +// For each vertex, there is a line giving the vertex's +// coordinates. In this example, +// +// vertex 0 is at ( 50, 50) +// vertex 1 is at (150, 50) +// ... +// vertex 7 is at (100, 200) +// +// Recall that the vertical axis (the second coordinate) is +// directed top-to-bottom. + +8 + 50 50 + 150 50 + 150 150 + 50 150 + 100 100 + 200 100 + 200 200 + 100 200 + +// Edges +// +// The next '8' means that this example has 8 edges. +// +// The edges are numbered implicitly, beginning at 0. +// For each edge, there is a line giving the indices of the +// edge's endpoints. In this example, +// +// edge 0 runs from vertex 0 to vertex 1 +// edge 1 runs from vertex 1 to vertex 2 +// ... +// edge 7 runs from vertex 7 to vertex 4 +// +// All edges are directed, so the order of the vertices +// is important. + +8 + 0 1 + 1 2 + 2 3 + 3 0 + 4 5 + 5 6 + 6 7 + 7 4 + +// Crossings +// +// The following '2' means that this example has 2 crossings. +// +// For each crossing, there is a line giving the indices of the +// underedge and the overedge. In this example +// +// edge 2 passes under edge 7 +// edge 4 passes under edge 1 + +2 + 2 7 + 4 1 + +// If there is a "hot vertex" (cf. above) its index is given here. +// If there is no hot vertex, a -1 appears here. +// In this example, there is no hot vertex. (Indeed, a hot vertex +// makes sense only when it's the endpoint of a linear component.) + +-1 + +// Technical note: Just because a component has the same first +// and last vertex, you can't conclude that it's a circular +// component. It could be a component consisting of a single +// vertex and no edges. + +// The end. diff --git a/dev/symplectic_basis/file_formats/ReadMe b/dev/symplectic_basis/file_formats/ReadMe new file mode 100644 index 000000000..ac5e609fc --- /dev/null +++ b/dev/symplectic_basis/file_formats/ReadMe @@ -0,0 +1,21 @@ +SnapPea File Formats + +Strictly speaking, the SnapPea kernel is platform independent and +defines no file formats. Nevertheless, while writing the Macintosh +SnapPea UI I tried to design file formats which would be suitable +on all platforms (Macintosh, Windows & Unix). In particular, all +SnapPea files are standard text files. I strongly encourage the use +of these file formats on all platforms. The "unix kit" provides +unix-style code (fprintf/fscanf) for reading them and passing them +to the SnapPea kernel. + +SnapPea uses three types of files: + + triangulation files + matrix generator files + link projection files + +Each is described in a separate document in this directory. + +The only format the user needs to be aware of is that for matrix +generators, which is essentially just a list of matrices. diff --git a/dev/symplectic_basis/file_formats/TriangulationFileFormat b/dev/symplectic_basis/file_formats/TriangulationFileFormat new file mode 100644 index 000000000..9d8ade4c9 --- /dev/null +++ b/dev/symplectic_basis/file_formats/TriangulationFileFormat @@ -0,0 +1,94 @@ +// SnapPea Triangulation File Format +// +// This document contains an annotated triangulation file. If you remove +// all comments (i.e. remove all text preceded by a double slash) you'll +// be left with a SnapPea-readable triangulation file. The manifold is +// a (1,1) Dehn filling on one cusp of the Whitehead link complement, which +// turns out to be homeomorphic to the figure eight knot complement. +// +// The information on the hyperbolic structure (solution type, volume, +// and tetrahedron shapes) is provided solely for human readers. +// The SnapPea kernel ignores this information and recomputes the +// hyperbolic structure from scratch. +// +// The meridian and longitude are optional. The SnapPea kernel will +// use them if they are provided. If they are all zero, it will use +// a default meridian and longitude. +// +// The numbers of torus and Klein bottle cusps are also optional. +// You may set both to zero (and of course omit the cusp topology +// and Dehn filling information) if you want SnapPea to figure out +// the cusps for you and assign arbitrary indices. +// +// 97/12/6 This file format now allows finite ( = non ideal) vertices. +// If you have set the number of torus and Klein bottle cusps to zero +// (cf. preceding paragraph) SnapPea will figure out for itself which +// vertices are ideal and which are finite (by checking the Euler +// characteristic of each boundary component). +// If you have manually specified the real cusps, then simply assign +// an index of -1 for the "incident cusp" of each finite vertex. +// Even if there is more than one finite vertex, all get cusp index -1. +// +// Any low-dimensional topologist should be able to understand the +// header information. There is no need to understand the information +// about each tetrahedron, but if you want to understand it anyhow you +// should first read the file triangulation.h. + +% Triangulation // Every triangulation file must begin + // with the header "% Triangulation". +sample // name of manifold +geometric_solution 2.02988321 // SolutionType and volume (cf. SnapPea.h) +oriented_manifold // Orientability + // oriented_manifold + // or nonorientable_manifold + // or unknown_orientability +CS_known 0.00000000000000000000 // CS_known or CS_unknown + // if CS_known, value is given + +2 0 // number of torus and Klein bottle cusps + torus 1.000000000000 1.000000000000 // topology and Dehn filling + // for cusp #0 + torus 0.000000000000 0.000000000000 // topology and Dehn filling + // for cusp #1 + // 0 0 means the cusp is unfilled + +4 // number of tetrahedra + 3 1 2 1 // neighbors (cf. Triangulation.h) + 0132 0321 0132 3120 // gluings (in contrast to the old file format, + // permutations are given in "forwards order", + // e.g. 0123 is the identity) + 1 1 0 1 // incident cusps + 0 0 0 0 0 0 0 0 0 1 0 -1 -1 1 0 0 // meridian (right sheet) + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 // meridian (left sheet) + 0 1 0 -1 0 0 -1 1 1 1 0 -2 -1 1 0 0 // longitude (right sheet) + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 // longitude (left sheet) + 0.429304013127 0.107280447008 // tetrahedron shape + +// and similarly for the remaining three tetrahedra . . . + + 0 2 3 0 + 3120 1230 0132 0321 + 1 1 0 1 + 0 0 0 0 0 0 1 -1 1 0 0 -1 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 1 0 -1 -1 0 2 -1 2 -1 0 -1 1 0 -1 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0.692440400998 0.318147957810 + + 3 3 1 0 + 1023 0213 3012 0132 + 1 1 1 0 + 0 0 0 0 0 0 0 0 1 -1 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 -1 1 2 -2 0 0 0 -1 1 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1.692440400998 0.318147957810 + + 0 2 2 1 + 0132 1023 0213 0132 + 1 1 1 0 + 0 0 0 0 0 0 1 -1 1 -1 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 2 -2 1 -2 0 1 -1 0 1 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0.692440400998 0.318147957810 diff --git a/dev/symplectic_basis/graph.py b/dev/symplectic_basis/graph.py new file mode 100644 index 000000000..274ce8eeb --- /dev/null +++ b/dev/symplectic_basis/graph.py @@ -0,0 +1,57 @@ +import networkx as nx +import matplotlib.pyplot as plt + + +def debug_to_graph(lines): + graph_segment = False + graph_index = -1 + index = 0 + retval = [[]] + + for line in lines: + list = line.split(' ') + + if graph_segment is False: + graph_segment = list[0] == "Graph" + elif list[0] == "Boundary": + graph_segment = True + graph_index += 1 + retval[index].append([]) + elif len(list) < 4 or list[4] != "Vertex": + graph_segment = False + index += 1 + graph_index = -1 + retval.append([]) + else: + vertex = int(list[5]) + edges = list[12:] + for edge in edges[:-1]: + retval[index][graph_index].append((vertex, int(edge))) + + retval.pop() + return retval + + +def file_to_graph(filename: str): + with open(filename, "r") as file: + graphs = debug_to_graph(file.readlines()) + g = [[nx.Graph() for _ in range(len(graphs[0]))] for _ in range(len(graphs))] + + for i, cusp in enumerate(graphs): + for j, graph in enumerate(cusp): + for v1, v2 in graph: + g[i][j].add_node(v1) + g[i][j].add_node(v2) + g[i][j].add_edge(v1, v2) + + return g + + +if __name__ == "__main__": + graphs = file_to_graph("logs/link-116084.log") + pos = nx.spring_layout(graphs[0][0]) + for i, graph in enumerate(graphs): + plt.figure(i) + nx.draw(graph[0], pos=nx.spring_layout(graph[0]), with_labels=True, font_weight="bold") + + plt.show() diff --git a/dev/symplectic_basis/large-db b/dev/symplectic_basis/large-db new file mode 100644 index 000000000..b9e4d460a --- /dev/null +++ b/dev/symplectic_basis/large-db @@ -0,0 +1,145 @@ +285558 +365263 +137474 +25666 +83741 +259456 +137990 +91439 +198059 +357551 +152251 +8742 +180232 +141102 +166589 +87677 +400218 +287798 +146476 +86585 +326468 +349598 +345888 +174870 +65112 +271538 +341265 +386833 +51447 +34195 +370741 +372867 +26116 +130100 +125100 +94210 +16993 +111351 +39814 +334339 +172287 +376568 +172266 +96897 +366049 +393535 +323526 +307215 +97211 +78367 +182660 +105005 +113579 +304433 +340167 +189852 +372507 +321058 +315682 +23640 +34936 +407750 +424292 +419355 +1891 +216571 +337329 +71550 +105417 +128914 +223829 +369165 +241154 +432247 +30273 +31032 +261297 +21895 +72609 +387860 +69475 +70292 +255041 +395567 +332010 +261648 +264555 +316718 +138808 +102246 +196510 +52150 +153421 +40827 +53665 +255449 +150391 +93354 +49948 +32714 +315729 +350829 +431682 +147485 +186750 +62341 +183477 +295680 +29433 +159488 +65130 +326593 +14393 +175887 +65986 +402124 +272927 +113674 +405690 +97049 +268124 +206323 +428658 +32859 +216692 +147019 +405520 +96431 +6205 +194494 +345231 +238026 +225334 +309348 +8552 +18550 +395218 +383999 +15335 +413972 +200858 +158844 +130806 +206765 +362976 diff --git a/dev/symplectic_basis/small-db b/dev/symplectic_basis/small-db new file mode 100644 index 000000000..f5e708212 --- /dev/null +++ b/dev/symplectic_basis/small-db @@ -0,0 +1,1000 @@ +207 +89507 +174765 +152218 +176575 +134368 +31163 +58162 +67251 +119847 +123629 +3182 +121687 +125514 +11625 +153072 +2400 +76154 +73599 +127122 +100591 +146529 +91076 +69030 +17725 +6478 +114923 +79763 +148583 +173009 +149160 +85704 +78188 +136538 +58447 +80756 +35655 +3180 +155043 +149052 +40181 +173628 +178913 +3882 +58078 +171017 +175203 +12708 +20197 +6339 +124325 +63040 +72190 +136586 +177806 +28199 +95735 +169152 +83576 +103681 +149997 +111760 +16315 +86239 +96245 +35543 +57832 +26545 +78439 +144360 +79564 +109397 +52545 +144735 +35114 +172453 +27765 +58191 +69339 +49700 +103362 +511 +176496 +39193 +171289 +145622 +23771 +98560 +132889 +24295 +51779 +118868 +105509 +143024 +140959 +1241 +70942 +175311 +24481 +131943 +63674 +29479 +175714 +53866 +55282 +137140 +115398 +71509 +73197 +95432 +120474 +133128 +120399 +67199 +81870 +47187 +122454 +75345 +151253 +114238 +179657 +44737 +89033 +85258 +70985 +68034 +56968 +174738 +35463 +149864 +85414 +67847 +90251 +93077 +153960 +153076 +31520 +1558 +132703 +86755 +140129 +38732 +22402 +129976 +95047 +60058 +156592 +69500 +172006 +4752 +93373 +131776 +90368 +16291 +99588 +35814 +52150 +74297 +39717 +32337 +52326 +54836 +109522 +85007 +89201 +145402 +52916 +98830 +44920 +25333 +75788 +11602 +134813 +66392 +149436 +72949 +82169 +11196 +134177 +111442 +93803 +62219 +137107 +108396 +143624 +179697 +151640 +39226 +18513 +93150 +77559 +12379 +25093 +7050 +135284 +176524 +103302 +9901 +5017 +164183 +1387 +47017 +119485 +109192 +1454 +88570 +108271 +81537 +100103 +131817 +160583 +21275 +47651 +168813 +112554 +9387 +49296 +100796 +41140 +117193 +64224 +167183 +9518 +69722 +65243 +99558 +54129 +134527 +120075 +92140 +134378 +17932 +66416 +54412 +32324 +13436 +95250 +42889 +106777 +127631 +80179 +126806 +43061 +19012 +43557 +60575 +155030 +153614 +86015 +40834 +80730 +71132 +76830 +150469 +139366 +80673 +146643 +66080 +93496 +63920 +157383 +114296 +9474 +146581 +125750 +133225 +89735 +62556 +9676 +59238 +66244 +62863 +56454 +112891 +133894 +67772 +142741 +24174 +76474 +46628 +75354 +765 +158973 +89987 +103545 +33025 +116802 +178996 +141148 +58822 +96865 +128613 +68553 +83728 +64958 +75051 +86616 +56929 +113872 +127650 +46824 +165079 +55721 +33987 +111693 +69879 +163310 +104905 +17856 +102265 +175079 +165235 +74843 +77544 +39938 +114165 +170815 +124323 +91503 +91146 +52125 +69804 +143937 +61557 +168039 +146670 +77271 +33307 +137303 +156307 +147284 +63358 +123040 +179862 +121084 +41835 +103916 +113432 +127689 +32314 +175790 +96177 +693 +141546 +54702 +87555 +36253 +70754 +70142 +124380 +123761 +60032 +91939 +3286 +124071 +106802 +95610 +46755 +95834 +11821 +3466 +89823 +167995 +122054 +176555 +67033 +514 +25465 +127047 +118904 +61545 +169502 +120811 +108045 +65563 +51444 +37945 +59000 +118024 +112865 +12663 +7871 +616 +57924 +156967 +150790 +40324 +67856 +125739 +180300 +48970 +118296 +79062 +88700 +96552 +15438 +151423 +106630 +136066 +64056 +60716 +101882 +56450 +114251 +83755 +143743 +3699 +88934 +29558 +59351 +83689 +171885 +171461 +113420 +63821 +85354 +84837 +149031 +73989 +23763 +35002 +132875 +154264 +143857 +114877 +156050 +9071 +36341 +129664 +10196 +88891 +47332 +147341 +98589 +73022 +151043 +171556 +1993 +85217 +176032 +144812 +175018 +121482 +153368 +27025 +126358 +151576 +148018 +126909 +133150 +95265 +176194 +167483 +39375 +137570 +77127 +3271 +144080 +64689 +76980 +162343 +36462 +119328 +167518 +47752 +98142 +155565 +52033 +117275 +106616 +95251 +156361 +133597 +152933 +809 +160318 +31580 +95923 +79590 +128212 +90275 +147266 +120443 +121606 +102649 +42067 +76392 +21860 +58149 +24302 +44534 +161065 +40555 +131647 +112008 +31169 +157097 +85412 +99755 +48121 +39975 +179702 +30764 +125535 +156854 +119421 +11171 +126473 +54630 +172838 +143652 +116233 +54125 +5921 +59935 +24947 +73486 +106091 +137042 +90242 +134223 +9903 +12260 +6444 +67974 +125086 +54433 +5361 +168120 +75044 +127599 +54672 +68455 +45919 +6482 +82838 +99657 +171697 +80503 +18339 +140103 +123831 +159075 +34271 +66093 +135412 +101073 +71807 +110909 +134942 +4653 +150146 +105441 +180139 +9127 +72990 +48185 +69477 +52354 +130224 +38730 +16924 +119528 +35402 +21271 +6080 +138739 +111220 +102579 +119963 +41132 +97427 +43130 +125994 +95326 +96275 +96831 +102181 +132782 +92174 +47178 +145860 +94369 +83430 +39506 +33913 +127369 +70182 +86724 +131883 +151985 +52492 +101540 +90775 +454 +25590 +110588 +49893 +120674 +87892 +102312 +137167 +94766 +111151 +140822 +68125 +13444 +21524 +43080 +47262 +136304 +141973 +58186 +91187 +162573 +174436 +125023 +29773 +17841 +44592 +77124 +26976 +92408 +151122 +8972 +158078 +22619 +55420 +132648 +104335 +10421 +178919 +159685 +29511 +23165 +22215 +134884 +125475 +59959 +132938 +49195 +124416 +127554 +80266 +42262 +47817 +126070 +150016 +173210 +62634 +30555 +109348 +78624 +96152 +100916 +161232 +60921 +40194 +62859 +116075 +96909 +86429 +55008 +86291 +164187 +163532 +120477 +156218 +79188 +127307 +64342 +158599 +85439 +12684 +169550 +690 +5871 +119851 +41272 +99825 +52353 +149834 +46016 +170995 +132276 +140879 +142790 +65280 +134065 +45710 +110883 +113609 +41125 +150501 +86807 +59112 +101570 +80811 +172841 +27492 +8406 +152787 +125904 +177987 +24034 +52804 +81927 +1575 +46138 +137905 +36743 +159914 +94018 +85090 +164707 +82227 +161915 +110392 +97819 +59508 +104773 +159407 +95192 +58571 +155033 +159452 +126723 +25120 +13321 +96645 +57794 +96699 +13044 +28167 +121541 +43563 +176554 +56117 +15406 +62704 +123005 +137590 +49773 +65803 +49595 +1644 +60399 +8509 +166040 +12587 +130044 +6595 +80488 +65845 +59534 +30995 +57126 +90933 +155198 +171689 +80423 +168414 +113460 +165460 +100987 +120042 +101612 +40811 +111581 +130922 +102408 +173859 +2300 +173675 +142146 +60551 +54369 +66014 +104671 +135158 +2185 +154470 +133163 +95417 +35144 +77604 +32697 +100357 +29781 +159895 +70248 +64501 +146872 +12688 +52907 +110269 +3441 +19785 +94853 +98331 +45524 +18615 +29457 +5865 +159253 +140304 +171058 +57214 +93423 +22085 +64016 +126109 +56615 +60309 +64111 +90783 +10413 +9677 +99417 +152156 +31894 +84110 +21916 +48674 +17790 +61496 +61622 +134426 +26551 +3055 +123122 +86041 +99370 +8959 +169886 +27506 +95346 +27232 +62323 +166386 +80111 +51602 +117608 +154023 +421 +124802 +11243 +136450 +54409 +154273 +81922 +36535 +53899 +156780 +76615 +141784 +156623 +107662 +55091 +59208 +27660 +32462 +76300 +81977 +17167 +118001 +106288 +65909 +3635 +90514 +42279 +74197 +146538 +50083 +75439 +103733 +57245 +164320 +51053 +16145 +134655 +138059 +80679 +126440 +38035 +55122 +84157 +24786 +177757 +63759 +66134 +4988 +38464 +98930 +90588 +129738 +125847 +45275 +26277 +62288 +116580 +158903 +108951 +131197 +8341 +19255 +6149 +18737 +140466 +20724 +2358 +62649 +125922 +111658 +145786 +1121 +45932 +143814 +62795 +51524 +77474 +15816 +146782 +29533 +1378 +114697 +43873 +58155 +69917 +66044 +69485 +30917 +69364 +155601 +101203 +134749 +21640 +157542 +150645 +456 +165048 +125247 +127262 +156403 +35685 +8081 +141611 +87481 +172040 +179514 +160079 +145690 +50018 +62090 +133040 +159736 +19929 +32410 +39588 +137645 +142398 +127074 +98180 +40260 +179026 +49646 +160799 +172860 +165441 +115019 +100340 +160268 +54325 +54242 +20925 +122392 +174259 +89236 +26535 +9253 +39763 +109271 +38297 +93417 +146868 +33973 +107369 +9099 +60825 +136293 +153043 +87450 +46865 +9320 +149201 +11529 +121334 +22880 +92036 +121457 \ No newline at end of file diff --git a/dev/symplectic_basis/symplectic_basis_main.c b/dev/symplectic_basis/symplectic_basis_main.c new file mode 100644 index 000000000..1a9ca8d4b --- /dev/null +++ b/dev/symplectic_basis/symplectic_basis_main.c @@ -0,0 +1,109 @@ +/* + * + */ + +#include "SnapPea.h" +#include "kernel.h" +#include "unix_cusped_census.h" +#include "unix_file_io.h" +#include "addl_code.h" +#include + +void printMatrix(int**, int, int); +int omega(int *, int *, int); +void test_matrix(int **, int, int); + +int main(void) { + int i, **eqns, num_rows, num_cols; + Triangulation *theTriangulation; + + int fromFile = 0; + + int count = 1; + int numTet[] = {5}; + int index[] = {4}; + + char *error[] = { + "CuspedCensusData/link-60819.tri", /* curve holonomy */ + "CuspedCensusData/link-83653.tri", /* curve holonomy */ + "CuspedCensusData/link-116084.tri", /* */ + }; + + + for (i = 0; i < count; i++) { + if (fromFile == 1) { + printf("Triangulation: %s\n", error[i]); + theTriangulation = read_triangulation(error[i]); + } else { + theTriangulation = GetCuspedCensusManifold("", numTet[i], oriented_manifold, index[i]); + printf("Num Tet: %d Index: %d\n", numTet[i], index[i]); + } + + if (theTriangulation != NULL) { + if (get_orientability(theTriangulation) == nonorientable_manifold) + continue; + + eqns = get_symplectic_basis(theTriangulation, &num_rows, &num_cols, 1); + printMatrix(eqns, num_cols, num_rows); + test_matrix(eqns, num_rows, num_cols); + + printf("---------------------------\n"); + free_symplectic_basis(eqns, num_rows); + free_triangulation(theTriangulation); + } else + printf("Couldn't read census manifold.\n"); + } + + return 0; +} + +void test_matrix(int **basis, int dual_rows, int dual_cols) { + int k, retval1, retval2; + + for (k = 0; k < dual_rows / 2; k ++) { + retval1 = ABS(omega(basis[2 * k], basis[2 * k + 1], dual_cols)); + + if (2 * k + 2 < dual_rows) + retval2 = ABS(omega(basis[2 * k], basis[2 * k + 2], dual_cols)); + else + retval2 = 0; + + if (2 * k + 3 < dual_rows) + retval2 += ABS(omega(basis[2 * k + 1], basis[2 * k + 3], dual_cols)); + + if (retval1 == 2 && retval2 == 0) { + continue; + } + + printf("Failed %d\n", k); +// return; + } + + printf("Passed\n"); +} + +int omega(int *v1, int *v2, int num_cols) { + int i, yyval = 0; + + for (i = 0; i < num_cols / 3; i++) { + yyval += ((v1[3 * i] - v1[3 * i + 2]) * (v2[3 * i + 1] - v2[3 * i + 2]) + - (v1[3 * i + 1] - v1[3 * i + 2]) * (v2[3 * i] - v2[3 * i + 2])); + } + + return yyval; +} + + +void printMatrix(int** M, int numCols, int numRows) { + int i, j; + + for (i = 0; i < numRows; i ++) { + printf("["); + + for (j = 0; j < numCols - 1; j ++) { + printf(" %d,", M[i][j]); + } + + printf(" %d]\n", M[i][numCols - 1]); + } +} diff --git a/dev/symplectic_basis/symplectic_basis_old.h b/dev/symplectic_basis/symplectic_basis_old.h new file mode 100644 index 000000000..7120b9c5e --- /dev/null +++ b/dev/symplectic_basis/symplectic_basis_old.h @@ -0,0 +1,244 @@ +// +// Created by joshu on 16/05/2023. +// + +#ifndef SNAPPEA_SYMPLECTIC_BASIS_H +#define SNAPPEA_SYMPLECTIC_BASIS_H + +#endif //SNAPPEA_SYMPLECTIC_BASIS_H + +#include "triangulation.h" +#include + + +/* + * Queue + */ + +struct Queue { + int front; // First element of queue + int rear; // Last element of queue + int len; // num of elements + int size; // array size + int *array; +}; + +/** + * Graph + */ + +struct EdgeNode { + int y; /** cusp region index */ + int edgeClass; /** edgeClass of the edge for edges in the end multi graph */ + int nextFace; /** face the path crosses to the next node */ + int prevFace; /** face the path crosses to the prev node */ + int insideVertex; /** inside vertex of the path */ + struct EdgeNode *next; /** next node in doubly linked list */ + struct EdgeNode *prev; /** prev node in doubly linked list */ +}; + +struct Graph { + struct EdgeNode *edge_list_begin; /** header node of doubly linked list */ + struct EdgeNode *edge_list_end; /** tail node of doubly linked list */ + struct CuspRegion **pRegion; /** list of regions in the graph */ + int *degree; /** degree of each vertex */ + int *color; /** color a tree bipartite */ + int nVertices; /** number of vertices in the graph */ + int directed; /** is the graph directed */ +}; + +struct CuspEndPoint { + int cuspIndex; + int edgeClass1; + int edgeClass2; + int pos; +}; + +struct EndMultiGraph { + int e0; /** edge connecting vertices of the same color */ + struct Graph *multi_graph; /** tree with extra edge of cusps */ +}; + +/** + * Dual Curves + * + * Each oscillating curve is made up of two components, accessed using macros + * FIRST and SECOND. For each component we have the starting endpoint, + * *endpoints[curveNum][START] and finish endpoint *endpoint[curveNum][FINISH]. + * The path of the curve is stored as a double linked list with header and tail nodes, + * the header is curves[curveNum][START] and the tail is curve[curveNum][FINISH]. + */ + +struct extra { + int curve[4][4]; /** oscillating curve holonomy for a cusp triangle */ +}; + +struct PathEndPoint { + int face; /** face containg the short rectangle carrying the curve */ + int vertex; /** vertex we dive through the manifold along */ + int regionIndex; /** index of the region the endpoint lies in */ + struct CuspRegion *region; /** pointer to the region the endpoint lies in */ +}; + +struct CurveComponent { + int edgeClass[2]; + struct EdgeNode curves_begin; /** header node of doubbly linked list */ + struct EdgeNode curves_end; /** tailer node of doubbly linked list */ + struct PathEndPoint endpoints[2]; /** path end points */ + struct CurveComponent *next; /** next dual curve in doubly linked list */ + struct CurveComponent *prev; /** prev dual curve in doubly linked list */ +}; + +struct OscillatingCurves { + int numCurves; + int *edgeClass; + struct CurveComponent *curve_begin; /** array of doubly linked lists of dual curves */ + struct CurveComponent *curve_end; /** array of doubly linkek lists of dual curves */ +}; + +/** + * Cusp Triangulation + * + * CuspTriangle stores information about a triangle in the + * cusp triangulation. The homology curves bound a fundamental + * domain, and cusp regions store the information for + * intersection of this domain with each cusp triangle. When + * we add oscillating curves, these regions are divided further. +*/ + +struct CuspVertex { + int edgeClass; + int edgeIndex; + EdgeClass *edge; + int v1; + int v2; +}; + +struct CuspTriangle { + Tetrahedron *tet; /** tetrahedron the triangle comes from */ + int tetIndex; /** tet->index */ + int tetVertex; /** vertex the triangle comes from */ + int numCurves; /** number of homology curves on the triangle */ + struct CuspVertex vertices[4]; /** information about each vertex */ + struct CuspTriangle *neighbours[4]; /** triangle neighbouring a face */ + struct CuspTriangle *next; /** next cusp triangle on doubly linked list */ + struct CuspTriangle *prev; /** prev cusp triangle on doubly linkled list */ +}; + +struct CuspRegion { + struct CuspTriangle *tri; /** cusp triangle the region lies on */ + int tetIndex; /** tri->tetIndex */ + int tetVertex; /** tri->tetVertex */ + int index; /** index of the cusp region */ + int curve[4][4]; /** looking at face, number of curves between the region and vertex */ + int adjTri[4]; /** does the region meet this edge of the cusp triangle */ + struct CuspRegion *adjRegions[4]; /** index of the adjacent regions */ + int dive[4][4]; /** can we dive along the face into this vertex */ + struct CuspRegion *next; /** next cusp region in doubly linked list */ + struct CuspRegion *prev; /** prev cusp region in doubly linked list */ +}; + +struct CuspStructure { + int intersectTetIndex; /** index of the intersection triangle */ + int intersectTetVertex; /** vertex of the intersection triangle */ + int numEdgeClasses; /** number of edge classes in the boundary */ + int numCuspTriangles; /** number of cusp triangle in the boundary */ + int numCuspRegions; /** number of cusp regions in the boundary */ + int numDualCurves; /** number of dual curves in the boundary */ + Triangulation *manifold; /** manifold */ + Cusp *cusp; /** which cusp is the boundary in */ + struct Graph *dual_graph; /** dual graph of the cusp region */ + struct CuspTriangle cusp_triangle_begin; /** header node of doubly linked list of cusp triangles */ + struct CuspTriangle cusp_triangle_end; /** tail node of doubly linked list of cusp triangles */ + struct CuspRegion cusp_region_begin; /** header node of doubly linked list of cusp regions */ + struct CuspRegion cusp_region_end; /** tail node of doubly linked list of cusp regions */ +}; + +// Graph +struct Graph *init_graph(int, bool); +void free_graph(struct Graph *); +int insert_edge(struct Graph *, int, int, bool); +void delete_edge(struct Graph *, int, int, bool); +int edge_exists(struct Graph *, int, int); + +// Symplectic Basis +int *gluing_equations_for_edge_class(Triangulation *, int); +int **get_symplectic_equations(Triangulation *, int *, int *, int); + +struct CuspStructure *init_cusp_structure(Triangulation *, Cusp *); +void free_boundary(struct CuspStructure **, int); +void init_cusp_triangulation(Triangulation *, struct CuspStructure *); +int flow(struct CuspTriangle *, int); +void label_triangulation_edges(Triangulation *); +struct CuspTriangle *find_cusp_triangle(struct CuspTriangle *, struct CuspTriangle *, struct CuspTriangle *, int); +void label_cusp_vertex_indices(struct CuspTriangle *, struct CuspTriangle *, int); +void walk_around_cusp_vertex(struct CuspTriangle *, int, int); +void init_cusp_region(struct CuspStructure *); +int init_intersect_cusp_region(struct CuspStructure *, struct CuspTriangle *, int); +int init_normal_cusp_region(struct CuspStructure *, struct CuspTriangle *, int); +void set_cusp_region_data(struct CuspRegion *, struct CuspTriangle *, int [4], int [4], int); +void update_adj_region_data(struct CuspRegion *, struct CuspRegion *); +struct CuspRegion *find_adj_region(struct CuspRegion *, struct CuspRegion *, struct CuspRegion *, int); +struct OscillatingCurves*init_oscillating_curves(Triangulation *, int *); +void free_oscillating_curves(struct OscillatingCurves *); +void find_intersection_triangle(Triangulation *, struct CuspStructure *); + +/** + * Construct Oscillating Curves and calculate holonomy + */ + +void do_oscillating_curves(struct CuspStructure **, struct OscillatingCurves *, struct EndMultiGraph *); +void do_one_dual_curve(struct CuspStructure **, struct CurveComponent *, struct CurveComponent *, struct EndMultiGraph *, int); +void do_one_cusp(struct CuspStructure *, struct CurveComponent *, int); +struct Graph * construct_cusp_region_dual_graph(struct CuspStructure *); +void print_debug_info(Triangulation *, struct CuspStructure **, struct OscillatingCurves *, int); +struct CurveComponent *init_dual_curve(struct CurveComponent *, int, int); +void find_path_endpoints(struct Graph *, struct CurveComponent *, struct CurveComponent *, int, int); +void find_single_endpoints(struct Graph *, struct PathEndPoint *, struct PathEndPoint *, int, int, bool); +void update_path_info(struct Graph *g, struct CurveComponent *, int); +void update_path_endpoint_info(struct CuspRegion *, struct EdgeNode *, struct PathEndPoint *, int, int); +void split_cusp_regions_along_path(struct CuspStructure *, struct CurveComponent *); +struct CuspRegion *update_cusp_region(struct CuspRegion *region, struct EdgeNode *, struct PathEndPoint *, int, int); +void update_cusp_triangle(struct CuspRegion *, struct CuspRegion *, struct CuspRegion *, struct EdgeNode *); +void update_cusp_triangle_endpoints(struct CuspRegion *, struct CuspRegion *, struct CuspRegion *, struct PathEndPoint *, struct EdgeNode *, int); +void copy_region(struct CuspRegion *, struct CuspRegion *); +void calculate_holonomy(Triangulation *, int **, int); +void inside_vertex(struct CuspRegion *, struct EdgeNode *); + +/** + * Queue Data Structure + */ + +struct Queue *init_queue(int); +struct Queue *enqueue(struct Queue *, int); +int dequeue(struct Queue *); +struct Queue *resize_queue(struct Queue *); +int empty_queue(struct Queue *); +void free_queue(struct Queue *); + +/** + * Graph for Breadth First Search + */ + +void init_search(struct Graph *, bool *, bool *, int *); +void bfs(struct Graph *, int, bool *, bool *, int *); +void find_path(int, int, int *, struct EdgeNode *); + + +/** + * Spanning Tree for End Multi Graph + */ + +struct EndMultiGraph *init_end_multi_graph(Triangulation *, int *); +void free_end_multi_graph(struct EndMultiGraph *); +int insert_edge_end_multi_graph(struct Graph *, int, int, int, bool); +void spanning_tree(struct Graph *, struct Graph *, int, int *); +void cusp_graph(Triangulation *, struct Graph *); +void color_graph(struct Graph *); +int *find_tree_edges(struct Graph *, int); +int find_same_color_edge(struct Graph *, struct Graph *, int *); +int find_path_len(int, int, int *, int); +struct CuspEndPoint *find_multi_graph_path(struct Graph *, Triangulation *, int, int, int *); +struct CuspEndPoint *find_cusp_endpoint_edge_classes(struct Graph *, struct EdgeNode *, struct EdgeNode *, int, int *); +void find_edge_ends(struct Graph *, Triangulation *, int, int *, int *); +void print_graph(struct Graph *, int); diff --git a/dev/symplectic_basis/symplectic_basis_test.c b/dev/symplectic_basis/symplectic_basis_test.c new file mode 100644 index 000000000..6cb1ddb59 --- /dev/null +++ b/dev/symplectic_basis/symplectic_basis_test.c @@ -0,0 +1,112 @@ +// +// Created by joshu on 19/03/2023. +// + +#include +#include "kernel.h" +#include "kernel_namespace.h" +#include "unix_cusped_census.h" +#include "addl_code.h" +#include "symplectic_basis_old.h" + +void testDual(void); +int omega(int *, int *, int); +void printMatrix(int **, int, int); + +int main() { + printf("Testing Symplectic Basis: \n"); + testDual(); +} + +void testDual(void) { + int **basis, dual_rows, dual_cols, i, j, k, l, retval1, retval2; + Triangulation *theTriangulation; + + int failed[] = {0, 0, 0}; + int count[] = {0, 0, 0}; + int index[][3] = {{5, 0, 110}, + {6, 0, 950}, + {7, 0, 3550} + }; + int *passed[3]; + passed[0] = NEW_ARRAY(index[0][2] - index[0][1], int); + passed[1] = NEW_ARRAY(index[1][2] - index[1][1], int); + passed[2] = NEW_ARRAY(index[2][2] - index[2][1], int); + + for (i = 0; i < 3; i++) + for (j = 0; j < index[i][2] - index[i][1]; j++) + passed[i][j] = 1; + + for (i = 0; i < 3; i++) { + for (j = index[i][1]; j < index[i][2]; j++) { + theTriangulation = GetCuspedCensusManifold("", index[i][0], oriented_manifold, j); + + if (get_orientability(theTriangulation) == nonorientable_manifold || get_num_cusps(theTriangulation) > 1) { + free_triangulation(theTriangulation); + continue; + } + +// printf("Num Tet: %d Index: %d \n", index[i][0], j); + + basis = get_symplectic_basis(theTriangulation, &dual_rows, &dual_cols, 0); + + for (k = 0; 2 * k + 1 < dual_rows; k++) { + retval1 = ABS(omega(basis[2 * k], basis[2 * k + 1], dual_cols)); + + retval2 = 0; + for (l = 2 * k + 2; 2 * l < dual_rows; l++) { + retval2 += ABS(omega(basis[2 * k], basis[2 * l], dual_cols)); + retval2 += ABS(omega(basis[2 * k + 1], basis[2 * l], dual_cols)); + } + + if (retval1 == 2 && retval2 == 0) { +// printf("Passed\n"); + continue; + } + +// printf("Failed Num Tet: %d Index: %d \n", index[i][0], j); + //printMatrix(basis, dual_cols, dual_rows); + passed[i][j - index[i][1]] = 0; + failed[i]++; + break; + } + count[i]++; + + free_triangulation(theTriangulation); + free_symplectic_basis(basis, dual_rows); + } + } + + for (i = 0; i < 3; i++) { + printf(" (Num. of Tet %d) Failed: %d out of %d tests\n", index[i][0], failed[i], count[i]); + } + + my_free(passed[0]); + my_free(passed[1]); + my_free(passed[2]); +} + +int omega(int *v1, int *v2, int num_cols) { + int i, yyval = 0; + + for (i = 0; i < num_cols / 3; i++) { + yyval += ((v1[3 * i] - v1[3 * i + 2]) * (v2[3 * i + 1] - v2[3 * i + 2]) + - (v1[3 * i + 1] - v1[3 * i + 2]) * (v2[3 * i] - v2[3 * i + 2])); + } + + return yyval; +} + +void printMatrix(int** M, int numCols, int numRows) { + int i, j; + + for (i = 0; i < numRows; i++) { + printf("["); + + for (j = 0; j < numCols - 1; j++) { + printf(" %d,", M[i][j]); + } + + printf(" %d]\n", M[i][numCols - 1]); + } +} diff --git a/dev/symplectic_basis/test_base.py b/dev/symplectic_basis/test_base.py new file mode 100644 index 000000000..feaee765f --- /dev/null +++ b/dev/symplectic_basis/test_base.py @@ -0,0 +1,161 @@ +import random +from datetime import datetime +import snappy +import unittest +import spherogram +from tqdm import tqdm + +start = 0 +end = 1 +scale = 1000 +num_tests = 5000 +test = "random" + +if len(snappy.HTLinkExteriors(crossings=15)) == 0: + file_name = "small-db" +else: + file_name = "large-db" + + +def is_symplectic(M): + """ + Test if the matrix M is symplectic + :param M: square matrix + :return: true or false + """ + n = M.dimensions() + + for i in range(n[0]): + for j in range(i, n[1]): + omega = abs(symplectic_form(M.data[i], M.data[j])) + + if i % 2 == 0 and j % 2 == 1 and j == i + 1: + if omega != 2: + return False + elif omega: + return False + + return True + + +def symplectic_form(u, v): + return sum([u[2 * i] * v[2 * i + 1] - u[2 * i + 1] * v[2 * i] + for i in range(len(u) // 2)]) + + +def save_manifold(index: int): + M = snappy.HTLinkExteriors[index] + M.save(f"CuspedCensusData/link-{index}.tri") + + +def process_manifold(index: int): + M = snappy.HTLinkExteriors[index] + label = M.identify()[0] if len(M.identify()) > 0 else "" + + if index == 0: + return True + + # print(index) + + basis = M.symplectic_basis() + result = is_symplectic(basis) + + if result: + string = "Passed" + else: + string = "Failed" + + print(f"Testing: {str(index)} {(20 - len(str(index))) * ' '} {str(label)} {(40 - len(str(label))) * ' '} {string}") + + return result + + +def random_link_exteriors(n: int, n_tet: int, n_cusps: int): + for i in range(n): + L = spherogram.random_link(n_tet, n_cusps, alternating=True) + M = spherogram.Link.exterior(L) + print(M.num_cusps()) + M.save(f"CuspedCensusData/link-{n_tet}-{n_cusps}-{i}.tri") + + +def generate_tests(output: bool): + print(f"[{datetime.now().strftime('%d-%m-%y %H:%M:%S')}] Generating symplectic basis tests") + for _ in range(2000): + index = random.randint(1, len(snappy.HTLinkExteriors) - 1) + process_manifold(index) + + if output: + with open(file_name, "a") as file: + file.write(f"{index}\n") + + +def testing_string(num: int = 0): + time = datetime.now().strftime('%d-%m-%y %H:%M:%S') + + if test == "random": + return f"[{time}] Testing ({test}): {num} manifolds\n" + elif test == "sequence": + return f"[{time}] Testing ({test}): {scale * start} - {scale * end}\n" + + +def test_link_complements(): + """ Pick 'num_test' random manifolds from HTLinkExteriors and test symplectic_basis() """ + # with open(file_name, "r") as file: + # lst = file.readlines() + + # manifolds = list(set([int(x[:-1]) for x in lst])) + + # with open("logs/total.log", "a") as file: + # file.write(testing_string(len(manifolds))) + print(testing_string(num_tests)) + + # if test == "random": + # result = [process_manifold(i) for i in range(len(manifolds))] + # else: + # result = [process_manifold(i) for i in range(scale * start, scale * end)] + manifolds = [random.randint(1, len(snappy.HTLinkExteriors)) for _ in range(num_tests)] + result = [process_manifold(i) for i in manifolds] + + # with open("logs/total.log", "a") as file: + # file.write(f"[{datetime.now().strftime('%d-%m-%y %H:%M:%S')}] Passed: {sum(result)} / {len(result)}\n") + print(f"[{datetime.now().strftime('%d-%m-%y %H:%M:%S')}] Passed: {sum(result)} / {len(result)}") + + +class TestSymplecticBasis(unittest.TestCase): + def test_knot_complements(self): + i = 0 + for M in tqdm(snappy.CensusKnots, desc="Knots...", ncols=120): + with self.subTest(i=i): + # print(M.identify()[0]) + basis = M.symplectic_basis() + self.assertTrue(is_symplectic(basis), str(M.identify()[0])) + i += 1 + + # @unittest.skip + def test_link_complements(self): + i = 0 + for M in tqdm(snappy.HTLinkExteriors[1:1000], desc="Links...", ncols=120): + with self.subTest(i=i): + # print(M.identify()[0]) + basis = M.symplectic_basis() + self.assertTrue(is_symplectic(basis)) + i += 1 + + @unittest.skip + def test_random_links(self): + iterations = 10 + + for i in tqdm(range(iterations), desc="Random Links...", ncols=120): + with self.subTest(i=i): + L = spherogram.random_link(100, num_components=random.randint(3, 10), alternating=True) + M = spherogram.Link.exterior(L) + basis = M.symplectic_basis() + self.assertTrue(is_symplectic(basis)) + + +if __name__ == "__main__": + test_link_complements() + # generate_tests(True) + # unittest.main() + + # save_manifold(116084) diff --git a/dev/symplectic_basis/test_linux.py b/dev/symplectic_basis/test_linux.py new file mode 100644 index 000000000..e4fde9391 --- /dev/null +++ b/dev/symplectic_basis/test_linux.py @@ -0,0 +1,69 @@ +from datetime import datetime +import snappy +from test_base import is_symplectic, testing_string +from multiprocessing import Pool +import itertools +import random + +start = 0 +end = 1 +scale = 500 +test = "random" + +print(f"[{datetime.now().strftime('%d-%m-%y %H:%M:%S')}] Building test set") + +manifolds = [random.randint(1, len(snappy.HTLinkExteriors)) for _ in range(scale * (end - start))] +manifolds_tri = [snappy.HTLinkExteriors[i] for i in manifolds] +manifolds_labels = [M.identify()[0] for M in manifolds_tri if len(M.identify()) > 0] + + +def process_manifold(i: int, output: bool = True): + M = manifolds_tri[i] + index = manifolds[i] + label = manifolds_labels[i] + + # print(index) + + if index == 0: + return True + + basis = M.symplectic_basis() + result = is_symplectic(basis) + + if result: + string = "Passed" + else: + string = "Failed" + + if output: + with open(f"logs/links-{i // scale}.log", "a") as file: + file.write(f"Testing: {str(index)} {(20 - len(str(index))) * ' '} {str(label)} {(40 - len(str(label))) * ' '} {string}\n") + + print(f"Testing: {str(index)} {(20 - len(str(index))) * ' '} {str(label)} {(40 - len(str(label))) * ' '} {string}") + + return result + + +def test_link_complements_pool(): + with open("logs/total.log", "a") as file: + file.write(testing_string(scale * (end - start))) + + with Pool(maxtasksperchild=25) as pool: + if test == "random": + result = pool.imap(process_manifold, range(len(manifolds))) + else: + result = pool.imap(process_manifold, range(start * scale, end * scale)) + + for _ in range(start, end): + lst = list(itertools.islice(result, scale)) + + time = datetime.now().strftime('%d-%m-%y %H:%M:%S') + # print(f"[{time}] Passed: {sum(lst)} / {len(lst)}") + + with open("logs/total.log", "a") as file: + file.write(f"[{time}] Passed: {sum(lst)} / {len(lst)}\n") + + +if __name__ == "__main__": + test_link_complements_pool() + # unittest.main() diff --git a/dev/symplectic_basis/test_windows.py b/dev/symplectic_basis/test_windows.py new file mode 100644 index 000000000..374555e33 --- /dev/null +++ b/dev/symplectic_basis/test_windows.py @@ -0,0 +1,82 @@ +from datetime import datetime +import snappy +from multiprocessing import Pool +import itertools +from test_base import is_symplectic, testing_string +import random + +start = 0 +end = 1 +scale = 1000 +test = "random" + +if len(snappy.HTLinkExteriors(crossings=15)) == 0: + file_name = "small-db" +else: + file_name = "large-db" + + +def process_manifold(index: int, output: bool = True): + if test == "random": + index = random.randint(1, len(snappy.HTLinkExteriors) - 1) + + M = snappy.HTLinkExteriors[index] + label = M.identify()[0] if len(M.identify()) > 0 else "" + # print(label) + + if index == 0: + return True + + basis = M.symplectic_basis() + result = is_symplectic(basis) + + if result: + string = "Passed" + else: + string = "Failed" + + if output: + with open("logs/links-0.log", "a") as file: + file.write(f"Testing: {str(index)} {(20 - len(str(index))) * ' '} {str(label)} {(40 - len(str(label))) * ' '} {string}\n") + + # print(f"Testing: {str(index)} {(20 - len(str(index))) * ' '} {str(label)} {(40 - len(str(label))) * ' '} {string}") + + return result + + +def test_link_complements_pool(manifolds): + with open("logs/total.log", "a") as file: + if test == "database": + length = len(manifolds) + else: + length = scale * (end - start) + + file.write(testing_string(length)) + print(testing_string(length)) + + with Pool(maxtasksperchild=25) as pool: + if test == "database": + result = pool.imap(process_manifold, manifolds) + else: + result = pool.imap(process_manifold, range(start * scale, end * scale)) + + for _ in range(start, end): + lst = list(itertools.islice(result, scale)) + + # lst = list(result) + time = datetime.now().strftime('%d-%m-%y %H:%M:%S') + print(f"[{time}] Passed: {sum(lst)} / {len(lst)}") + + with open("logs/total.log", "a") as file: + file.write(f"[{time}] Passed: {sum(lst)} / {len(lst)}\n") + + +if __name__ == "__main__": + with open(file_name, "r") as file: + lst = file.readlines() + + manifolds = list(set([int(x[:-1]) for x in lst])) + test_link_complements_pool(manifolds) + # test_link_complements() + # generate_tests() + # unittest.main() diff --git a/kernel/headers/SnapPea.h b/kernel/headers/SnapPea.h index 30434d865..1d4f79cec 100644 --- a/kernel/headers/SnapPea.h +++ b/kernel/headers/SnapPea.h @@ -2590,6 +2590,22 @@ extern void free_symmetry_group_presentation(SymmetryGroupPresentation *group); * Frees the storage occupied by a SymmetryGroupPresentation. */ +/************************************************************************/ +/* */ +/* symplectic_basis.c */ +/* */ +/************************************************************************/ + +extern int** get_symplectic_basis(Triangulation *manifold, int *, int *, int); +/**< + * Returns the symplectic basis + */ + +extern void free_symplectic_basis(int **, int); +/**< + * Free the memory for the symplectic basis + */ + /************************************************************************/ /* */ diff --git a/kernel/kernel_code/symplectic_basis.c b/kernel/kernel_code/symplectic_basis.c new file mode 100644 index 000000000..97bb9dbbe --- /dev/null +++ b/kernel/kernel_code/symplectic_basis.c @@ -0,0 +1,3035 @@ +/** + * Symplectic Basis + * + * Computes a symplectic basis of a triangulated knot with orientable torus cusps. + * This symplectic matrix extends the Neumann-Zagier matrix to one which is symplectic + * up to factors of 2, and which arises from the triangulation of the manifold. + * + * See - https://arxiv.org/abs/2208.06969 + * + */ + +#include +#include +#include "SnapPea.h" +#include "kernel.h" + +#define ATLEAST_TWO(a, b, c) ((a) && (b)) || ((a) && (c)) || ((b) && (c)) +#define TRI_TO_INDEX(tet_index, tet_vertex) (4 * (tet_index) + (tet_vertex)) + +#define COPY_PATH_ENDPOINT(new, old) { \ + (new)->vertex = (old)->vertex; \ + (new)->face = (old)->face; \ + (new)->tri = (old)->tri; \ + (new)->region_index = (old)->region_index; \ + (new)->region = (old)->region; \ + (new)->node = (old)->node; \ + (new)->num_adj_curves = (old)->num_adj_curves; \ + } + +#define COPY_PATH_NODE(new, old) { \ + (new)->next = NULL; \ + (new)->prev = NULL; \ + (new)->next_face = (old)->next_face; \ + (new)->prev_face = (old)->prev_face; \ + (new)->inside_vertex = (old)->inside_vertex; \ + (new)->cusp_region_index = (old)->cusp_region_index; \ + (new)->tri = (old)->tri; \ + } + +enum pos { + START, + FINISH +}; + +static int debug = 0; + +/** + * Queue + */ + +typedef struct Queue { + int front; + int rear; + int len; + int size; + int *array; +} Queue ; + +/** + * Graph + */ + +typedef struct EdgeNode { + int y; + struct EdgeNode *next; + struct EdgeNode *prev; +} EdgeNode; + +typedef struct Graph { + EdgeNode *edge_list_begin; /** header node of doubly linked list */ + EdgeNode *edge_list_end; /** tailer node ... */ + int *degree; /** degree of each vertex */ + int *color; /** color a tree bipartite */ + int num_vertices; /** number of vertices in the graph */ + Boolean directed; /** is the graph directed */ +} Graph; + +/** + * The End multi graph is a quotient of certain Heegard surface by a map which + * collapses each boundary component of the manifold to a point and collapses + * each annulus around an edge of the triangulation to an edge. + */ + +typedef struct CuspEndPoint { + int cusp_index; + int edge_class[2]; + struct CuspEndPoint *next; + struct CuspEndPoint *prev; +} CuspEndPoint; + +typedef struct EndMultiGraph { + int e0; /** edge connecting vertices of the same color */ + int num_edge_classes; + int num_cusps; + int **edges; /** edge_class[u][v] is the edge class of the edge u->v */ + Boolean *edge_classes; /** which edge classes are in the multigraph */ + Graph *multi_graph; /** tree with extra edge of cusps */ +} EndMultiGraph; + +/** + * Dual Curves + * + * Each oscillating curve contributes combinatorial holonomy, we store this in + * curve[4][4] in a similar way to the curve[4][4] attribute of a Tetrahedron. + * An array of size num_edge_classes is attached to each Tetrahedron. + * tet->extra[edge_class]->curve[v][f] is the intersection number of + * the oscillating curve associated to edge_class with the face 'f' of the + * cusp triangle at vertex 'v' of tet. + */ + +struct extra { + int curve[4][4]; /** oscillating curve holonomy for a cusp triangle */ +}; + +/** + * Path End Points + * + * Path endpoints can have different states of initialisation. As a convention + * if the pointers tri and region are NULL then endpoint is not initialised. + * If tri is not NULL and region is NULL then the endpoint is initialised but + * the region is not known either because it has not been choosen or we have + * split along the curve. In either of the previous cases the tri pointer is + * still valid. If tri is not NULL and region is not NULL then the region + * pointer is valid and tri = region->tri. + * + * Oscillating curves consist of a path through the end multi graph, on each + * cusp of the path, we have a curve component, which contains a path through + * the cusp region graph on the cusp, and two path end points, one at either + * end of the curve. + */ + +typedef struct PathEndPoint { + FaceIndex face; /** face containg the short rectangle carrying the curve */ + VertexIndex vertex; /** vertex we dive through the manifold along */ + int region_index; /** index of the region the endpoint lies in */ + int num_adj_curves; /** where the curve dives into the manifold */ + struct PathNode *node; /** pointer to the path node which connects to the endpoint */ + struct CuspRegion *region; /** pointer to the region the endpoint lies in */ + struct CuspTriangle *tri; /** pointer to the cusp triangle the endpoint lies in */ +} PathEndPoint; + +typedef struct PathNode { + int cusp_region_index; + FaceIndex next_face; /** face the path crosses to the next node */ + FaceIndex prev_face; /** face the path crosses to the prev node */ + VertexIndex inside_vertex; /** inside vertex of the path */ + struct CuspTriangle *tri; /** cusp triangle the node lies in */ + struct PathNode *next; /** next node in doubly linked list */ + struct PathNode *prev; +} PathNode; + +typedef struct CurveComponent { + int edge_class[2]; /** edge classes at path end points */ + int cusp_index; /** which cusp does the curve lie in */ + PathNode path_begin; /** header node of doubbly linked list */ + PathNode path_end; /** tailer node of ... */ + PathEndPoint endpoints[2]; /** path end points */ + struct CurveComponent *next; /** next curve component in doubly linked list */ + struct CurveComponent *prev; /** prev ... */ +} CurveComponent; + +typedef struct OscillatingCurves { + int num_curves; + int *edge_class; + CurveComponent *curve_begin; /** array of doubly linked lists of dual curves */ + CurveComponent *curve_end; /** array of ... */ +} OscillatingCurves; + +/** + * Cusp Triangulation + * + * CuspTriangle stores information about a triangle in the cusp triangulation. + * The homology curves bound a fundamental domain, and cusp regions store the + * information for intersection of this domain with each cusp triangle. When + * we add oscillating curves, these regions are divided further. +*/ + +typedef struct CuspVertex { + int edge_class; + int edge_index; + EdgeClass *edge; + VertexIndex v1; + VertexIndex v2; +} CuspVertex; + +typedef struct CuspTriangle { + Tetrahedron *tet; /** tetrahedron the triangle comes from */ + Cusp *cusp; /** cusp the triangle lies in */ + int tet_index; /** tet->index */ + VertexIndex tet_vertex; /** vertex the triangle comes from */ + int num_curves; /** number of curves on the triangle */ + CuspVertex vertices[4]; /** information about each vertex */ + struct CuspTriangle *neighbours[4]; /** triangle neighbouring a face */ + struct CuspTriangle *next; /** next cusp triangle on doubly linked list */ + struct CuspTriangle *prev; /** prev cusp triangle on doubly linkled list */ +} CuspTriangle; + +typedef struct CuspRegion { + CuspTriangle *tri; /** cusp triangle the region lies on */ + int tet_index; /** tri->tetIndex */ + VertexIndex tet_vertex; /** tri->tet_vertex */ + int index; /** index of the cusp region */ + int curve[4][4]; /** looking at face, number of curves between the region and vertex */ + Boolean adj_cusp_triangle[4]; /** does the region meet this edge of the cusp triangle */ + Boolean dive[4][4]; /** can we dive along the face into this vertex */ + int num_adj_curves[4][4]; /** stores the number of curves between a region and a face */ + int temp_adj_curves[4][4]; /** store the adj curve until pathfinding is complete */ + struct CuspRegion *adj_cusp_regions[4]; /** index of the adjacent regions */ + struct CuspRegion *next; /** next cusp region in doubly linked list */ + struct CuspRegion *prev; /** prev cusp region in doubly linked list */ +} CuspRegion; + +typedef struct CuspStructure { + int intersect_tet_index; /** index of the intersection triangle */ + VertexIndex intersect_tet_vertex; /** vertex of the intersection triangle */ + int num_edge_classes; /** number of edge classes in the cusp */ + int num_cusp_triangles; /** number of cusp triangle in the cusp */ + int num_cusp_regions; /** number of cusp regions in the cusp */ + Triangulation *manifold; /** manifold */ + Cusp *cusp; /** which manifold cusp does the struct lie in */ + Graph *cusp_region_graph; /** dual graph of the cusp region */ + CuspRegion **regions; /** regions in the cusp region graph */ + CuspTriangle cusp_triangle_begin; /** header node of doubly linked list of cusp triangles */ + CuspTriangle cusp_triangle_end; /** tailer node of ... */ + CuspRegion *cusp_region_begin; /** array of header nodes for cusp regions, index by cusp tri */ + CuspRegion *cusp_region_end; /** array of tailer nodes for ...*/ +} CuspStructure; + +/** + * Queue Data Structure + */ + +Queue *init_queue(int); +Queue *enqueue(Queue *, int); +int dequeue(Queue *); +Queue *resize_queue(Queue *); +Boolean empty_queue(Queue *); +void free_queue(Queue *); + +/** + * Graph + */ + +Graph *init_graph(int, Boolean); +void free_graph(Graph *); +int insert_edge(Graph *, int, int, Boolean); +void delete_edge(Graph *, int, int, Boolean); +Boolean edge_exists(Graph *, int, int); + +/** + * Breadth First Search + */ + +void init_search(Graph *, Boolean *, Boolean *, int *); +void bfs(Graph *, int, Boolean *, Boolean *, int *); +void find_path(int, int, int *, EdgeNode *, EdgeNode *); +Boolean cycle_exists(Graph *, int, Boolean *, Boolean *, int *, int *, int *); +int **ford_fulkerson(Graph *, int, int); +int augment_path(Graph *, int **, Boolean *, int, int, int); +int bfs_target_list(Graph *, int, int *, int, Boolean *, Boolean *, int *); +Boolean contains(int *, int, int); +void free_edge_node(EdgeNode *, EdgeNode *); + +/** + * Symplectic Basis + */ + +int *gluing_equations_for_edge_class(Triangulation *, int); +int *combinatorial_holonomy(Triangulation *, int); +void oscillating_curves(Triangulation *, Boolean *); + +/** + * Initialisation Functions + */ + +CuspStructure *init_cusp_structure(Triangulation *, Cusp *); +void free_cusp_structure(CuspStructure **, int, int); +void init_cusp_triangulation(Triangulation *, CuspStructure *); +void init_cusp_region(CuspStructure *); +int init_intersect_cusp_region(CuspStructure *, CuspTriangle *, int); +int init_intersect_vertex_two_zero_flows(CuspStructure *, CuspTriangle *, int); +int init_normal_cusp_region(CuspStructure *, CuspTriangle *, int); +void set_cusp_region_data(CuspStructure *, CuspTriangle *, const int [4], const Boolean [4], int); +void init_train_line(CuspStructure *); +CurveComponent *init_curve_component(int, int, int); +OscillatingCurves *init_oscillating_curves(Triangulation *, const Boolean *); +void free_oscillating_curves(OscillatingCurves *); +void find_intersection_triangle(Triangulation *, CuspStructure *); + +/** + * Cusp Functions + */ + +int net_flow_around_vertex(CuspTriangle *, int); +void label_triangulation_edges(Triangulation *); +void label_cusp_vertex_indices(CuspTriangle *, CuspTriangle *, int); +void walk_around_cusp_vertex(CuspTriangle *, int, int); +CuspTriangle *find_cusp_triangle(CuspTriangle *, CuspTriangle *, CuspTriangle *, int); +void update_adj_region_data(CuspStructure *); +CuspRegion *find_adj_region(CuspRegion *, CuspRegion *, CuspRegion *, int); +void copy_region(CuspRegion *, CuspRegion *); +void construct_cusp_region_dual_graph(CuspStructure *); +void log_structs(Triangulation *, CuspStructure **, OscillatingCurves *, char *); + +/** + * Construct Oscillating Curves and calculate holonomy + */ + +void do_oscillating_curves(CuspStructure **, OscillatingCurves *, EndMultiGraph *); +void do_one_oscillating_curve(CuspStructure **, OscillatingCurves *, EndMultiGraph *, CuspEndPoint *, CuspEndPoint *, int, int); +CurveComponent *setup_first_curve_component(CuspStructure *, EndMultiGraph *, CuspEndPoint *, CurveComponent *, CurveComponent *); +CurveComponent *setup_last_curve_component(CuspStructure *, EndMultiGraph *, CuspEndPoint *, CurveComponent *, CurveComponent *); +void do_curve_component_to_new_edge_class(CuspStructure *, CurveComponent *); +void find_single_endpoint(CuspStructure *, PathEndPoint *, int, int); +void find_single_matching_endpoint(CuspStructure *, PathEndPoint *, PathEndPoint *); + +void graph_path_to_dual_curve(CuspStructure *, EdgeNode *, EdgeNode *, PathNode *, PathNode *, PathEndPoint *, PathEndPoint *); +void endpoint_edge_node_to_path_node(CuspRegion *, PathNode *, EdgeNode *, PathEndPoint *, int); +void interior_edge_node_to_path_node(CuspRegion *, PathNode *, EdgeNode *); + +void split_cusp_regions_along_path(CuspStructure *, PathNode *, PathNode *, PathEndPoint *, PathEndPoint *); +void split_path_len_one(CuspStructure *, PathNode *, PathEndPoint *, PathEndPoint *); +void split_cusp_region_path_interior(CuspRegion *, CuspRegion *, PathNode *, int); +void split_cusp_region_path_endpoint(CuspRegion *, CuspRegion *, PathNode *, PathEndPoint *, int, int); +void update_cusp_triangle_path_interior(CuspRegion *, CuspRegion *, CuspRegion *, PathNode *); +void update_cusp_triangle_endpoints(CuspRegion *, CuspRegion *, CuspRegion *, PathEndPoint *, PathNode *, int); + +void update_adj_curve_along_path(CuspStructure **, OscillatingCurves *, int, Boolean); +void update_adj_curve_at_endpoint(PathEndPoint *, CurveComponent *, int); +void update_adj_curve_on_cusp(CuspStructure *); +void update_path_holonomy(CurveComponent *, int); + +/** + * End Multi Graph + */ + +EndMultiGraph *init_end_multi_graph(Triangulation *); +void free_end_multi_graph(EndMultiGraph *); +Graph *spanning_tree(Graph *, int, int *); +int **find_end_multi_graph_edge_classes(EndMultiGraph *, Triangulation *); +int find_edge_class(Triangulation *, int, int); +void cusp_graph(Triangulation *, Graph *); +void color_graph(Graph *); +int find_same_color_edge(Triangulation *, EndMultiGraph *, Graph *); +int find_path_len(int, int, int *, int); +void find_multi_graph_path(Triangulation *, EndMultiGraph *, CuspEndPoint *, CuspEndPoint *, int); +void graph_path_to_cusp_path(EndMultiGraph *, EdgeNode *, EdgeNode *, CuspEndPoint *, CuspEndPoint *, int); +void find_edge_ends(Graph *, Triangulation *, int, int *, int *); + +int edgesThreeToFour[4][3] = {{1, 2, 3}, + {0, 2, 3}, + {0, 1, 3}, + {0, 1, 2}}; + +// ------------------------------------------------- + +// Queue + +Queue *init_queue(int size) { + Queue *q = NEW_STRUCT( Queue ); + + q->front = 0; + q->rear = -1; + q->len = 0; + q->size = MAX(size, 256); + q->array = NEW_ARRAY(q->size, int); + + return q; +} + +Queue *enqueue(Queue *q, int i) { + // Queue is full + if ( q->size == q->len ) { + q = resize_queue(q); + q = enqueue(q, i); + } else { + q->rear = (q->rear + 1) % q->size; + q->array[q->rear] = i; + q->len++; + } + + return q; +} + +int dequeue(Queue *q) { + // User to verify queue is not empty + int i = q->array[q->front]; + + q->front = (q->front + 1) % q->size; + q->len--; + + return i; +} + +Boolean empty_queue(Queue *q) { + if (q->len > 0) + return FALSE; + + return TRUE; +} + +Queue *resize_queue(Queue *q) { + int i; + Queue *p = init_queue(2 * q->size); + + // Copy elements to new array + while (!empty_queue(q)) { + i = dequeue(q); + enqueue(p, i); + } + + free_queue(q); + return p; +} + +void free_queue(Queue *q) { + my_free(q->array); + my_free(q); +} + +// Graph + +/* + * Initialise the arrays of the graph 'g' to their default values + */ + +Graph *init_graph(int max_vertices, Boolean directed) { + int i; + Graph *g = NEW_STRUCT(Graph); + + g->num_vertices = max_vertices; + g->directed = directed; + + g->edge_list_begin = NEW_ARRAY(max_vertices, EdgeNode); + g->edge_list_end = NEW_ARRAY(max_vertices, EdgeNode); + g->degree = NEW_ARRAY(max_vertices, int); + g->color = NEW_ARRAY(max_vertices, int); + + for (i = 0; i < max_vertices; i++) { + g->degree[i] = 0; + g->color[i] = -1; + + g->edge_list_begin[i].next = &g->edge_list_end[i]; + g->edge_list_begin[i].prev = NULL; + g->edge_list_end[i].next = NULL; + g->edge_list_end[i].prev = &g->edge_list_begin[i]; + } + + return g; +} + +void free_graph(Graph *g) { + if (g == NULL) + return; + + for (int i = 0; i < g->num_vertices; i++) { + free_edge_node(&g->edge_list_begin[i], &g->edge_list_end[i]); + } + + my_free(g->edge_list_begin); + my_free(g->edge_list_end); + my_free(g->degree); + my_free(g->color); + my_free(g); +} + +/* + * Insert an edge into the graph 'g' from vertex x to y. + */ + +int insert_edge(Graph *g, int x, int y, Boolean directed) { + // Ignore edge if it already exists + if (edge_exists(g, x, y)) + return x; + + EdgeNode *p = NEW_STRUCT( EdgeNode); + INSERT_AFTER(p, &g->edge_list_begin[x]); + p->y = y; + g->degree[x]++; + + if (!directed) { + insert_edge(g, y, x, TRUE); + } + + return x; +} + +/* + * Remove the edge from vertex x to vertex y + */ + +void delete_edge(Graph *g, int vertex_x, int vertex_y, Boolean directed) { + EdgeNode *node; + + for (node = g->edge_list_begin[vertex_x].next; + node != &g->edge_list_end[vertex_x] && node->y != vertex_y; + node = node->next); + + if (node == &g->edge_list_end[vertex_x]) + return; + + REMOVE_NODE(node) + my_free(node); + + if (!directed) { + delete_edge(g, vertex_y, vertex_x, TRUE); + } +} + +/* + * Check if an edge already exists in the graph + */ + +Boolean edge_exists(Graph *g, int v1, int v2) { + EdgeNode *node = &g->edge_list_begin[v1]; + + while ((node = node->next)->next != NULL) { + if (node->y == v2) { + return TRUE; + } + } + + return FALSE; +} + +// --------------------------------------------------------------- + +// Breadth First Search + +/* + * Initialise default values for bfs arrays + */ + +void init_search(Graph *g, Boolean *processed, Boolean *discovered, int *parent) { + int i; + + for (i = 0; i < g->num_vertices; i ++) { + processed[i] = FALSE; + discovered[i] = FALSE; + parent[i] = -1; + } +} + +/* + * Graph search algorithm starting at vertex 'start'. + */ + +void bfs(Graph *g, int start, Boolean *processed, Boolean *discovered, int *parent) { + Queue *q = init_queue(10); + int v, y; + EdgeNode *p; + + enqueue(q, start); + discovered[start] = TRUE; + + while (!empty_queue(q)) { + v = dequeue(q); + processed[v] = TRUE; + p = &g->edge_list_begin[v]; + + while ((p = p->next)->next != NULL) { + y = p->y; + + if (!discovered[y]) { + q = enqueue(q, y); + discovered[y] = TRUE; + parent[y] = v; + } + } + } + + free_queue(q); +} + +/* + * Recover the path through the graph from the parents array and store + * in the doubly linked list node_begin -> ... -> node_end. + */ + +void find_path(int start, int end, int *parents, EdgeNode *node_begin, EdgeNode *node_end) { + int u; + + if (start != end && parents[end] == -1) { + uFatalError("find_path", "symplectic_basis"); + } + + u = end; + while (u != start) { + EdgeNode *new_node = NEW_STRUCT(EdgeNode); + new_node->y = u; + INSERT_AFTER(new_node, node_begin); + + u = parents[u]; + }; + + EdgeNode *new_node = NEW_STRUCT(EdgeNode); + new_node->y = start; + INSERT_AFTER(new_node, node_begin); +} + +void free_edge_node(EdgeNode *node_begin, EdgeNode *node_end) { + EdgeNode *node; + + while (node_begin->next != node_end) { + node = node_begin->next; + REMOVE_NODE(node); + my_free(node); + } +} + +// --------------------------------------------------- + +// Symplectic Basis + +/* + * Allocates arrays for symplectic basis and gluing equations. + * get_gluing_equations find oscillating curves on the manifold. + * Constructs return array using gluing_equations_for_edge_class + * and combinatorial_holonomy + */ + +int** get_symplectic_basis(Triangulation *manifold, int *num_rows, int *num_cols, int log) { + int i, j, k; + debug = log; + Boolean *edge_classes = NEW_ARRAY(manifold->num_tetrahedra, Boolean); + Tetrahedron *tet; + + peripheral_curves(manifold); + + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + if (tet->extra != NULL) + uFatalError("oscillating_curves", "symplectic_basis"); + + tet->extra = NEW_ARRAY(manifold->num_tetrahedra, Extra); + + for (i = 0; i < manifold->num_tetrahedra; i++) + for (j = 0; j < 4; j++) + for (k = 0; k < 4; k++) + tet->extra[i].curve[j][k] = 0; + } + + // Dual Edge Curves Gamma_i -> symplectic equations + oscillating_curves(manifold, edge_classes); + + // Construct return array + *num_rows = 2 * (manifold->num_tetrahedra - manifold->num_cusps); + int **eqns = NEW_ARRAY(*num_rows, int *); + + j = 0; + for (i = 0; i < manifold->num_tetrahedra; i++) { + if (!edge_classes[i]) { + continue; + } + + eqns[2 * j] = gluing_equations_for_edge_class(manifold, i); + eqns[2 * j + 1] = combinatorial_holonomy(manifold, i); + j++; + } + + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + my_free(tet->extra); + tet->extra = NULL; + } + my_free(edge_classes); + + *num_cols = 3 * manifold->num_tetrahedra; + return eqns; +} + +/* + * Copy of get_gluings_equations.c get_gluing_equations() which finds + * the edge gluings equations for a given edge index. Used instead + * of get_gluing_equations to ensure we have the correct edge index + * and simplify memory management since we don't need all the rows of + * the gluing equations matrix. + */ + +int *gluing_equations_for_edge_class(Triangulation *manifold, int edgeClass) { + int *eqns, i, T; + EdgeClass *edge; + PositionedTet ptet0, ptet; + + T = manifold->num_tetrahedra; + eqns = NEW_ARRAY(3 * T, int); + + for (i = 0; i < 3 * T; i++) + eqns[i] = 0; + + /* + * Build edge equations. + */ + + for (edge = manifold->edge_list_begin.next; edge != &manifold->edge_list_end; edge = edge->next) { + if (edge->index == edgeClass) + break; + } + + set_left_edge(edge, &ptet0); + ptet = ptet0; + do { + eqns[3 * ptet.tet->index + edge3_between_faces[ptet.near_face][ptet.left_face]]++; + veer_left(&ptet); + } while (same_positioned_tet(&ptet, &ptet0) == FALSE); + + return eqns; +} + +/* + * Construct the symplectic equations from the oscillating curves + */ + +int *combinatorial_holonomy(Triangulation *manifold, int edge_class) { + int v, f, ff; + int *eqns = NEW_ARRAY(3 * manifold->num_tetrahedra, int); + Tetrahedron *tet; + + for (int i = 0; i < 3 * manifold->num_tetrahedra; i++) { + eqns[i] = 0; + } + + // which tet + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + // which tet vertex + for (v = 0; v < 4; v++) { + // which face + for (f = 0; f < 4; f++) { + if (f == v) + continue; + + ff = (int) remaining_face[v][f]; + + eqns[3 * tet->index + edge3_between_faces[f][ff]] + += FLOW(tet->extra[edge_class].curve[v][f], tet->extra[edge_class].curve[v][ff]); + } + } + } + + return eqns; +} + +/* + * Initialise cusp structure on each cusp, construct train lines, construct + * oscillating curves and store the intersection numbers of each curve with the + * cusp triangles it enters in tet->extra[edge_class]->curve, in the same fashion + * as the peripheral curves. + */ + +void oscillating_curves(Triangulation *manifold, Boolean *edge_classes) { + int i; + label_triangulation_edges(manifold); + + CuspStructure **cusps = NEW_ARRAY(manifold->num_cusps, CuspStructure *); + EndMultiGraph *multi_graph = init_end_multi_graph(manifold); + Cusp *cusp; + + for (i = 0; i < multi_graph->num_edge_classes; i++) + edge_classes[i] = multi_graph->edge_classes[i] == TRUE ? FALSE : TRUE; + + edge_classes[multi_graph->e0] = FALSE; + + OscillatingCurves *curves = init_oscillating_curves(manifold, edge_classes); + + for (i = 0; i < manifold->num_cusps; i++) { + for (cusp = manifold->cusp_list_begin.next; cusp != &manifold->cusp_list_end && cusp->index != i; cusp = cusp->next); + + if (cusp == &manifold->cusp_list_end) + uFatalError("oscillating_curves", "symplectic_basis"); + + cusps[i] = init_cusp_structure(manifold, cusp); + } + + if (debug) { + printf("\n"); + printf("Struct Initialisation\n"); + printf("\n"); + + log_structs(manifold, cusps, NULL, "gluing"); + log_structs(manifold, cusps, NULL, "homology"); + log_structs(manifold, cusps, NULL, "edge_indices"); + log_structs(manifold, cusps, NULL, "inside_edge"); + log_structs(manifold, cusps, NULL, "cusp_regions"); + } + + do_oscillating_curves(cusps, curves, multi_graph); + + if (debug) { + for (i = 0; i < manifold->num_cusps; i++) { + printf("%d, ", cusps[i]->num_cusp_regions); + } + printf("\n"); + } + + free_end_multi_graph(multi_graph); + free_oscillating_curves(curves); + free_cusp_structure(cusps, manifold->num_cusps, manifold->num_tetrahedra); +} + +void free_symplectic_basis(int **eqns, int num_rows) { + int i; + + for (i = 0; i < num_rows; i++) + my_free(eqns[i]); + my_free(eqns); +} + +// ------------------------------------ + +/* + * Initialisation Functions + */ + +CuspStructure *init_cusp_structure(Triangulation *manifold, Cusp *cusp) { + CuspStructure *boundary = NEW_STRUCT(CuspStructure); + + // Invalid cusp topology + if (cusp->topology == Klein_cusp) + uFatalError("init_cusp_structure", "symplectic_basis"); + + boundary->manifold = manifold; + boundary->cusp = cusp; + boundary->num_edge_classes = manifold->num_tetrahedra; + boundary->num_cusp_triangles = 0; + boundary->num_cusp_regions = 0; + boundary->regions = NULL; + + find_intersection_triangle(manifold, boundary); + init_cusp_triangulation(manifold, boundary); + init_cusp_region(boundary); + + boundary->cusp_region_graph = NULL; + construct_cusp_region_dual_graph(boundary); + + return boundary; +} + +void free_cusp_structure(CuspStructure **cusps, int num_cusps, int num_edge_classes) { + int cusp_index; + CuspTriangle *tri; + CuspRegion *region; + CuspStructure *cusp; + + for (cusp_index = 0; cusp_index < num_cusps; cusp_index++) { + cusp = cusps[cusp_index]; + // free graph + free_graph(cusp->cusp_region_graph); + my_free(cusp->regions); + + // free cusp regions + for (int i = 0; i < 4 * cusp->manifold->num_tetrahedra; i++) { + while (cusp->cusp_region_begin[i].next != &cusp->cusp_region_end[i]) { + region = cusp->cusp_region_begin[i].next; + REMOVE_NODE(region); + my_free(region); + } + } + + my_free(cusp->cusp_region_begin); + my_free(cusp->cusp_region_end); + + // free cusp triangle + while (cusp->cusp_triangle_begin.next != &cusp->cusp_triangle_end) { + tri = cusp->cusp_triangle_begin.next; + REMOVE_NODE(tri); + my_free(tri); + } + + my_free(cusp); + } + + my_free(cusps); +} + +/* + * Construct the cusp triangle doubly linked list which consists of the + * triangles in the cusp triangulation + */ + +void init_cusp_triangulation(Triangulation *manifold, CuspStructure *cusp) { + int index = 0; + VertexIndex vertex; + FaceIndex face; + Tetrahedron *tet; + CuspTriangle *tri; + + // Allocate Cusp Triangulation Header and Tail Null nodes + cusp->cusp_triangle_begin.next = &cusp->cusp_triangle_end; + cusp->cusp_triangle_begin.prev = NULL; + cusp->cusp_triangle_end.next = NULL; + cusp->cusp_triangle_end.prev = &cusp->cusp_triangle_begin; + + // which tetrahedron are we on + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + // while vertex are we on + for (vertex = 0; vertex < 4; vertex++) { + // is this vertex on the right cusp + if (tet->cusp[vertex] != cusp->cusp) { + continue; + } + + tri = NEW_STRUCT( CuspTriangle ); + INSERT_BEFORE(tri, &cusp->cusp_triangle_end); + index++; + + tri->tet = tet; + tri->cusp = tet->cusp[vertex]; + tri->tet_index = tri->tet->index; + tri->tet_vertex = vertex; + + tri->num_curves = net_flow_around_vertex(tri, edgesThreeToFour[tri->tet_vertex][0]) + + net_flow_around_vertex(tri, edgesThreeToFour[tri->tet_vertex][1]) + + net_flow_around_vertex(tri, edgesThreeToFour[tri->tet_vertex][2]); + + for (face = 0; face < 4; face ++) { + if (tri->tet_vertex == face) + continue; + + tri->vertices[face].v1 = tri->tet_vertex; + tri->vertices[face].v2 = face; + tri->vertices[face].edge = tri->tet->edge_class[ + edge_between_vertices[tri->vertices[face].v1][tri->vertices[face].v2]]; + tri->vertices[face].edge_class = tri->vertices[face].edge->index; + tri->vertices[face].edge_index = -1; + } + } + } + + // which cusp triangle + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + // which vertex + for (face = 0; face < 4; face++) { + if (face == tri->tet_vertex) + continue; + + tri->neighbours[face] = find_cusp_triangle(&cusp->cusp_triangle_begin, &cusp->cusp_triangle_end, tri, face); + } + } + + label_cusp_vertex_indices(&cusp->cusp_triangle_begin, &cusp->cusp_triangle_end, cusp->num_edge_classes); + cusp->num_cusp_triangles = index; +} + +/* + * Initialise the cusp region doubly linked list to cotain the regions bounded + * by the meridian and longitude curves. + */ + +void init_cusp_region(CuspStructure *cusp) { + int index; + CuspTriangle *tri; + + // Header and tailer nodes. + cusp->cusp_region_begin = NEW_ARRAY(4 * cusp->manifold->num_tetrahedra, CuspRegion); + cusp->cusp_region_end = NEW_ARRAY(4 * cusp->manifold->num_tetrahedra, CuspRegion); + + for (index = 0; index < 4 * cusp->manifold->num_tetrahedra; index++) { + cusp->cusp_region_begin[index].next = &cusp->cusp_region_end[index]; + cusp->cusp_region_begin[index].prev = NULL; + cusp->cusp_region_end[index].next = NULL; + cusp->cusp_region_end[index].prev = &cusp->cusp_region_begin[index]; + } + + index = 0; + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + // Intersection vertex doesn't have a center + if (tri->tet_index == cusp->intersect_tet_index && tri->tet_vertex == cusp->intersect_tet_vertex) { + index = init_intersect_cusp_region(cusp, tri, index); + continue; + } + + index = init_normal_cusp_region(cusp, tri, index); + } + + update_adj_region_data(cusp); + cusp->num_cusp_regions = index; +} + +/* + * Assume peripheral_curves() has been called, and as a result the only curves + * on the intersection triangle are those which intersect, and they give a + * valid intersection. + */ + +int init_intersect_cusp_region(CuspStructure *cusp, CuspTriangle *tri, int index) { + int i, curve_index, vertex, v1, v2, v3; + int distance[4]; + Boolean adj_triangle[4]; + + // which vertex are we inside the flow of + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex) { + continue; + } + + v1 = (int) remaining_face[tri->tet_vertex][vertex]; + v2 = (int) remaining_face[vertex][tri->tet_vertex]; + + for (i = 1; i < net_flow_around_vertex(tri, vertex); i++) { + for (curve_index = 0; curve_index < 2; curve_index++) { + distance[v1] = i; + distance[v2] = MIN(distance[v1], 2 * net_flow_around_vertex(tri, vertex) - distance[v1]) + + net_flow_around_vertex(tri, v2) + net_flow_around_vertex(tri, v1); + distance[vertex] = net_flow_around_vertex(tri, vertex) + - distance[v1] + net_flow_around_vertex(tri, v1); + distance[tri->tet_vertex] = -1; + + adj_triangle[v1] = 1; + adj_triangle[v2] = 0; + adj_triangle[vertex] = 0; + adj_triangle[tri->tet_vertex] = -1; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + + // Swap vertices + v1 = (int) remaining_face[vertex][tri->tet_vertex]; + v2 = (int) remaining_face[tri->tet_vertex][vertex]; + } + } + + // Region in the middle of face vertex + if (net_flow_around_vertex(tri, v1) && net_flow_around_vertex(tri, v2)) { + distance[v1] = net_flow_around_vertex(tri, v2); + distance[v2] = net_flow_around_vertex(tri, v1); + distance[vertex] = MIN(net_flow_around_vertex(tri, v1) + distance[v1], + net_flow_around_vertex(tri, v2) + distance[v2]) + + net_flow_around_vertex(tri, vertex); + distance[tri->tet_vertex] = -1; + + adj_triangle[v1] = 0; + adj_triangle[v2] = 0; + adj_triangle[vertex] = 1; + adj_triangle[tri->tet_vertex] = -1; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + } + } + + // Region of distance 0 to vertex + v1 = edgesThreeToFour[tri->tet_vertex][0]; + v2 = edgesThreeToFour[tri->tet_vertex][1]; + v3 = edgesThreeToFour[tri->tet_vertex][2]; + + // Edge Case: Two vertices with 0 flow + if (ATLEAST_TWO(!net_flow_around_vertex(tri, v1), + !net_flow_around_vertex(tri, v2), + !net_flow_around_vertex(tri, v3))) + return init_intersect_vertex_two_zero_flows(cusp, tri, index); + + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex) + continue; + + v1 = (int) remaining_face[tri->tet_vertex][vertex]; + v2 = (int) remaining_face[vertex][tri->tet_vertex]; + + distance[vertex] = 0; + distance[v1] = net_flow_around_vertex(tri, vertex) + net_flow_around_vertex(tri, v1); + distance[v2] = net_flow_around_vertex(tri, vertex) + net_flow_around_vertex(tri, v2); + distance[tri->tet_vertex] = -1; + + adj_triangle[vertex] = 0; + adj_triangle[v1] = 1; + adj_triangle[v2] = 1; + adj_triangle[tri->tet_vertex] = 0; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + } + + return index; +} + +int init_intersect_vertex_two_zero_flows(CuspStructure *cusp, CuspTriangle *tri, int index) { + int vertex, v1, v2, v3, distance[4]; + Boolean adj_triangle[4]; + + v1 = (int) edgesThreeToFour[tri->tet_vertex][0]; + v2 = (int) edgesThreeToFour[tri->tet_vertex][1]; + v3 = (int) edgesThreeToFour[tri->tet_vertex][2]; + + distance[v1] = net_flow_around_vertex(tri, v1); + distance[v2] = net_flow_around_vertex(tri, v2); + distance[v3] = net_flow_around_vertex(tri, v3); + distance[tri->tet_vertex] = -1; + + adj_triangle[v1] = 1; + adj_triangle[v2] = 1; + adj_triangle[v3] = 1; + adj_triangle[tri->tet_vertex] = -1; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + + // Find vertex with non-zero flow + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex) + continue; + + if (net_flow_around_vertex(tri, vertex)) { + v1 = vertex; + v2 = (int) remaining_face[tri->tet_vertex][v1]; + v3 = (int) remaining_face[v1][tri->tet_vertex]; + break; + } + } + distance[v1] = 0; + distance[v2] = net_flow_around_vertex(tri, v1); + distance[v3] = net_flow_around_vertex(tri, v1); + distance[tri->tet_vertex] = -1; + + adj_triangle[v1] = 0; + adj_triangle[v2] = 1; + adj_triangle[v3] = 1; + adj_triangle[tri->tet_vertex] = 0; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + + return index + 1; +} + +int init_normal_cusp_region(CuspStructure *cusp, CuspTriangle *tri, int index) { + int i, vertex, v1, v2; + int distance[4]; + Boolean adj_triangle[4]; + + // which vertex are we inside the flow of + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex) { + continue; + } + + v1 = (int) remaining_face[tri->tet_vertex][vertex]; + v2 = (int) remaining_face[vertex][tri->tet_vertex]; + + for (i = 0; i < net_flow_around_vertex(tri, vertex); i++) { + distance[vertex] = i; + distance[v1] = net_flow_around_vertex(tri, v1) + + (net_flow_around_vertex(tri, vertex) - distance[vertex]); + distance[v2] = net_flow_around_vertex(tri, v2) + + (net_flow_around_vertex(tri, vertex) - distance[vertex]); + distance[tri->tet_vertex] = -1; + + adj_triangle[vertex] = 0; + adj_triangle[v1] = 1; + adj_triangle[v2] = 1; + adj_triangle[tri->tet_vertex] = 0; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + } + + } + + // center region + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex) + continue; + + distance[vertex] = net_flow_around_vertex(tri, vertex); + adj_triangle[vertex] = 1; + } + + distance[tri->tet_vertex] = -1; + adj_triangle[tri->tet_vertex] = 0; + + set_cusp_region_data(cusp, tri, distance, adj_triangle, index); + index++; + return index; +} + +/* + * Helper function to init_cusp_regions which allocates the attributes of the + * cusp region + */ + +void set_cusp_region_data(CuspStructure *cusp, CuspTriangle *tri, const int distance[4], + const Boolean adj_cusp_triangle[4], int index) { + int i, j, v1, v2, v3; + CuspRegion *region = NEW_STRUCT( CuspRegion ); + INSERT_BEFORE(region, &cusp->cusp_region_end[TRI_TO_INDEX(tri->tet_index, tri->tet_vertex)]); + + region->tri = tri; + region->tet_index = region->tri->tet_index; + region->tet_vertex = region->tri->tet_vertex; + region->index = index; + + // default values + for (i = 0; i < 4; i++) { + region->adj_cusp_triangle[i] = FALSE; + region->adj_cusp_regions[i] = NULL; + + for (j = 0; j < 4; j++) { + region->curve[i][j] = -1; + region->dive[i][j] = 0; + region->num_adj_curves[i][j] = 0; + region->temp_adj_curves[i][j] = 0; + } + } + + for (i = 0; i < 3; i++) { + v1 = edgesThreeToFour[tri->tet_vertex][i]; + v2 = edgesThreeToFour[tri->tet_vertex][(i + 1) % 3]; + v3 = edgesThreeToFour[tri->tet_vertex][(i + 2) % 3]; + + region->curve[v2][v1] = distance[v1]; + region->curve[v3][v1] = distance[v1]; + region->dive[v2][v1] = distance[v1] ? FALSE : TRUE; + region->dive[v3][v1] = distance[v1] ? FALSE : TRUE; + + region->adj_cusp_triangle[v1] = adj_cusp_triangle[v1]; + } +} + +CurveComponent *init_curve_component(int edge_class_start, int edge_class_finish, int cusp_index) { + int i; + + CurveComponent *path = NEW_STRUCT(CurveComponent ); + + path->path_begin.next = &path->path_end; + path->path_begin.prev = NULL; + path->path_end.next = NULL; + path->path_end.prev = &path->path_begin; + + path->edge_class[START] = edge_class_start; + path->edge_class[FINISH] = edge_class_finish; + path->cusp_index = cusp_index; + + for (i = 0; i < 2; i++) { + path->endpoints[i].tri = NULL; + path->endpoints[i].region = NULL; + path->endpoints[i].num_adj_curves = 0; + } + + return path; +} + +/* + * Initialise dual curve doubly linked list which stores the oscillating curves + * on the cusp + */ + +OscillatingCurves *init_oscillating_curves(Triangulation *manifold, const Boolean *edge_classes) { + int i, j; + OscillatingCurves *curves = NEW_STRUCT(OscillatingCurves ); + + curves->num_curves = 0; + for (i = 0; i < manifold->num_tetrahedra; i++) + if (edge_classes[i]) + curves->num_curves++; + + curves->curve_begin = NEW_ARRAY(curves->num_curves, CurveComponent ); + curves->curve_end = NEW_ARRAY(curves->num_curves, CurveComponent ); + curves->edge_class = NEW_ARRAY(curves->num_curves, int); + + j = 0; + for (i = 0; i < manifold->num_tetrahedra; i++) { + if (!edge_classes[i]) + continue; + + curves->edge_class[j] = i; + j++; + } + + // which curve + for (i = 0; i < curves->num_curves; i++) { + curves->curve_begin[i].next = &curves->curve_end[i]; + curves->curve_begin[i].prev = NULL; + curves->curve_end[i].next = NULL; + curves->curve_end[i].prev = &curves->curve_begin[i]; + } + + return curves; +} + +void free_oscillating_curves(OscillatingCurves *curves) { + int i; + CurveComponent *path; + PathNode *path_node; + + for (i = 0; i < curves->num_curves; i++) { + while (curves->curve_begin[i].next != &curves->curve_end[i]) { + path = curves->curve_begin[i].next; + REMOVE_NODE(path); + + while (path->path_begin.next != &path->path_end) { + path_node = path->path_begin.next; + REMOVE_NODE(path_node); + my_free(path_node); + } + + my_free(path); + } + } + + my_free(curves->curve_begin); + my_free(curves->curve_end); + my_free(curves->edge_class); + my_free(curves); +} + +// ---------------------------------------------------- + +/* + * Cusp Functions + */ + +/* + * peripheral_curves.c places a meridian and longitude curve on each cusp. It + * starts at a base triangle, the intersection point, and searches outwards. + * Note it does not visit a cusp triangle more than once. So we find a cusp + * triangle which contains both a meridian and longitude (this should be the + * same intersection triangle that peripheral_curves sets since it is the same + * search process) and assert this is the intersection triangle. Currently + * init_cusp_regions assumes the intersection triangle only contains curves + * which intersect. This is because we need some information about the curves + * to construct the cusp regions. + */ + +void find_intersection_triangle(Triangulation *manifold, CuspStructure *boundary) { + FaceIndex face; + Cusp *cusp = boundary->cusp; + int n; + + for (cusp->basepoint_tet = manifold->tet_list_begin.next; + cusp->basepoint_tet != &manifold->tet_list_end; + cusp->basepoint_tet = cusp->basepoint_tet->next) + + for (cusp->basepoint_vertex = 0; + cusp->basepoint_vertex < 4; + cusp->basepoint_vertex++) + { + if (cusp->basepoint_tet->cusp[cusp->basepoint_vertex] != cusp) + continue; + + for (face = 0; face < 4; face++) + { + if (face == cusp->basepoint_vertex) + continue; + + for (n = 0; n < 2; n++) { + cusp->basepoint_orientation = ORIENTATION(n); + + if (cusp->basepoint_tet->curve + [M] + [cusp->basepoint_orientation] + [cusp->basepoint_vertex] + [face] != 0 + && cusp->basepoint_tet->curve + [L] + [cusp->basepoint_orientation] + [cusp->basepoint_vertex] + [face] != 0) { + /* + * We found the basepoint! + */ + + boundary->intersect_tet_index = cusp->basepoint_tet->index; + boundary->intersect_tet_vertex = cusp->basepoint_vertex; + return; + } + + + } + } + } +} + +/* + * Calculate the number of curves passing around a vertex in the cusp + * triangulation. + */ + +int net_flow_around_vertex(CuspTriangle *tri, int vertex) { + int mflow, lflow, retval; + + // Contribution from meridian curves + mflow = FLOW(tri->tet->curve[M][right_handed][tri->tet_vertex][remaining_face[tri->tet_vertex][vertex]], + tri->tet->curve[M][right_handed][tri->tet_vertex][remaining_face[vertex][tri->tet_vertex]]); + + // Contribution from longitudinal curves + lflow = FLOW(tri->tet->curve[L][right_handed][tri->tet_vertex][remaining_face[tri->tet_vertex][vertex]], + tri->tet->curve[L][right_handed][tri->tet_vertex][remaining_face[vertex][tri->tet_vertex]]); + + retval = ABS(mflow) + ABS(lflow); + return retval; +} + +/* + * Returns a pointer to the cusp triangle which is the neighbour of tri across + * face 'face'. + */ + +CuspTriangle *find_cusp_triangle(CuspTriangle *cusp_triangle_begin, CuspTriangle *cusp_triangle_end, + CuspTriangle *tri, int face) { + int tet_index, tet_vertex; + CuspTriangle *pTri; + + tet_index = tri->tet->neighbor[face]->index; + tet_vertex = EVALUATE(tri->tet->gluing[face], tri->tet_vertex); + + for (pTri = cusp_triangle_begin->next; pTri != cusp_triangle_end; pTri = pTri->next) { + if (pTri->tet_index == tet_index && pTri->tet_vertex == tet_vertex) + return pTri; + } + + // Didn't find a neighbour + return NULL; +} + +/* + * Give each edge of the triangulation an index to identify the cusp vertices + */ + +void label_triangulation_edges(Triangulation *manifold) { + int i = 0; + EdgeClass *edge = &manifold->edge_list_begin; + + while ((edge = edge->next)->next != NULL) + edge->index = i++; + + // incorrect number of edge classes + if (i != manifold->num_tetrahedra) + uFatalError("label_triangulation_edges", "symplectic_basis"); +} + +/* + * Each edge class of the manifold appears as two vertices in the cusp + * triangulation. We iterate over the cusp triangulation, walking around each + * vertex to give it the same index. + */ + +void label_cusp_vertex_indices(CuspTriangle *cusp_triangle_begin, CuspTriangle *cusp_triangle_end, int numEdgeClasses) { + int i, vertex; + CuspTriangle *tri; + + int *current_index = NEW_ARRAY(numEdgeClasses, int); + + for (i = 0; i < numEdgeClasses; i++) + current_index[i] = 0; + + for (tri = cusp_triangle_begin->next; tri != cusp_triangle_end; tri = tri->next) { + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == tri->tet_vertex || tri->vertices[vertex].edge_index != -1) + continue; + + walk_around_cusp_vertex(tri, vertex, current_index[tri->vertices[vertex].edge_class]); + current_index[tri->vertices[vertex].edge_class]++; + } + } + + my_free(current_index); +} + +/* + * Walk around vertex cusp_vertex of triangle *tri and set edge_index to index. + */ + +void walk_around_cusp_vertex(CuspTriangle *tri, int cusp_vertex, int index) { + int gluing_vertex, outside_vertex, old_gluing_vertex, old_cusp_vertex, old_outside_vertex; + gluing_vertex = (int) remaining_face[cusp_vertex][tri->tet_vertex]; + outside_vertex = (int) remaining_face[tri->tet_vertex][cusp_vertex]; + + while (tri->vertices[cusp_vertex].edge_index == -1) { + tri->vertices[cusp_vertex].edge_index = index; + + // Move to the next cusp triangle + old_cusp_vertex = cusp_vertex; + old_gluing_vertex = gluing_vertex; + old_outside_vertex = outside_vertex; + + cusp_vertex = EVALUATE(tri->tet->gluing[old_gluing_vertex], old_cusp_vertex); + gluing_vertex = EVALUATE(tri->tet->gluing[old_gluing_vertex], old_outside_vertex); + outside_vertex = EVALUATE(tri->tet->gluing[old_gluing_vertex], old_gluing_vertex); + tri = tri->neighbours[old_gluing_vertex]; + } +} + +/* + * Calculate which regions are located across cusp edges and store the result + * in the adj_cusp_regions attribute + */ + +void update_adj_region_data(CuspStructure *cusp) { + CuspTriangle *adj_triangle; + CuspRegion *region; + FaceIndex f; + int i, adj_index; + + // Add adjacent region info + for (i = 0; i < 4 * cusp->manifold->num_tetrahedra; i++) { + for (region = cusp->cusp_region_begin[i].next; region != &cusp->cusp_region_end[i]; region = region->next) { + for (f = 0; f < 4; f++) { + if (!region->adj_cusp_triangle[f] || region->tet_vertex == f) { + region->adj_cusp_regions[f] = NULL; + continue; + } + + adj_triangle = region->tri->neighbours[f]; + adj_index = TRI_TO_INDEX(adj_triangle->tet_index, adj_triangle->tet_vertex); + region->adj_cusp_regions[f] = find_adj_region(&cusp->cusp_region_begin[adj_index], + &cusp->cusp_region_end[adj_index], + region, f); + } + } + } +} + +/* + * Find the cusp region which is adjacent to x across face. + */ + +CuspRegion *find_adj_region(CuspRegion *cusp_region_begin, CuspRegion *cusp_region_end, + CuspRegion *x, int face) { + int v1, v2, y_vertex1, y_vertex2, y_face, distance_v1, distance_v2, tet_index, tet_vertex; + Boolean adj_face; + CuspTriangle *tri = x->tri; + CuspRegion *region; + + v1 = (int) remaining_face[tri->tet_vertex][face]; + v2 = (int) remaining_face[face][tri->tet_vertex]; + + y_vertex1 = EVALUATE(tri->tet->gluing[face], v1); + y_vertex2 = EVALUATE(tri->tet->gluing[face], v2); + y_face = EVALUATE(tri->tet->gluing[face], face); + + // Check current adj region first + if (x->adj_cusp_regions[face] != NULL) { + distance_v1 = (x->curve[face][v1] == x->adj_cusp_regions[face]->curve[y_face][y_vertex1]); + distance_v2 = (x->curve[face][v2] == x->adj_cusp_regions[face]->curve[y_face][y_vertex2]); + adj_face = x->adj_cusp_regions[face]->adj_cusp_triangle[y_face]; + + if (distance_v1 && distance_v2 && adj_face) + return x->adj_cusp_regions[face]; + } + + /* + * We search through the regions in reverse as the new regions + * are added to the end of the doubly linked list + */ + for (region = cusp_region_end->prev; region != cusp_region_begin; region = region->prev) { + tet_index = (tri->neighbours[face]->tet_index == region->tet_index); + tet_vertex = (tri->neighbours[face]->tet_vertex == region->tet_vertex); + + if (!tet_index || !tet_vertex) + continue; + + distance_v1 = (x->curve[face][v1] == region->curve[y_face][y_vertex1]); + distance_v2 = (x->curve[face][v2] == region->curve[y_face][y_vertex2]); + adj_face = region->adj_cusp_triangle[y_face]; + + // missing distance + if (region->curve[y_face][y_vertex1] == -1 || region->curve[y_face][y_vertex2] == -1) + uFatalError("find_adj_region", "symplectic_basis"); + + if (distance_v1 && distance_v2 && adj_face) + return region; + } + + // We didn't find a cusp region + //uFatalError("find_cusp_region", "symplectic_basis"); + return NULL; +} + +/* + * region1 splits into region1 and region2, set them up to be split + */ + +void copy_region(CuspRegion *region1, CuspRegion *region2) { + int i, j; + + if (region1 == NULL || region2 == NULL || region1->tri == NULL) + uFatalError("copy_region", "symplectic_basis"); + + region2->tri = region1->tri; + region2->tet_index = region1->tet_index; + region2->tet_vertex = region1->tet_vertex; + + for (i = 0; i < 4; i++) { + region2->adj_cusp_triangle[i] = region1->adj_cusp_triangle[i]; + region2->adj_cusp_regions[i] = NULL; + + for (j = 0; j < 4; j++) { + region2->curve[i][j] = region1->curve[i][j]; + region2->dive[i][j] = FALSE; + region2->num_adj_curves[i][j] = region1->num_adj_curves[i][j]; + region2->temp_adj_curves[i][j] = region1->temp_adj_curves[i][j]; + } + } +} + +/* + * Construct the graph with edges coming from adjacent regions, using + * region->index to label each vertex. + */ + +void construct_cusp_region_dual_graph(CuspStructure *cusp) { + int i, face; + CuspRegion *region; + + Graph *graph1 = init_graph(cusp->num_cusp_regions, FALSE); + + int *visited = NEW_ARRAY(graph1->num_vertices, int); + + my_free(cusp->regions); + cusp->regions = NEW_ARRAY(graph1->num_vertices, CuspRegion *); + + for (i = 0; i < graph1->num_vertices; i++) { + visited[i] = FALSE; + cusp->regions[i] = NULL; + } + + // Walk around the cusp triangulation inserting edges + for (i = 0; i < 4 * cusp->manifold->num_tetrahedra; i++) { + for (region = cusp->cusp_region_begin[i].next; region != &cusp->cusp_region_end[i]; region = region->next) { + if (visited[region->index]) + continue; + + for (face = 0; face < 4; face++) { + if (!region->adj_cusp_triangle[face]) + continue; + + // Missing adj region data + if (region->adj_cusp_regions[face] == NULL) + uFatalError("construct_cusp_region_dual_graph", "symplectic_basis"); + + insert_edge(graph1, region->index, region->adj_cusp_regions[face]->index, graph1->directed); + cusp->regions[region->index] = region; + } + + visited[region->index] = 1; + } + } + + free_graph(cusp->cusp_region_graph); + my_free(visited); + + cusp->cusp_region_graph = graph1; +} + +/* + * Types: gluing, train_lines, cusp_regions, homology, edge_indices, + * dual_curves, inside_edge, graph, endpoints + */ + +void log_structs(Triangulation *manifold, CuspStructure **cusps, OscillatingCurves *curves, char *type) { + int i, j, k, x_vertex1, x_vertex2, y_vertex1, y_vertex2, v1, v2, v3; + + CuspTriangle *tri; + CuspRegion *region; + EdgeNode *edge_node; + PathNode *path_node; + CurveComponent *path; + Graph *g; + CuspStructure *cusp; + + if (strcmp(type, "gluing") == 0) { + printf("Triangle gluing info\n"); + for (i = 0; i < manifold->num_cusps; i++) { + printf("Boundary %d\n", i); + cusp = cusps[i]; + + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + for (j = 0; j < 4; j++) { + if (j == tri->tet_vertex) + continue; + + x_vertex1 = (int) remaining_face[tri->tet_vertex][j]; + x_vertex2 = (int) remaining_face[j][tri->tet_vertex]; + y_vertex1 = EVALUATE(tri->tet->gluing[j], x_vertex1); + y_vertex2 = EVALUATE(tri->tet->gluing[j], x_vertex2); + + printf(" (Tet Index: %d, Tet Vertex: %d) Cusp Edge %d glues to " + "(Tet Index: %d, Tet Vertex: %d) Cusp Edge %d. (%d -> %d, %d -> %d)\n", + tri->tet_index, // Tet Index + tri->tet_vertex, // Tet Vertex + j, // Cusp Edge + tri->tet->neighbor[j]->index, // Tet Index + EVALUATE(tri->tet->gluing[j], tri->tet_vertex), // Tet Vertex + EVALUATE(tri->tet->gluing[j], j), // Cusp Edge + x_vertex1, y_vertex1, + x_vertex2, y_vertex2 + ); + } + } + } + } else if (strcmp(type, "cusp_regions") == 0) { + printf("Cusp Region info\n"); + + for (i = 0; i < manifold->num_cusps; i++) { + printf("Boundary %d\n", i); + + cusp = cusps[i]; + for (j = 0; j < 4 * cusp->manifold->num_tetrahedra; j++) { + printf(" Cusp Triangle (Tet Index %d Tet Vertex %d)\n", j / 4, j % 4); + for (region = cusp->cusp_region_begin[j].next; + region != &cusp->cusp_region_end[j]; region = region->next) { + v1 = edgesThreeToFour[region->tet_vertex][0]; + v2 = edgesThreeToFour[region->tet_vertex][1]; + v3 = edgesThreeToFour[region->tet_vertex][2]; + + printf(" Region %d (Tet Index: %d, Tet Vertex: %d) (Adj Tri: %d, %d, %d) (Adj Regions: %d, %d, %d) " + " (Curves: [%d %d] [%d %d] [%d %d]) (Adj Curves: [%d %d] [%d %d] [%d %d]) (Dive: [%d %d] [%d %d] [%d %d])\n", + region->index, region->tet_index, region->tet_vertex, + region->adj_cusp_triangle[v1], region->adj_cusp_triangle[v2], region->adj_cusp_triangle[v3], + region->adj_cusp_regions[v1] == NULL ? -1 : region->adj_cusp_regions[v1]->index, + region->adj_cusp_regions[v2] == NULL ? -1 : region->adj_cusp_regions[v2]->index, + region->adj_cusp_regions[v3] == NULL ? -1 : region->adj_cusp_regions[v3]->index, + region->curve[v2][v1], region->curve[v3][v1], + region->curve[v1][v2], region->curve[v3][v2], + region->curve[v1][v3], region->curve[v2][v3], + region->num_adj_curves[v2][v1], region->num_adj_curves[v3][v1], + region->num_adj_curves[v1][v2], region->num_adj_curves[v3][v2], + region->num_adj_curves[v1][v3], region->num_adj_curves[v2][v3], + region->dive[v2][v1], region->dive[v3][v1], + region->dive[v1][v2], region->dive[v3][v2], + region->dive[v1][v3], region->dive[v2][v3] + ); + } + } + } + + } else if (strcmp(type, "homology") == 0) { + printf("Homology info\n"); + for (i = 0; i < manifold->num_cusps; i++) { + cusp = cusps[i]; + + printf("Boundary %d\n", i); + printf("Intersect Tet Index %d, Intersect Tet Vertex %d\n", cusp->intersect_tet_index, cusp->intersect_tet_vertex); + printf(" Meridian\n"); + + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + printf(" (Tet Index: %d, Tet Vertex: %d) %d %d %d %d\n", + tri->tet_index, + tri->tet_vertex, + tri->tet->curve[M][right_handed][tri->tet_vertex][0], + tri->tet->curve[M][right_handed][tri->tet_vertex][1], + tri->tet->curve[M][right_handed][tri->tet_vertex][2], + tri->tet->curve[M][right_handed][tri->tet_vertex][3] + ); + } + printf(" Longitude\n"); + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + printf(" (Tet Index: %d, Tet Vertex: %d) %d %d %d %d\n", + tri->tet_index, + tri->tet_vertex, + tri->tet->curve[L][right_handed][tri->tet_vertex][0], + tri->tet->curve[L][right_handed][tri->tet_vertex][1], + tri->tet->curve[L][right_handed][tri->tet_vertex][2], + tri->tet->curve[L][right_handed][tri->tet_vertex][3] + ); + } + } + + } else if (strcmp(type, "edge_indices") == 0) { + printf("Edge classes\n"); + + for (i = 0; i < manifold->num_cusps; i++) { + printf("Boundary %d\n", i); + + cusp = cusps[i]; + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + v1 = edgesThreeToFour[tri->tet_vertex][0]; + v2 = edgesThreeToFour[tri->tet_vertex][1]; + v3 = edgesThreeToFour[tri->tet_vertex][2]; + + printf(" (Tet Index: %d, Tet Vertex: %d) Vertex %d: (%d %d), " + "Vertex %d: (%d %d), Vertex %d: (%d %d)\n", + tri->tet_index, tri->tet_vertex, + v1, tri->vertices[v1].edge_class, tri->vertices[v1].edge_index, + v2, tri->vertices[v2].edge_class, tri->vertices[v2].edge_index, + v3, tri->vertices[v3].edge_class, tri->vertices[v3].edge_index + ); + } + } + } else if (strcmp(type, "dual_curves") == 0) { + printf("Oscillating curve paths\n"); + + // which dual curve + for (i = 0; i < curves->num_curves; i++) { + j = 0; + + printf("Dual Curve %d\n", i); + // which curve component + for (path = curves->curve_begin[i].next; path != &curves->curve_end[i]; path = path->next) { + printf(" Part %d: \n", j); + + for (path_node = path->path_begin.next; + path_node != &path->path_end; + path_node = path_node->next) + printf(" Node %d: (Tet Index %d, Tet Vertex %d) Next Face: %d, Prev Face: %d, Inside Vertex: %d\n", + path_node->cusp_region_index, path_node->tri->tet_index, path_node->tri->tet_vertex, + path_node->next_face, path_node->prev_face, path_node->inside_vertex + ); + j++; + } + } + } else if (strcmp(type, "inside_edge") == 0) { + printf("Inside edge info\n"); + + for (i = 0; i < manifold->num_cusps; i++) { + printf("Boundary %d\n", i); + + cusp = cusps[i]; + for (tri = cusp->cusp_triangle_begin.next; tri != &cusp->cusp_triangle_end; tri = tri->next) { + printf(" (Tet Index: %d, Tet Vertex: %d) Edge label (%d, %d, %d)\n", + tri->tet_index, // Tet Index + tri->tet_vertex, // Tet Vertex + edge3_between_faces[edgesThreeToFour[tri->tet_vertex][1]][edgesThreeToFour[tri->tet_vertex][2]], + edge3_between_faces[edgesThreeToFour[tri->tet_vertex][0]][edgesThreeToFour[tri->tet_vertex][2]], + edge3_between_faces[edgesThreeToFour[tri->tet_vertex][0]][edgesThreeToFour[tri->tet_vertex][1]] + ); + } + } + } else if (strcmp(type, "graph") == 0) { + printf("Graph info\n"); + + for (i = 0; i < manifold->num_cusps; i++) { + cusp = cusps[i]; + + printf("Boundary %d\n", i); + g = cusp->cusp_region_graph; + for (j = 0; j < 4 * manifold->num_tetrahedra; j++) { + for (region = cusp->cusp_region_begin[j].next; + region != &cusp->cusp_region_end[j]; + region = region->next) { + if (region->index >= g->num_vertices) { + continue; + } + + printf(" Vertex %d (Tet Index: %d, Tet Vertex: %d): ", + region->index, region->tet_index, region->tet_vertex + ); + for (edge_node = g->edge_list_begin[region->index].next; + edge_node != &g->edge_list_end[region->index]; + edge_node = edge_node->next) + printf("%d ", edge_node->y); + + printf("\n"); + } + } + } + } else if (strcmp(type, "endpoints") == 0) { + printf("EndPoint Info\n"); + + // which curve + for (i = 0; i < curves->num_curves; i++) { + printf("Dual Curve %d\n", i); + + j = 0; + // which component + for (path = curves->curve_begin[i].next; path != &curves->curve_end[i]; path = path->next) { + printf(" Part %d Cusp %d\n", j, path->endpoints[0].tri->tet->cusp[path->endpoints[0].tri->tet_vertex]->index); + for (k = 0; k < 2; k++) { + if (k == 0) + printf(" Start: "); + else + printf(" End: "); + + x_vertex1 = (int) remaining_face[path->endpoints[k].tri->tet_vertex][path->endpoints[k].vertex]; + x_vertex2 = (int) remaining_face[path->endpoints[k].vertex][path->endpoints[k].tri->tet_vertex]; + + printf("Region %d (Tet Index %d, Tet Vertex %d) Face %d Vertex %d Edge Class (%d, %d) Adj Curves %d\n", + path->endpoints[k].region_index, path->endpoints[k].tri->tet_index, + path->endpoints[k].tri->tet_vertex, path->endpoints[k].face, path->endpoints[k].vertex, + path->endpoints[k].tri->vertices[path->endpoints[k].vertex].edge_class, + path->endpoints[k].tri->vertices[path->endpoints[k].vertex].edge_index, + path->endpoints[k].num_adj_curves); + } + + j++; + } + } + } else { + printf("Unknown type: %s\n", type); + } + printf("-------------------------------\n"); +} + +// ------------------------------------ + +/* + * Find oscillating curves. Each curve is made up of an even number of + * components, with each component contained in a cusp, and connecting + * two cusp vertices. Each oscillating curve is associated to an edge + * of the triangulation, the rest of the edges come from the end multi + * graph. + * + * The result is stored in tet->extra[edge_class].curve[f][v] array + * on each tetrahedron. + */ + +void do_oscillating_curves(CuspStructure **cusps, OscillatingCurves *curves, EndMultiGraph *multi_graph) { + CuspEndPoint cusp_path_begin, cusp_path_end, *temp_cusp; + int i; + + cusp_path_begin.next = &cusp_path_end; + cusp_path_begin.prev = NULL; + cusp_path_end.next = NULL; + cusp_path_end.prev = &cusp_path_begin; + + for (i = 0; i < curves->num_curves; i++) { + find_multi_graph_path(cusps[0]->manifold, multi_graph, + &cusp_path_begin, &cusp_path_end, curves->edge_class[i]); + do_one_oscillating_curve(cusps, curves, multi_graph, &cusp_path_begin, &cusp_path_end, + curves->edge_class[i], i); + + while (cusp_path_begin.next != &cusp_path_end) { + temp_cusp = cusp_path_begin.next; + REMOVE_NODE(temp_cusp); + my_free(temp_cusp); + } + + if (debug) { + printf("\n"); + printf("Oscillating Curve %d\n", i); + printf("\n"); + printf("-------------------------------\n"); + + log_structs(cusps[0]->manifold, cusps, curves, "dual_curves"); + log_structs(cusps[0]->manifold, cusps, curves, "endpoints"); + log_structs(cusps[0]->manifold, cusps, curves, "cusp_regions"); + log_structs(cusps[0]->manifold, cusps, curves, "graph"); + } + } +} + +/* + * Construct a curve dual to the edge class 'edge_class'. The first and last + * components connect to edge_class which is not in the end multi graph so + * we need to find a new curve. Any intermediate components, if they exist, will + * make use of the train lines, as they consist of curves between edge classes + * in the end multi graph and thus is a segment of the train line. + */ + +void do_one_oscillating_curve(CuspStructure **cusps, OscillatingCurves *curves, EndMultiGraph *multi_graph, + CuspEndPoint *cusp_path_begin, CuspEndPoint *cusp_path_end, + int edge_class, int curve_index) { + int orientation = START; + CuspEndPoint *endpoint = cusp_path_begin->next; + CurveComponent *path, + *curve_begin = &curves->curve_begin[curve_index], + *curve_end = &curves->curve_end[curve_index]; + + curve_begin->edge_class[FINISH] = edge_class; + curve_end->edge_class[START] = edge_class; + + path = setup_first_curve_component(cusps[endpoint->cusp_index], multi_graph, endpoint, + curve_begin, curve_end); + do_curve_component_to_new_edge_class(cusps[path->cusp_index], path); + update_path_holonomy(path, edge_class); + + // interior curve components (not used for knots) + for (endpoint = endpoint->next; endpoint->next != cusp_path_end; endpoint = endpoint->next) { + // not implemented + uFatalError("do_one_oscillating_curve", "symplectic_basis"); + } + + orientation = (orientation == START ? FINISH : START); + + path = setup_last_curve_component(cusps[endpoint->cusp_index], multi_graph, endpoint, + curve_begin, curve_end); + do_curve_component_to_new_edge_class(cusps[path->cusp_index], path); + update_path_holonomy(path, edge_class); + + update_adj_curve_along_path(cusps, curves, curve_index, + (Boolean) (cusp_path_begin->next->next->next != cusp_path_end)); +} + +/* + * Initalise the first curve component of an oscillating curve. + * Set edge classes and find path endpoints. + */ + +CurveComponent *setup_first_curve_component(CuspStructure *cusp, EndMultiGraph *multi_graph, CuspEndPoint *endpoint, + CurveComponent *curves_begin, CurveComponent *curves_end) { + CurveComponent *path; + path = init_curve_component(endpoint->edge_class[START], endpoint->edge_class[FINISH], endpoint->cusp_index); + INSERT_BEFORE(path, curves_end); + + construct_cusp_region_dual_graph(cusp); + find_single_endpoint(cusp, &path->endpoints[START], path->edge_class[START], START); + find_single_endpoint(cusp, &path->endpoints[FINISH], path->edge_class[FINISH], START); + return path; +} + +/* + * Initalise the last curve component of an oscillating curve. + * Set edge classes and find path endpoints. + */ + +CurveComponent *setup_last_curve_component(CuspStructure *cusp, EndMultiGraph *multi_graph, CuspEndPoint *endpoint, + CurveComponent *curves_begin, CurveComponent *curves_end) { + CurveComponent *path; + path = init_curve_component(endpoint->edge_class[START], endpoint->edge_class[FINISH], endpoint->cusp_index); + INSERT_BEFORE(path, curves_end); + + construct_cusp_region_dual_graph(cusp); + find_single_matching_endpoint(cusp, &curves_begin->next->endpoints[START], &path->endpoints[START]); + find_single_matching_endpoint(cusp, &path->prev->endpoints[FINISH], &path->endpoints[FINISH]); + + return path; +} + +/* + * Construct an oscillating curve component, which is either the + * first or last component of an oscillating curve. + */ + +void do_curve_component_to_new_edge_class(CuspStructure *cusp, CurveComponent *curve) { + int *parent; + Boolean *processed, *discovered; + EdgeNode node_begin, node_end; + + processed = NEW_ARRAY(cusp->cusp_region_graph->num_vertices, Boolean); + discovered = NEW_ARRAY(cusp->cusp_region_graph->num_vertices, Boolean); + parent = NEW_ARRAY(cusp->cusp_region_graph->num_vertices, int); + + node_begin.next = &node_end; + node_begin.prev = NULL; + node_end.next = NULL; + node_end.prev = &node_begin; + + // Find curve using bfs + init_search(cusp->cusp_region_graph, processed, discovered, parent); + bfs(cusp->cusp_region_graph, curve->endpoints[START].region_index, processed, discovered, parent); + + find_path(curve->endpoints[START].region_index, curve->endpoints[FINISH].region_index, + parent, &node_begin, &node_end); + graph_path_to_dual_curve(cusp, &node_begin, &node_end, + &curve->path_begin, &curve->path_end, + &curve->endpoints[START], &curve->endpoints[FINISH]); + + // Reallocate memory + my_free(processed); + my_free(discovered); + my_free(parent); + + // Split the regions along the curve + split_cusp_regions_along_path(cusp, &curve->path_begin, &curve->path_end, + &curve->endpoints[START], &curve->endpoints[FINISH]); + + free_edge_node(&node_begin, &node_end); +} + + +/* + * Find a cusp region which can dive along a face into a vertex of + * the cusp triangle which corresponds to 'edge_class' and 'edge_index', + * and store the result in path_endpoint. + */ + +void find_single_endpoint(CuspStructure *cusp, PathEndPoint *path_endpoint, int edge_class, int edge_index) { + int i; + VertexIndex vertex; + FaceIndex face1, face2, face; + CuspRegion *region; + + // which cusp region + for (i = 0; i < cusp->num_cusp_triangles; i++) { + for (region = cusp->cusp_region_begin[i].next; + region != &cusp->cusp_region_end[i]; + region = region->next) { + + // which vertex to dive through + for (vertex = 0; vertex < 4; vertex++) { + if (vertex == region->tet_vertex) + continue; + + if (region->tri->vertices[vertex].edge_class != edge_class) + continue; + + if (region->tri->vertices[vertex].edge_index != edge_index) + continue; + + face1 = remaining_face[region->tet_vertex][vertex]; + face2 = remaining_face[vertex][region->tet_vertex]; + + if (region->dive[face1][vertex]) + face = face1; + else if (region->dive[face2][vertex]) + face = face2; + else + continue; + + path_endpoint->region = region; + path_endpoint->tri = region->tri; + path_endpoint->vertex = vertex; + path_endpoint->face = face; + path_endpoint->region_index = region->index; + path_endpoint->num_adj_curves = region->num_adj_curves[path_endpoint->face][path_endpoint->vertex]; + + return ; + } + } + } + + // didn't find valid path endpoints + uFatalError("find_single_endpoints", "symplectic_basis"); +} + +/* + * Find a cusp region which can dive into a vertex of the cusp triangle + * corresponding 'edge_class' and 'edge_index', while matching path_endpoint1. + * + * See 'region_index', 'region_vertex', 'region_dive', 'region_curve' for the + * conditions for a matching endpoint. + */ + +void find_single_matching_endpoint(CuspStructure *cusp, PathEndPoint *path_endpoint1, PathEndPoint *path_endpoint2) { + int i; + Boolean region_index, region_vertex, region_dive, region_curve; + CuspRegion *region; + + // which cusp region + for (i = 0; i < cusp->num_cusp_triangles; i++) { + for (region = cusp->cusp_region_begin[i].next; + region != &cusp->cusp_region_end[i]; + region = region->next) { + + // are we in the matching endpoint + region_index = (Boolean) (region->tet_index != path_endpoint1->tri->tet_index); + region_vertex = (Boolean) (region->tet_vertex != path_endpoint1->vertex); + region_dive = (Boolean) !region->dive[path_endpoint1->face][path_endpoint1->tri->tet_vertex]; + region_curve = (Boolean) (region->num_adj_curves[path_endpoint1->face][path_endpoint1->tri->tet_vertex] + != path_endpoint1->num_adj_curves); + + if (region_index || region_vertex || region_dive || region_curve) + continue; + + path_endpoint2->region = region; + path_endpoint2->tri = region->tri; + path_endpoint2->vertex = path_endpoint1->tri->tet_vertex; + path_endpoint2->face = path_endpoint1->face; + path_endpoint2->region_index = region->index; + path_endpoint2->num_adj_curves = region->num_adj_curves[path_endpoint2->face][path_endpoint2->vertex]; + + return ; + } + } + + // didn't find valid path endpoints + uFatalError("find_single_matching_endpoints", "symplectic_basis"); +} + +/* + * After finding a path, each node contains the index of the region it lies in. + * Update path info calculates the face the path crosses to get to the next node + * and the vertex it cuts off to simplify combinatorial holonomy calculation. + */ + +void graph_path_to_dual_curve(CuspStructure *cusp, EdgeNode *node_begin, EdgeNode *node_end, PathNode *path_begin, + PathNode *path_end, PathEndPoint *start_endpoint, PathEndPoint *finish_endpoint) { + FaceIndex face; + EdgeNode *edge_node; + PathNode *path_node; + CuspRegion *region; + + // path len 0 + if (node_begin->next == node_end) + return; + + edge_node = node_begin->next; + // path len 1 + if (edge_node->next == node_end) { + for (face = 0; face < 4; face++) + if (cusp->regions[edge_node->y]->tet_vertex != face && + start_endpoint->vertex != face && + finish_endpoint->vertex != face) + break; + + region = cusp->regions[edge_node->y]; + + path_node = NEW_STRUCT( PathNode ); + INSERT_BEFORE(path_node, path_end); + path_node->next_face = finish_endpoint->face; + path_node->prev_face = start_endpoint->face; + path_node->cusp_region_index = edge_node->y; + path_node->tri = region->tri; + path_node->inside_vertex = face; + return; + } + + // Set Header node + endpoint_edge_node_to_path_node(cusp->regions[edge_node->y], path_end, edge_node, + start_endpoint, START); + + for (edge_node = node_begin->next->next; edge_node->next != node_end; edge_node = edge_node->next) + interior_edge_node_to_path_node(cusp->regions[edge_node->y], path_end, edge_node); + + // Set Tail node + endpoint_edge_node_to_path_node(cusp->regions[edge_node->y], path_end, edge_node, + finish_endpoint, FINISH); +} + +void endpoint_edge_node_to_path_node(CuspRegion *region, PathNode *path_end, EdgeNode *edge_node, + PathEndPoint *path_endpoint, int pos) { + FaceIndex face; + VertexIndex vertex1, vertex2; + PathNode *path_node = NEW_STRUCT( PathNode ); + path_node->cusp_region_index = edge_node->y; + path_node->tri = region->tri; + + vertex1 = remaining_face[region->tet_vertex][path_endpoint->vertex]; + vertex2 = remaining_face[path_endpoint->vertex][region->tet_vertex]; + + if (pos == START) { + path_node->next_face = -1; + for (face = 0; face < 4; face++) { + if (face == region->tet_vertex || !region->adj_cusp_triangle[face] || path_node->next_face != -1) + continue; + + if (region->adj_cusp_regions[face]->index == edge_node->next->y) + path_node->next_face = face; + } + + // next node isn't in an adjacent region + if (path_node->next_face == -1) + uFatalError("endpoint_edge_node_to_path_node", "symplectic_basis"); + + path_node->prev_face = path_endpoint->face; + + if (path_node->next_face == path_endpoint->vertex) { + if (path_endpoint->face == vertex1) + path_node->inside_vertex = vertex2; + else + path_node->inside_vertex = vertex1; + } else if (path_node->next_face == path_endpoint->face) { + path_node->inside_vertex = -1; + } else { + path_node->inside_vertex = path_endpoint->vertex; + } + } else { + path_node->prev_face = EVALUATE(path_end->prev->tri->tet->gluing[path_end->prev->next_face], + path_end->prev->next_face); + path_node->next_face = path_endpoint->face; + + if (path_node->prev_face == path_endpoint->vertex) { + if (path_endpoint->face == vertex1) + path_node->inside_vertex = vertex2; + else + path_node->inside_vertex = vertex1; + } else if (path_node->prev_face == path_endpoint->face) { + path_node->inside_vertex = -1; + } else { + path_node->inside_vertex = path_endpoint->vertex; + } + } + + INSERT_BEFORE(path_node, path_end); +} + +/* + * node lies in 'region', find the vertex which the subpath + * node->prev->y --> node->y --> node->next->y cuts off of the cusp triangle + * >tri. + */ + +void interior_edge_node_to_path_node(CuspRegion *region, PathNode *path_end, EdgeNode *edge_node) { + VertexIndex vertex1, vertex2; + PathNode *path_node = NEW_STRUCT( PathNode ); + path_node->cusp_region_index = edge_node->y; + path_node->tri = region->tri; + + path_node->prev_face = EVALUATE(path_end->prev->tri->tet->gluing[path_end->prev->next_face], + path_end->prev->next_face); + + vertex1 = remaining_face[path_node->tri->tet_vertex][path_node->prev_face]; + vertex2 = remaining_face[path_node->prev_face][path_node->tri->tet_vertex]; + + if (region->adj_cusp_triangle[vertex1] && region->adj_cusp_regions[vertex1]->index == edge_node->next->y) { + path_node->next_face = vertex1; + path_node->inside_vertex = vertex2; + } else if (region->adj_cusp_triangle[vertex2] && region->adj_cusp_regions[vertex2]->index == edge_node->next->y) { + path_node->next_face = vertex2; + path_node->inside_vertex = vertex1; + } else + uFatalError("interior_edge_node_to_path_node", "symplectic_basis"); + + INSERT_BEFORE(path_node, path_end); +} + +/* + * The oscillating curve splits the region it passes through into two regions. + * Split each region in two and update attributes + */ + +void split_cusp_regions_along_path(CuspStructure *cusp, PathNode *path_begin, PathNode *path_end, + PathEndPoint *start_endpoint, PathEndPoint *finish_endpoint) { + int index = cusp->num_cusp_regions, region_index; + PathNode *node; + CuspRegion *region; + + // empty path + if (path_begin->next == path_end) + return ; + + // path of len 1 + if (path_begin->next->next == path_end) { + split_path_len_one(cusp, path_begin->next, start_endpoint, finish_endpoint); + return; + } + + /* + * Update first region + * + * Standing at the vertex where the curve dives through, and looking + * at the opposite face, region becomes the cusp region to the right + * of the curve and region to the left of the curve. + */ + node = path_begin->next; + region = cusp->regions[node->cusp_region_index]; + region_index = TRI_TO_INDEX(region->tet_index, region->tet_vertex); + update_cusp_triangle_endpoints(&cusp->cusp_region_begin[region_index], + &cusp->cusp_region_end[region_index], + region, start_endpoint, node, START); + split_cusp_region_path_endpoint(&cusp->cusp_region_end[region_index], region, + node, start_endpoint, index, START); + index++; + + // interior edges + while ((node = node->next)->next->next != NULL) { + region = cusp->regions[node->cusp_region_index]; + region_index = TRI_TO_INDEX(region->tet_index, region->tet_vertex); + update_cusp_triangle_path_interior(&cusp->cusp_region_begin[region_index], + &cusp->cusp_region_end[region_index], region, node); + split_cusp_region_path_interior(&cusp->cusp_region_end[region_index], region, node, index); + index++; + } + + // update last region + region = cusp->regions[node->cusp_region_index]; + region_index = TRI_TO_INDEX(region->tet_index, region->tet_vertex); + update_cusp_triangle_endpoints(&cusp->cusp_region_begin[region_index], + &cusp->cusp_region_end[region_index], + region, finish_endpoint, node, FINISH); + split_cusp_region_path_endpoint(&cusp->cusp_region_end[region_index], region, + node, finish_endpoint, index, FINISH); + index++; + + update_adj_region_data(cusp); + cusp->num_cusp_regions = index; +} + +void split_path_len_one(CuspStructure *cusp, PathNode *node, PathEndPoint *start_endpoint, PathEndPoint *finish_endpoint) { + int index = cusp->num_cusp_regions, region_index; + FaceIndex face; + CuspRegion *new_region, *old_region, *region; + + new_region = NEW_STRUCT(CuspRegion); + old_region = cusp->regions[node->cusp_region_index]; + region_index = TRI_TO_INDEX(old_region->tet_index, old_region->tet_vertex); + INSERT_BEFORE(new_region, &cusp->cusp_region_end[region_index]) + copy_region(old_region, new_region); + + face = node->inside_vertex; + + new_region->index = index; + new_region->adj_cusp_triangle[start_endpoint->vertex] = FALSE; + new_region->adj_cusp_triangle[finish_endpoint->vertex] = FALSE; + new_region->dive[face][start_endpoint->vertex] = TRUE; + new_region->dive[face][finish_endpoint->vertex] = TRUE; + new_region->dive[start_endpoint->vertex][finish_endpoint->vertex] = (Boolean) (face != finish_endpoint->face); + new_region->dive[finish_endpoint->vertex][start_endpoint->vertex] = (Boolean) (face != start_endpoint->face); + new_region->temp_adj_curves[start_endpoint->vertex][finish_endpoint->vertex]++; + new_region->temp_adj_curves[finish_endpoint->vertex][start_endpoint->vertex]++; + + old_region->adj_cusp_triangle[face] = FALSE; + old_region->dive[face][start_endpoint->vertex] = (Boolean) (face == start_endpoint->face); + old_region->dive[face][finish_endpoint->vertex] = (Boolean) (face == finish_endpoint->face); + old_region->temp_adj_curves[face][start_endpoint->vertex]++; + old_region->temp_adj_curves[face][finish_endpoint->vertex]++; + + // update other cusp regions + for (region = cusp->cusp_region_begin[region_index].next; + region != &cusp->cusp_region_end[region_index]; + region = region->next) { + + if (new_region->tet_index != region->tet_index || new_region->tet_vertex != region->tet_vertex) + continue; + + if (region == new_region || region == old_region) + continue; + + if (region->adj_cusp_triangle[start_endpoint->vertex] || region->adj_cusp_triangle[finish_endpoint->vertex]) { + region->temp_adj_curves[face][finish_endpoint->vertex]++; + region->temp_adj_curves[face][start_endpoint->vertex]++; + + } else { + region->temp_adj_curves[start_endpoint->vertex][finish_endpoint->vertex]++; + region->temp_adj_curves[finish_endpoint->vertex][start_endpoint->vertex]++; + } + } + + update_adj_region_data(cusp); + cusp->num_cusp_regions++; +} + +/* + * Set the new and old region data. Draw a picture to see how the attributes + * change in each case + */ + +void split_cusp_region_path_interior(CuspRegion *region_end, CuspRegion *region, PathNode *node, int index) { + int v1, v2; + CuspRegion *new_region = NEW_STRUCT( CuspRegion ); + + v1 = (int) remaining_face[region->tet_vertex][node->inside_vertex]; + v2 = (int) remaining_face[node->inside_vertex][region->tet_vertex]; + + /* + * new_region becomes the cusp region closest to the inside vertex and + * region becomes the cusp region on the other side of the oscillating curve + */ + copy_region(region, new_region); + new_region->index = index; + + // Update new region + new_region->curve[v1][v2]++; + new_region->curve[v2][v1]++; + new_region->dive[v1][node->inside_vertex] = region->dive[v1][node->inside_vertex]; + new_region->dive[v2][node->inside_vertex] = region->dive[v2][node->inside_vertex]; + new_region->adj_cusp_triangle[node->inside_vertex] = FALSE; + + // Update region + region->curve[v1][node->inside_vertex]++; + region->curve[v2][node->inside_vertex]++; + region->dive[v1][node->inside_vertex] = FALSE; + region->dive[v2][node->inside_vertex] = FALSE; + + INSERT_BEFORE(new_region, region_end); +} + +void split_cusp_region_path_endpoint(CuspRegion *region_end, CuspRegion *region, PathNode *path_node, + PathEndPoint *path_endpoint, int index, int pos) { + FaceIndex face; + VertexIndex vertex1, vertex2; + CuspRegion *new_region = NEW_STRUCT(CuspRegion); + + vertex1 = remaining_face[region->tet_vertex][path_endpoint->vertex]; + vertex2 = remaining_face[path_endpoint->vertex][region->tet_vertex]; + + /* + * Region becomes the cusp region closest to the inside vertex and + * new_region becomes the cusp region on the other side of the oscillating curve + */ + copy_region(region, new_region); + new_region->index = index; + path_endpoint->region = NULL; + + if (pos == START) { + face = path_node->next_face; + } else { + face = path_node->prev_face; + } + + if (face == path_endpoint->vertex) { + // curve passes through the face opposite the vertex it dives through + new_region->curve[path_endpoint->vertex][vertex2]++; + new_region->temp_adj_curves[vertex1][path_endpoint->vertex]++; + new_region->dive[vertex1][path_endpoint->vertex] = (Boolean) (path_endpoint->face == vertex1); + new_region->dive[vertex2][path_endpoint->vertex] = region->dive[vertex2][path_endpoint->vertex]; + new_region->dive[vertex2][vertex1] = region->dive[vertex2][vertex1]; + new_region->dive[path_endpoint->vertex][vertex1] = region->dive[path_endpoint->vertex][vertex1]; + new_region->adj_cusp_triangle[vertex1] = FALSE; + + region->curve[path_endpoint->vertex][vertex1]++; + region->temp_adj_curves[vertex2][path_endpoint->vertex]++; + region->dive[vertex2][path_endpoint->vertex] = (Boolean) (path_endpoint->face == vertex2); + region->dive[vertex2][vertex1] = FALSE; + region->dive[path_endpoint->vertex][vertex1] = FALSE; + region->adj_cusp_triangle[vertex2] = FALSE; + } else if (face == path_endpoint->face) { + // curve passes through the face that carries it + new_region->curve[path_endpoint->face][path_endpoint->face == vertex1 ? vertex2 : vertex1]++; + new_region->temp_adj_curves[face == vertex1 ? vertex2 : vertex1][path_endpoint->vertex]++; + new_region->dive[path_endpoint->face][path_endpoint->vertex] + = region->dive[path_endpoint->face][path_endpoint->vertex]; + new_region->adj_cusp_triangle[path_endpoint->vertex] = FALSE; + new_region->adj_cusp_triangle[path_endpoint->face == vertex1 ? vertex2 : vertex1] = FALSE; + + region->curve[path_endpoint->face][path_endpoint->vertex]++; + region->temp_adj_curves[face][path_endpoint->vertex]++; + } else { + // Curve goes around the vertex + new_region->curve[face][path_endpoint->face]++; + new_region->temp_adj_curves[path_endpoint->face][path_endpoint->vertex]++; + new_region->dive[vertex1][path_endpoint->vertex] = region->dive[vertex1][path_endpoint->vertex]; + new_region->dive[vertex2][path_endpoint->vertex] = region->dive[vertex2][path_endpoint->vertex]; + new_region->adj_cusp_triangle[path_endpoint->face] = FALSE; + new_region->adj_cusp_triangle[path_endpoint->vertex] = FALSE; + + region->curve[face][path_endpoint->vertex]++; + region->temp_adj_curves[face][path_endpoint->vertex]++; + region->dive[path_endpoint->face == vertex1 ? vertex2 : vertex1][path_endpoint->vertex] = FALSE; + } + + INSERT_BEFORE(new_region, region_end); +} + +/* + * After splitting each region the path travels through, the attributes for + * other regions in the same cusp triangle is now out of date. Update cusp + * triangles for nodes in the interior of the path. + */ + +void update_cusp_triangle_path_interior(CuspRegion *cusp_region_start, CuspRegion *cusp_region_end, + CuspRegion *region, PathNode *node) { + int face1, face2; + CuspRegion *current_region; + + face1 = (int) remaining_face[region->tet_vertex][node->inside_vertex]; + face2 = (int) remaining_face[node->inside_vertex][region->tet_vertex]; + + for (current_region = cusp_region_start->next; + current_region != cusp_region_end; + current_region = current_region->next) { + + // which triangle are we in? + if (current_region->tet_index != region->tet_index || current_region->tet_vertex != region->tet_vertex) + continue; + + if (current_region->curve[face1][node->inside_vertex] > region->curve[face1][node->inside_vertex]) { + current_region->curve[face1][node->inside_vertex]++; + } + else if (current_region->curve[face1][node->inside_vertex] < region->curve[face1][node->inside_vertex]) { + current_region->curve[face1][face2]++; + } + + if (current_region->curve[face2][node->inside_vertex] > region->curve[face2][node->inside_vertex]) { + current_region->curve[face2][node->inside_vertex]++; + } + else if (current_region->curve[face2][node->inside_vertex] < region->curve[face2][node->inside_vertex]) { + current_region->curve[face2][face1]++; + } + } +} + +/* + * After splitting each curveRegion the path travels through, the attributes + * for other regions in the same cusp triangle is now out of date. Update cusp + * triangles for nodes at the end of the path. + */ + +void update_cusp_triangle_endpoints(CuspRegion *cusp_region_start, CuspRegion *cusp_region_end, CuspRegion *region, + PathEndPoint *path_endpoint, PathNode *node, int pos) { + FaceIndex face, face1, face2; + CuspRegion *current_region; + + face1 = remaining_face[region->tet_vertex][path_endpoint->vertex]; + face2 = remaining_face[path_endpoint->vertex][region->tet_vertex]; + + if (pos == START) { + face = node->next_face; + } else { + face = node->prev_face; + } + + for (current_region = cusp_region_start->next; + current_region != cusp_region_end; + current_region = current_region->next) { + if (current_region == NULL || current_region->tet_index == -1) + continue; + + // which triangle are we in? + if (current_region->tet_index != region->tet_index || current_region->tet_vertex != region->tet_vertex) + continue; + + if (face == path_endpoint->vertex) { + // curve passes through the face opposite the vertex it dives through + if (!current_region->adj_cusp_triangle[face]) { + if (!current_region->adj_cusp_triangle[face1]) { + current_region->temp_adj_curves[face1][path_endpoint->vertex]++; + } else if (!current_region->adj_cusp_triangle[face2]) { + current_region->temp_adj_curves[face2][path_endpoint->vertex]++; + } else { + uFatalError("update_cusp_triangle_endpoints", "symplectic_basis"); + } + } else if (current_region->curve[path_endpoint->vertex][face1] > region->curve[path_endpoint->vertex][face1]) { + current_region->curve[face][face1]++; + current_region->temp_adj_curves[face2][path_endpoint->vertex]++; + } else if (current_region->curve[path_endpoint->vertex][face1] < region->curve[path_endpoint->vertex][face1]) { + current_region->curve[face][face2]++; + current_region->temp_adj_curves[face1][path_endpoint->vertex]++; + } + + continue; + } + + if (!current_region->adj_cusp_triangle[face]) { + current_region->temp_adj_curves[face][path_endpoint->vertex]++; + continue; + } + + // Curve goes around the vertex or passes through the face that carries it + if (current_region->curve[face][path_endpoint->vertex] > region->curve[face][path_endpoint->vertex]) { + current_region->curve[face][path_endpoint->vertex]++; + current_region->temp_adj_curves[face][path_endpoint->vertex]++; + + } else if (current_region->curve[face][path_endpoint->vertex] < region->curve[face][path_endpoint->vertex]) { + current_region->curve[face][face == face1 ? face2 : face1]++; + current_region->temp_adj_curves[face == face1 ? face2 : face1][path_endpoint->vertex]++; + } + } +} + +void update_adj_curve_along_path(CuspStructure **cusps, OscillatingCurves *curves, int curve_index, Boolean train_line) { + CurveComponent *curve, + *current_begin = &curves->curve_begin[curve_index], + *current_end = &curves->curve_end[curve_index]; + + // Update regions curve data along the current curve + for (curve = current_begin->next; curve != current_end; curve = curve->next) + update_adj_curve_on_cusp(cusps[curve->cusp_index]); + + // update endpoint curve data + for (int i = 0; i < curve_index; i++) { + // which oscillating curve + + for (curve = curves->curve_begin[i].next; curve != &curves->curve_end[i]; curve = curve->next) { + // which component of the curve + + for (int j = 0; j < 2; j++) { + // which end point + update_adj_curve_at_endpoint(&curve->endpoints[j], current_begin->next, START); + update_adj_curve_at_endpoint(&curve->endpoints[j], current_end->prev, FINISH); + } + } + } +} + +/* + * curve_begin and curve_end are header and tailer nodes of a doubly linked list of path + * components for a new path. Update the path_endpoint->num_adj_curves attribute to account for this + * new curve. + */ + +void update_adj_curve_at_endpoint(PathEndPoint *path_endpoint, CurveComponent *path, int pos) { + PathEndPoint *curve_end_point; + + curve_end_point = &path->endpoints[pos]; + + if (curve_end_point->tri->tet_index != path_endpoint->tri->tet_index || + curve_end_point->tri->tet_vertex != path_endpoint->tri->tet_vertex || + curve_end_point->face != path_endpoint->face || + curve_end_point->vertex != path_endpoint->vertex) + return; + + path_endpoint->num_adj_curves++; +} + +/* + * Move the temp adj curves into the current num of adj curves. + */ + +void update_adj_curve_on_cusp(CuspStructure *cusp) { + int i, j, k; + CuspRegion *region; + + for (i = 0; i < 4 * cusp->manifold->num_tetrahedra; i++) { + for (region = cusp->cusp_region_begin[i].next; region != &cusp->cusp_region_end[i]; region = region->next) { + // which cusp region + for (j = 0; j < 4; j++) { + for (k = 0; k < 4; k++) { + region->num_adj_curves[j][k] += region->temp_adj_curves[j][k]; + region->temp_adj_curves[j][k] = 0; + } + } + } + } +} + +void update_path_holonomy(CurveComponent *path, int edge_class) { + PathNode *path_node; + + for (path_node = path->path_begin.next; path_node != &path->path_end; path_node = path_node->next) { + path_node->tri->tet->extra[edge_class].curve[path_node->tri->tet_vertex][path_node->next_face]++; + path_node->tri->tet->extra[edge_class].curve[path_node->tri->tet_vertex][path_node->prev_face]--; + } +} + +// ------------------------------------------------------- + +/* + * End Multi Graph + * + * The end multi graph is a graph with vertices for the cusps of M and + * edges for each edge of the triangulation. We also refer to the spanning + * tree of this graph as the end multi graph. The end multi graph structure also + * keeps track of a special E_0 edge, which is used to construct a path of even + * length through the graph. + */ + +EndMultiGraph *init_end_multi_graph(Triangulation *manifold) { + int i, j; + int *parent; + EndMultiGraph *multi_graph = NEW_STRUCT( EndMultiGraph ); + + multi_graph->num_cusps = manifold->num_cusps; + multi_graph->num_edge_classes = manifold->num_tetrahedra; + + Graph *g = init_graph(multi_graph->num_cusps, FALSE); + cusp_graph(manifold, g); + + parent = NEW_ARRAY(g->num_vertices, int); + + multi_graph->multi_graph = spanning_tree(g, 0, parent); + color_graph(multi_graph->multi_graph); + + multi_graph->edges = find_end_multi_graph_edge_classes(multi_graph, manifold); + multi_graph->e0 = find_same_color_edge(manifold, multi_graph, g); + + multi_graph->edge_classes = NEW_ARRAY(multi_graph->num_edge_classes, Boolean); + for (i = 0; i < multi_graph->num_edge_classes; i++) { + multi_graph->edge_classes[i] = FALSE; + } + + for (i = 0; i < multi_graph->num_cusps; i++) { + for (j = 0; j < multi_graph->num_cusps; j++) { + if (multi_graph->edges[i][j] == -1) + continue; + + multi_graph->edge_classes[multi_graph->edges[i][j]] = TRUE; + } + } + + free_graph(g); + my_free(parent); + return multi_graph; +} + +void free_end_multi_graph(EndMultiGraph *multi_graph) { + int i; + + free_graph(multi_graph->multi_graph); + + for (i = 0; i < multi_graph->num_cusps; i++) + my_free(multi_graph->edges[i]); + + my_free(multi_graph->edge_classes); + my_free(multi_graph->edges); + my_free(multi_graph); +} + +void cusp_graph(Triangulation *manifold, Graph *g) { + int vertex1, vertex2; + Tetrahedron *tet; + + // which tet + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + // which vertex + for (vertex1 = 0; vertex1 < 4; vertex1++) { + // which vertex of the cusp triangle at vertex1 + for (vertex2 = 0; vertex2 < 4; vertex2++) { + if (vertex1 == vertex2) + continue; + + insert_edge(g, tet->cusp[vertex1]->index, tet->cusp[vertex2]->index, g->directed); + } + } + } +} + +/* + * Find a spanning tree of graph1 + */ + +Graph *spanning_tree(Graph *graph1, int start, int *parent) { + int i; + + Boolean *processed = NEW_ARRAY(graph1->num_vertices, Boolean); + Boolean *discovered = NEW_ARRAY(graph1->num_vertices, Boolean); + + Graph *graph2 = init_graph(graph1->num_vertices, graph1->directed); + + // Find path using bfs + init_search(graph1, processed, discovered, parent); + bfs(graph1, start, processed, discovered, parent); + + for (i = 0; i < graph1->num_vertices; i++) { + if (parent[i] == -1) + continue; + + insert_edge(graph2, i, parent[i], graph2->directed); + } + + my_free(processed); + my_free(discovered); + + return graph2; +} + +/* + * Assign an edge class to each edge of the graph g and return an array of + * Booleans indicating if an edge class is in the graph. + */ + +int **find_end_multi_graph_edge_classes(EndMultiGraph *multi_graph, Triangulation *manifold) { + int i, j, edge_class, **cusps; + EdgeNode *edge_node; + Graph *g = multi_graph->multi_graph; + + cusps = NEW_ARRAY(multi_graph->num_cusps, int *); + + for (i = 0; i < multi_graph->num_cusps; i++) { + cusps[i] = NEW_ARRAY(multi_graph->num_cusps, int); + + for (j = 0; j < multi_graph->num_cusps; j++) + cusps[i][j] = -1; + } + + for (i = 0; i < g->num_vertices; i++) { + for (edge_node = g->edge_list_begin[i].next; edge_node != &g->edge_list_end[i]; edge_node = edge_node->next) { + edge_class = find_edge_class(manifold, i, edge_node->y); + cusps[i][edge_node->y] = edge_class; + cusps[edge_node->y][i] = edge_class; + } + } + + return cusps; +} + +/* + * Find an edge class whose edge connects cusp1 and cusp2 + */ + +int find_edge_class(Triangulation *manifold, int cusp1, int cusp2) { + int v1, v2; + EdgeClass *edge; + Tetrahedron *tet; + + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + for (v1 = 0; v1 < 4; v1++) { + for (v2 = 0; v2 < 4; v2++) { + if (v1 == v2) + continue; + + if (tet->cusp[v1]->index != cusp1 || tet->cusp[v2]->index != cusp2) + continue; + + edge = tet->edge_class[edge_between_vertices[v1][v2]]; + return edge->index; + } + } + } + + uFatalError("find_edge_class", "symplectic_basis"); + return 0; +} + +void color_graph(Graph *g) { + int color = 0, v; + Queue *q = init_queue(g->num_vertices); + EdgeNode *node; + + g->color[0] = color; + q = enqueue(q, 0); + + while (!empty_queue(q)) { + v = dequeue(q); + color = g->color[v]; + + for (node = g->edge_list_begin[v].next; node != &g->edge_list_end[v]; node = node->next) { + // graph is not bipartite + if (g->color[node->y] == color) + uFatalError("color_graph", "symplectic_basis"); + + if (g->color[node->y] != -1) + continue; + + g->color[node->y] = !color; + q = enqueue(q, node->y); + } + } + + free_queue(q); +} + +/* + * g1 is the colored spanning tree of g2, return the edge class of the edge in + * g2 which connects vertices in g1 of the same color + */ + +int find_same_color_edge(Triangulation *manifold, EndMultiGraph *multi_graph, Graph *g2) { + int cusp; + EdgeNode *node; + Graph *g1 = multi_graph->multi_graph; + + for (cusp = 0; cusp < g2->num_vertices; cusp++) { + for (node = g2->edge_list_begin[cusp].next; node != &g2->edge_list_end[cusp]; node = node->next) { + if (g1->color[cusp] == g1->color[node->y] && multi_graph->edges[cusp][node->y] == -1) + // we found an edge + return find_edge_class(manifold, cusp, node->y); + } + } + + // we didn't find an edge connecting vertices of the same color + uFatalError("find_same_color_edge", "symplectic_basis"); + return -1; +} + +/* + * Find the length of a path between start and end + */ + +int find_path_len(int start, int end, int *parents, int path_length) { + if ((start == end) || (end == -1)) { + return path_length; + } else { + return find_path_len(start, parents[end], parents, path_length + 1); + } +} + +/* + * Find a path through the end multi graph, starting at 'edge_class' and which + * an odd length, since this corresponds to an even number of oscillating curve + * components. The path is stored in the doubly linked list cusp_path_begin -> + * cusp_path_end. + */ + +void find_multi_graph_path(Triangulation *manifold, EndMultiGraph *multi_graph, CuspEndPoint *cusp_path_begin, + CuspEndPoint *cusp_path_end, int edge_class) { + Graph *g = multi_graph->multi_graph; + Boolean *processed = NEW_ARRAY(g->num_vertices, Boolean); + Boolean *discovered = NEW_ARRAY(g->num_vertices, Boolean); + int *parent = NEW_ARRAY(g->num_vertices, int); + int start, end, startE0, endE0, path_len = 0; + EdgeNode node_begin, node_end; + + node_begin.next = &node_end; + node_begin.prev = NULL; + node_end.next = NULL; + node_end.prev = &node_begin; + + find_edge_ends(g, manifold, edge_class, &start, &end); + find_edge_ends(g, manifold, multi_graph->e0, &startE0, &endE0); + + init_search(g, processed, discovered, parent); + bfs(g, start, processed, discovered, parent); + + path_len = find_path_len(start, end, parent, path_len); + + if (path_len % 2 == 1) { + find_path(start, end, parent, &node_begin, &node_end); + } else { + init_search(g, processed, discovered, parent); + bfs(g, start, processed, discovered, parent); + + find_path(start, startE0, parent, &node_begin, &node_end); + + init_search(g, processed, discovered, parent); + bfs(g, endE0, processed, discovered, parent); + + find_path(endE0, end, parent, node_end.prev, &node_end); + } + + graph_path_to_cusp_path(multi_graph, &node_begin, &node_end, cusp_path_begin, cusp_path_end, edge_class); + + free_edge_node(&node_begin, &node_end); + my_free(parent); + my_free(discovered); + my_free(processed); +} + +/* + * Converts the EdgeNode path through the cusps in the end multigraph to + * a CuspEndPoint path which contains the edge classes on each cusp. + * A CuspEndPoint corresponding to one section of an oscillating curve, and + * constructing such a section for all CuspEndPoints gives the whole curve. + * + * node_begin -> node_end is a doubly linked list through the end multi graph + * and the result path is stored in the doubly linked list cusp_path_begin -> + * cusp_path_end. + */ + +void graph_path_to_cusp_path(EndMultiGraph *multi_graph, EdgeNode *node_begin, EdgeNode *node_end, + CuspEndPoint *cusp_path_begin, CuspEndPoint *cusp_path_end, int edge_class) { + int cusp, prev_edge_class; + EdgeNode *node; + CuspEndPoint *endpoint; + + prev_edge_class = edge_class; + for (node = node_begin->next; node->next != node_end; node = node->next) { + cusp = node->y; + + endpoint = NEW_STRUCT( CuspEndPoint ); + INSERT_BEFORE(endpoint, cusp_path_end); + + endpoint->cusp_index = cusp; + endpoint->edge_class[START] = prev_edge_class; + endpoint->edge_class[FINISH] = multi_graph->edges[node->y][node->next->y]; + + if (endpoint->edge_class[FINISH] == -1) + endpoint->edge_class[FINISH] = multi_graph->e0; + + prev_edge_class = endpoint->edge_class[FINISH]; + } + + endpoint = NEW_STRUCT( CuspEndPoint ); + INSERT_BEFORE(endpoint, cusp_path_end); + + endpoint->cusp_index = node->y; + endpoint->edge_class[START] = prev_edge_class; + endpoint->edge_class[FINISH] = edge_class; +} + +void find_edge_ends(Graph *g, Triangulation *manifold, int edge_class, int *start, int *end) { + int v1, v2; + Tetrahedron *tet; + EdgeClass *edge; + + // which tet + for (tet = manifold->tet_list_begin.next; tet != &manifold->tet_list_end; tet = tet->next) { + // which vertex + for (v1 = 0; v1 < 4; v1++) { + // which vertex of the cusp triangle at v1 + for (v2 = 0; v2 < 4; v2++) { + if (v1 == v2) + continue; + + edge = tet->edge_class[edge_between_vertices[v1][v2]]; + if (edge->index != edge_class) + continue; + + *start = tet->cusp[v1]->index; + *end = tet->cusp[v2]->index; + return; + } + } + } + + + // didn't find the edge class in the graph + uFatalError("find_edge_ends", "symplectic_basis"); +} diff --git a/quad_double/kernel_code/symplectic_basis.cpp b/quad_double/kernel_code/symplectic_basis.cpp new file mode 100644 index 000000000..9fd36a9b2 --- /dev/null +++ b/quad_double/kernel_code/symplectic_basis.cpp @@ -0,0 +1 @@ +#include \ No newline at end of file