\documentclass[10pt,twocolumn]{article} \usepackage{noweb,hyperref,amsmath,amsthm,amssymb} \noweboptions{smallcode,longchunks} \newtheorem{Thm}{Theorem} \newtheorem{Cor}[Thm]{Corollary} \newtheorem{Lem}[Thm]{Lemma} \theoremstyle{definition} \newtheorem{Def}[Thm]{Definition} \newtheorem{Rmk}[Thm]{Remark} \begin{document} \pagestyle{noweb} @ \title{[[unhyp]]} \author{Robert C. Haraway, III\thanks{Research partially supported by NSF grant DMS-1006553.}} \maketitle \paragraph{} This is a literate [[Python]] module to determine whether or not a compact orientable 3-manifold\footnote{We work in the PL category throughout.} with nonempty boundary\footnote{We require nonempty boundary because Regina implements no tests yet for small Seifert fiberings.} admits a complete hyperbolic metric on its interior. @ The following corollary of Thurston's hyperbolization theorem reduces this determination to a question about the existence of certain surfaces. \begin{Cor}\label{cor:hyp} A compact orientable bounded\footnote{That is, with nonempty boundary.} 3-manifold with $\chi = 0$ is hyp iff it has no faults. \end{Cor} @ The words hyp'' and fault'' mean the following. \begin{Def} A compact 3-manifold is \emph{hyp} just when its interior admits a complete, finite hyperbolic metric. \end{Def} \begin{Def} Let $s$ be a properly embedded codimension-1 submanifold of a connected p.l. manifold $M$. By abuse of notation, also let $s$ denote the image of $s$ in $M$. Pick a metric on $M$ compatible with its p.l. structure, and let $M'$ be the (abstract, i.e. not in $M$) path-metric completion of $M \smallsetminus s$. When $M'$ is disconnected, we say $s$ \emph{separates} $M$. When $M'$ has two connected components $N,N'$, we say $s$ \emph{cuts off (an) $N$ from $M$}. When $M$ is understood from context, we may say \emph{$s$ cuts off an $N$}, without reference to $M$. When $N,N'$ are not homeomorphic, we say $s$ cuts off \emph{one} $N$. \end{Def} @ \begin{Def} A properly embedded surface $s$ in an orientable 3-manifold $M$ is a \emph{fault} iff $\chi(s) \geq 0$ and it satisfies one of the following: \begin{itemize} \item $s$ is nonorientable. \item $s$ is a sphere which does not cut off a 3-ball. \item $s$ is a disc which does not off one 3-ball. \item $s$ is a torus which does not cut off a $T^2 \times I$, and does not cut off a $\partial$-compressible manifold. \item $s$ is an annulus which does not cut off a 3-ball, and does not cut off one $D^2 \times S^1.$ \end{itemize} \end{Def} @ A proof of Corollary \ref{cor:hyp} is sketched in \cite{Me}. Also found in \cite{Me} is the following algorithm, which we implement in [[Regina]]: \begin{verbatim} l := fundamental normal surfaces in T for surf in l: if surf is fault: return False T' := finite truncation of T if T' has a compressing disc: return False l' := vertex Q-normal surfaces in T' for annulus in l': if M has at least two boundary tori: if annulus is non-separating: return False else: if annulus is fault: return False else: return True \end{verbatim} Here is an implementation in [[Regina]]. <>= def unhypByNormalSurfaces(mfld): dno = mfld.getNumberOfBoundaryComponents() assert dno > 0 if not possiblyHyp(mfld): return True <> <> <> if isFault(surf): return True <> if TT.hasCompressingDisc(): return True <> <> if TT.getNumberOfBoundaryComponents() == 2: if isNonSeparatingAnnulus(surf): return True else: if isAnnulusFault(surf): return True else: return False <>= def possiblyHyp(mfld): m = mfld return m.isValid() \ and m.isOrientable() \ and m.getEulerCharManifold() == 0 \ and m.isConnected() <>= T = regina.NTriangulation(mfld) T.finiteToIdeal() T.intelligentSimplify() <>= nsl = regina.NNormalSurfaceList.enumerate std = regina.NS_STANDARD fnd = regina.NS_FUNDAMENTAL l = nsl(T,std,fnd) <>= n = l.getNumberOfSurfaces() for i in range(0,n): surf = l.getSurface(i) <>= TT = regina.NTriangulation(mfld) TT.idealToFinite() TT.intelligentSimplify() <>= vtx = regina.NS_VERTEX qd = regina.NS_QUAD ll = nsl(TT,qd,vtx) <>= nn = ll.getNumberOfSurfaces() for ii in range(0,nn): surf = ll.getSurface(ii) @ We need to implement the predicates is non-separating annulus,'' is fault,'' and is $T^2 \times I.$'' @ First, the test for whether or not a surface is a non-separating annulus. <>= def separates(surf): M = surf.cutAlong() return not M.isConnected() def isAnnulus(surf): return surf.hasRealBoundary() \ and surf.getEulerCharacteristic() == 0 \ and surf.isOrientable() def isNonSeparatingAnnulus(surf): return isAnnulus(surf) \ and not separates(surf) @ Next, the test for whether a surface is an annulus fault. <>= def isAnnulusFault(surf): return isAnnulus(surf) and \ isFault(surf) @ Later it will prove useful to find a non-separating annulus if one exists. So we do that here. <>= def findNonSeparatingAnnulus(mfld): T = mfld <> a = None <> if isNonSeparatingAnnulus(surf): a = surf break return a @ Now let us implement a fault test. This of course uses tests for 3-ball, $\partial$-compression, $D^2 \times S^1$, and $T^2\times I$, so abbreviate these. <>= b = lambda m: m.isBall() cd = lambda m: m.hasCompressingDisc() d2s1 = lambda m: m.isSolidTorus() t2i = isT2xI @ There aren't many quick sanity checks to do, so we inline them (so to speak): <>= def isFault(surf): s = surf x = s.getEulerCharacteristic() definitelyNotFault = \ x < 0 or \ not s.isConnected() or \ s.isVertexLink() if definitelyNotFault: return False @ At this point we know [[s]] has nonnegative Euler characteristic. If it's not orientable, then it's a fault. <>= if not s.isOrientable(): return True @ At this point, we know [[s]] is orientable with nonnegative Euler characteristic. So now we cut along it and see what we get. <>= M1 = s.cutAlong() M1.intelligentSimplify() @ If [[s]] doesn't cut off anything---i.e. if [[s]] doesn't separate---then it is a fault. <>= if M1.isConnected(): return True @ Otherwise, it separates, and we should look at the two pieces. <>= assert M1.splitIntoComponents() == 2 M2 = M1.getFirstTreeChild() M3 = M2.getNextTreeSibling() M2.idealToFinite() M2.intelligentSimplify() M3.idealToFinite() M3.intelligentSimplify() @ We run the tests now depending on whether [[s]] is closed or bounded, and depending on what its Euler characteristic is. @ When $s$ is a disc,\footnote{The test for hypness below doesn't ever run this code for discs, since we use [[hasCompressingDisc]] to find disc faults. But we include this for completeness.} $s$ is a fault when $s$ does not cut off \emph{one} 3-ball. So $s$ is a fault when either both $m_2,m_3$ are balls or neither $m_2$ nor $m_3$ is a ball. That is, when their ballnesses are equal, i.e. $\mbox{ball}(m_2) = \mbox{ball}(m_3)$. <>= if s.hasRealBoundary(): if x == 1: # s is a disc return b(M2) == b(M3) @ When $s$ is a separating annulus, $s$ is a fault when neither $s$ cuts off \emph{a} 3-ball, nor $s$ cuts off one solid torus. <>= else: # s had better be an annulus assert x == 0 return not (b(M2) or b(M3)) \ and d2s1(M2) == d2s1(M3) @ The rest should be clear. <>= else: # s is closed if x == 2: # s is a sphere return not (b(M2) or b(M3)) else: # s had better be a torus assert x == 0 return not (t2i(M2) or t2i(M3) \ or cd(M2) or cd(M3)) @ Now for the next part, $T^2 \times I$ detection using Dehn filling. \begin{Def} Suppose $M$ is finitely triangulated. Let $T$, $T'$ be boundary triangles adjacent along an edge $e$. Orient $e$ so that $T$ lies to its left and $T'$ to its right. Let $\Delta$ be a fresh tetrahedron, and let $\tau$, $\tau'$ be boundary triangles of $\Delta$ adjacent along an edge $\eta$. Orient $\eta$ so that $\tau$ lies to its left and $\tau'$ to its right. Without changing $M$'s topology we may glue $\Delta$ to $T$ by gluing $\eta$ to $e$, $\tau$ to $T'$ and $\tau'$ to $T$. This is called a \emph{two-two} move. The edge $\eta'$ opposite $\eta$ in $\Delta$ is now a boundary edge of the new finite triangulation. We say $e$ is \emph{embedded} iff its vertices are distinct. We say $e$ is \emph{co-embedded} or \emph{foldable} iff $\eta'$ is embedded. \end{Def} @ \begin{Rmk} We call a co-embedded edge foldable'' for the following reason. Given a boundary edge $e$ between two boundary triangles $T$ and $T'$, one may glue $T$ to $T'$ and $e$ to itself via a valid, orientation-reversing map, folding them together along $e$. This gluing will change the topology of $M$ when the vertices opposite $e$ in $T$ and $T'$ are the same vertex. Conversely, when these vertices are distinct, the folding preserves the topology. But the vertices are distinct iff $e$ is co-embedded. Hence the name foldable.'' \end{Rmk} @ Notice that folding along a foldable edge decreases the number of boundary triangles, and performing a two-two move on an embedded edge produces a foldable edge and preserves the number of boundary triangles. Therefore, the following [[while]]-loops terminate: @ \begin{verbatim} while there's an embedded boundary edge e: do a two-two move on e while there's a foldable boundary edge f: fold along f \end{verbatim} @ The obvious postcondition of the [[while]] loop is that there's no embedded boundary edge. Since the boundary is still triangulated, this is equivalent to each boundary component having only one vertex on it. Since each boundary component is a torus, $V - E + F = 0$. Now, $V = 1$, and since the cellulation is a triangulation, $3*F = 2*E$. \begin{align*} 1 - E + F &= 0 \\ 2 - 2*E + 2*F &= 0\\ 2 - 3*F + 2*F &= 0\\ 2 - F &= 0\\ 2 &= F, \end{align*} and there are only two triangles. We may fill any cusp we like by folding along one of the remaining three (non-foldable) edges. @ \begin{Rmk} The routine in [[SnapPea]] is more complicated because, rather than filling in a cusp any old way, [[SnapPea]] wants to make sure the filling compresses some given slope in the cusp. \end{Rmk} @ To implement this algorithm, let us take stock of the tools [[Regina]] provides. @ The algorithm is centered around edges, and instances of [[Regina]]'s class [[NEdge]] represent edges. [[NEdge]] has the following methods: \begin{itemize} \item [[isBoundary]] \item [[getVertex]] \item [[getTriangulation]] \item [[getEmbeddings]] \end{itemize} Except for the first, they all do more or less what you would expect. More specifically, if [[e]] instantiates [[NEdge]] and represents an edge $e$ in a triangulation $T$, then [[e.isBoundary()]] is [[True]] iff $e$ is a boundary edge; [[e.getVertex(0)]] instantiates [[NVertex]] and represents the source of $e$, whereas [[e.getVertex(1)]] represents its sink; and [[e.getTriangulation()]] is instantiates [[NTriangulation]] and represents $T$. @ In particular, here's a method to determine whether or not an edge is embedded. <>= def embedded(edge): src = edge.getVertex(0) snk = edge.getVertex(1) return src != snk @ [[getEmbeddings]] is more complicated, but also more important. [[e.getEmbeddings()]] is a list (in order) of instances of the class [[NEdgeEmbedding]]. Let [[phi]] be an element of [[e.getEmbeddings()]]. [[phi]] represents an embedding $\phi$ of $e$ into an incident tetrahedron $\Delta$. @ Some instance [[phi.getTetrahedron()]] of [[NTetrahedron]] represents $\Delta$. Each instance [[D]] of [[NTetrahedron]] comes with a method [[D.getVertex]], which represents an identification $j_\Delta:\mathbf{4} \to \Delta^0$ of $\mathbf{4} = \{0,1,2,3\}$ with the set $\Delta^0$ of vertices of $\Delta$. @ [[phi.getVertices()]], perhaps confusingly, is an instance of [[NPerm4]]. General instances of [[NPerm4]] represent elements of $S_{\mathbf{4}}$. [[phi.getVertices()]] actually represents an element $f \in A_{\mathbf{4}}$, the alternating group on $\mathbf{4}$, with the following property. Let $s,s'$ be the source and sink of $e$. Then $\phi(s) = j_\Delta(f(0))$ and $\phi(s') = j_\Delta(f(1))$. @ We note that there are two elements of $S_{\mathbf{4}}$ satisfying these equations, but only one in $A_{\mathbf{4}}$. We choose the one in $A_{\mathbf{4}}$. @ [[e.getEmbeddings()]] is ordered so that we can give a consistent orientation to the edges opposite $e$, in the following way: @ \begin{quote} Let [[l = e.getEmbeddings()]] have length $n$; for all $i$ with $0 \leq i < n$, let [[l[i]]] represent the embedding $\phi_i$; let [[l[i].getTetrahedron()]] represent $\Delta_i$; let $j_i = j_{\Delta_i}$; and let [[l[i].getVertices()]] represent $f_i$. Then for all $i$ with $1 \leq i < n - 1$, the vertices $j_{i-1}(f_{i-1}(3))$ and $j_{i}(f_{i}(2))$ are glued in the triangulation. \end{quote} @ Furthermore, if $e$ is a boundary edge, then each of [[l[0]]] and [[l[-1]]] (i.e. the last element of [[l]]) represents an embedding of $e$ into the boundary. In particular, if $j_0(f_0(2))$ becomes the vertex $v$ in the triangulation and $j_{-1}(f_{-1}(3))$ becomes $w$, then $v$ and $e$ determine a boundary face, and so do $w$ and $e$. @ We should finally explain another method [[NTetrahedron]] provides, namely [[joinTo]]. Suppose $\Delta,H$ are tetrahedra. How shall we join them up? Glue them along faces. How shall we name faces? Call faces by their opposite vertices. How shall we determine a gluing map? Well, let $p$ be a gluing map from the face $v_\ast$ opposite $v$ in $\Delta$ to the face $w_\ast$ opposite $w$ in $H$. Then $p$ restricted to the vertices of the faces has a unique extension to a bijection $P:\Delta^0 \to H^0$. Then we get a uniquely determined element $\sigma_p = j_H^{-1} \circ P \circ j_\Delta$ of $S_{\mathbf{4}}$. @ Conversely, for any such element $s \in S_{\mathbf{4}}$, there is a unique affine map $\pi_s: v_\ast \to w_\ast$ such that $\sigma_{\pi_s} = s$. @ So we may determine a gluing by \begin{itemize} \item which tetrahedra we're gluing, \item which faces are getting glued, and \item what is the associated element of $S_{\mathbf{4}}$. \end{itemize} @ In [[Regina]] it's more spartan than that. First of all, every instance of [[NTetrahedron]] has the child method [[joinTo]]. There's no need to include that instance as an argument to the procedure; [[joinTo]] implicitly regards its parent tetrahedron as $\Delta$ above. Also, given the permutation and the face on $\Delta$ to glue, the face on $H$ is determined, so that face need not be included as an argument to [[joinTo]]. @ In conclusion, \begin{verbatim} D.joinTo(i,E,s) \end{verbatim} is the [[Regina]] syntax for gluing tetrahedron [[D]] to a tetrahedron [[E]] by gluing the face in [[D]] opposite [[D.getVertex(i)]] to the face in [[E]] opposite [[E.getVertex(s(i))]] by the map determined by [[s]]. @ Therefore, we care primarily about tetrahedra and permutation representatives, and not so much about the edge embeddings $\phi_i$ themselves. In fact, since ultimately we're only concerned with boundary edges, all we care about are $f_0, f_{-1}, \Delta_0,$ and $\Delta_{-1}$. So let's write methods to return their representatives. <>= def lrmaps(edge): embs = edge.getEmbeddings() return (embs[0].getVertices(),\ embs[-1].getVertices()) def lrtets(edge): embs = edge.getEmbeddings() return (embs[0].getTetrahedron(),\ embs[-1].getTetrahedron()) @ We've already written a method for determining whether or not an edge is embedded. Let's write a method to determine whether or not it's coembedded. @ The definition we gave already was quick, but implementing it is altogether unnecessary, for with the terminology we have now, there is a better characterization of foldability. First of all, an edge had better be a boundary edge if it is going to be foldable. <>= def foldable(edge): if not edge.isBoundary(): return False @ As we've set it up, the tetrahedra and permutations are as follows: <>= (F0,F_1) = lrmaps(edge) (D0,D_1) = lrtets(edge) @ Now, an edge $e$ is coembedded iff it becomes embedded after a two-two move. This is equivalent to saying that the vertices opposite $e$ in the boundary faces are distinct. But these vertices are $j_0(f_0(2))$ and $j_{-1}(f_{-1}(3))$. So we can just test equality of these. <>= lvx = D0.getVertex(F0[2]) rvx = D_1.getVertex(F_1[3]) return lvx != rvx @ Having finished the foldability test, let us now implement the next part of the algorithm, viz. a two-two move on a boundary edge. This attaches a fresh tetrahedron $t$ to the triangulation of the edge. <>= def twoTwo(edge): M = edge.getTriangulation() T = M.newTetrahedron() @ Use the same notation as above. <>= (F0,F_1) = lrmaps(edge) (D0,D_1) = lrtets(edge) @ Now the faces adjacent to $e$ are opposite the vertices $j_0(f_0(3))$ and $j_{-1}(f_{-1}(2))$. We wish to glue to these faces the two faces of $t$ that include both $j_t(0)$ and $j_t(1)$. These are the faces opposite $j_t(2)$ and $j_t(3)$. @ To determine what the gluing maps should be, we may just follow the lead of [[getEdgeEmbedding]] and insist that $j_0(f_0(3))$ get glued to $j_t(2)$ and $j_{-1}(f_{-1}(3))$ to $j_t(3)$. (If such insistence isn't convincing, draw a picture.) @ The first gluing map also sends $j_t(0)$ to $f_0(0)$ and $j_t(1)$ to $f_0(1)$. So the associated permutation $s_0$ is plainly $f_0$ precomposed with the cycle $x = (2~3)$. The same is true for the other gluing map's permutation $s_{-1}$, except with $f_{-1}$ instead of $f_0$. <>= X = regina.NPerm(2,3) S0 = F0 * X S_1 = F_1 * X T.joinTo(2,D0,S0) T.joinTo(3,D_1,S_1) @ The last nontrivial operation we must implement is to fold along a boundary edge. We must first insist that the edge be a boundary edge. <>= def foldAlong(edge): assert edge.isBoundary() @ Use the same notation as before. <>= (D0,D_1) = lrtets(edge) (F0,F_1) = lrmaps(edge) @ The face $\ell$ of $\Delta_0$ to be glued is the face opposite $j_0(f_0(3))$, and the face $\ell'$ of $\Delta_{-1}$ to be glued is the face opposite $j_{-1}(f_{-1}(2))$. So to glue $\ell$ to $\ell'$, we need a permutation fixing $0$ and $1$, and taking $f_0(3)$ to $f_{-1}(2)$. This permutation is $f_{-1} \circ (2~3) \circ f_{0}^{-1}$. <>= X = regina.NPerm(2,3) glu = F_1 * X * F0.inverse() D0.joinTo(F0[3], D_1, glu) @ This concludes the difficult portion of the implementation. The rest is simple. @ The first and second [[while]] loops don't have implementations as such in [[Python]]. We can simulate them by implementing an operation that returns either the first boundary edge satisfying a predicate, or the [[Python]] primitive [[None]] if there is no such edge. <>= def firstBoundaryEdge(mfld,pred): cpts = mfld.getBoundaryComponents() for d in cpts: n = d.getNumberOfEdges() for i in range(0,n): e = d.getEdge(i) if pred(e): return e else: return None @ This uses the [[Python]] idiom of an [[else]] clause after a [[for]] loop. @ Now we are ready to implement the [[while]] loops. <>= def simplifyCusps(finite_mfld): M = finite_mfld fBE = firstBoundaryEdge f = fBE(M,embedded) while f != None: twoTwo(f) g = fBE(M,foldable) while g != None: foldAlong(g) g = fBE(M,foldable) f = fBE(M,embedded) @ To implement the $T^2\times I$ test, now we just need to implement a routine as described in \cite{Me}. We need to first make sure the manifold is irreducible and $\partial$-incompressible. There isn't at present an explicit method for irreducibility of bounded manifolds in Regina, so we roll our own very slow method. We define [[isFault]] below, which is allowed in [[Python]]. <>= def irreducible(regina_mfld): M = regina.NTriangulation(regina_mfld) M.finiteToIdeal() M.intelligentSimplify() nsl = regina.NNormalSurfaceList.enumerate l = nsl(M, regina.NS_STANDARD, \ regina.NS_FUNDAMENTAL) n = l.getNumberOfSurfaces() for i in range(0,n): s = l.getSurface(i) x = s.getEulerCharacteristic() if x != 2: continue if isFault(s): return False else: return True def isT2xI(regina_mfld): M = regina.NTriangulation(regina_mfld) M.finiteToIdeal() M.intelligentSimplify() @ There are some sanity checks we can run here before the irreducibility and $\partial$-incompressibility tests to save time---e.g. whether the manifold is connected, whether it has two torus boundary components, and so on. Put these sanity checks under the umbrella function [[possiblyT2xI]], and implement this later. <>= if not possiblyT2xI(M): return False @ Now we implement our test. We point out that since we end up simplifying the cusps and doing three fillings, we make a clone of $N$ of $M$, simplify $N$'s cusps, then clone $N$ three times before filling along the three slopes. We also note that [[simplifyCusps]] simplifies all boundary components' triangulations as specified in \cite{Me}. <>= if not irreducible(M): return False if M.hasCompressingDisc(): return False a = findNonSeparatingAnnulus(M) if a == None: return False mu = a.cutAlong() mu.intelligentSimplify() if not mu.isSolidTorus(): return False D = regina.NTriangulation(M) simplifyCusps(D) T = D.getBoundaryComponent(1) n = T.getNumberOfEdges() assert n == 3 for i in range(0,n): clone = regina.NTriangulation(D) cpt = clone.getBoundaryComponent(1) e = cpt.getEdge(i) foldAlong(e) if not clone.isSolidTorus(): return False else: return True @ Now we should get around to implementing the remaining sanity checks. <>= def possiblyT2xI(mfld): m = mfld dno = m.getNumberOfBoundaryComponents() if not (possiblyHyp(m) and dno == 2): return False cpts = m.getBoundaryComponents() for d in cpts: x = d.getEulerCharacteristic() if x != 0: return False H1 = m.getHomologyH1() if not H1.toString() == '2 Z': return False H2 = m.getHomologyH2() if not H2.isZ(): return False H1R = m.getHomologyH1Rel() if not H1R.isZ(): return False return True <>= def hasTorusBoundary(mfld): m = mfld if m.isClosed(): return False for v in m.getVertices(): if not v.getLink() == regina.NVertex.TORUS: return False return True @ Finally, the code at present has the following flaw: that it verifies hyperbolicity by enumerating all fundamental normal surfaces of a certain flavor, and checking that they are not faults. This takes a long time, and there is now a better routine which may verify hyperbolicity. This method is part of [[Regina]]; it detects whether a triangulation admits a strict angle structure, existence of which is a sufficient condition for hyperbolicity. <>= def isHyp(regina_manifold): m = regina.NTriangulation(regina_manifold) m.intelligentSimplify() if m.hasStrictAngleStructure(): return True else: return not unhypByNormalSurfaces(m) <>= import regina <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> @ \begin{thebibliography}{9} \bibitem{Bonahon} F. Bonahon, \emph{Geometric structures on 3-manifolds}, Chapter 3 from \emph{Handbook of Geometric Topology}, ed. R. J. Daverman and R. B. Sher, Elsevier, New York, 2002. \bibitem{Regina} B. A. Burton, R. Budney, W. Pettersson, et al., \emph{[[Regina]]: software for 3-manifold topology and normal surface theory}, http://regina.sourceforge.net/ , 1999--2014. \bibitem{Me} Robert C. Haraway, III, \emph{Determining hyperbolicity of multiply-cusped 3-manifolds}, preprint available at author's website. \bibitem{JT95} W. Jaco and J. L. Tollefson, \emph{Algorithms for the complete decomposition of a closed 3-manifold}, Illinois J. of Math., \textbf{39}:3, Fall 1995, pp. 358--406. \bibitem{Kapovich} M. Kapovich, \emph{Hyperbolic manifolds and discrete groups}, Birkh\"auser, Boston, 2001. \bibitem{Marden} A. Marden, \emph{Outer circles: an introduction to hyperbolic 3-manifolds}, Cambridge University Press, New York, 2007. \bibitem{Matveev} S. Matveev, \emph{Algorithmic topology and classification of 3-manifolds}, Springer-Verlag, New York, 2003. \bibitem{T82} W. P. Thurston, \emph{Three-dimensional manifolds, Kleinian groups and hyperbolic geometry}, Bull. Amer. Math. Soc. (N.S.) \textbf{6}:3 (1982), 357--381. \bibitem{Tol98} J. L. Tollefson, \emph{Normal surface Q-theory}, Pacific J. of Math., \textbf{183}:2 (1998), 359--374. \end{thebibliography} \end{document}