Step-by-step examples: Part 2

The Ptolemy list type

Recall that ptolemy_variety with obstruction_class='all' returns a list of varieties, one for each obstruction class:

>>> M = Manifold("m003")
>>> M.ptolemy_variety(2, obstruction_class = 'all')
[Ptolemy Variety for m003, N = 2, obstruction_class = 0
    c_0011_0 * c_0101_0 + c_0011_0^2 - c_0101_0^2
    c_0011_0 * c_0101_0 + c_0011_0^2 - c_0101_0^2
    - 1 + c_0011_0,
 Ptolemy Variety for m003, N = 2, obstruction_class = 1
    - c_0011_0 * c_0101_0 - c_0011_0^2 - c_0101_0^2
    - c_0011_0 * c_0101_0 - c_0011_0^2 - c_0101_0^2
    - 1 + c_0011_0]

Also recall that retrieve_solutions was a method of a PtolemyVariety. Assume we want to call retrieve_solutions for each Ptolemy variety. As in the previous example, we could write a loop such as:

>>> [p.retrieve_solutions(verbose=False) for p in M.ptolemy_variety(2, 'all')]

The ptolemy module allows to do this in a much shorter way:

>>> M.ptolemy_variety(2, 'all').retrieve_solutions(verbose=False)
[[PtolemyCoordinates(
      {'c_0011_0': 1,
       'c_0011_1': -1,
       'c_0101_0': Mod(x, x^2 - x - 1),
       ...,
       's_3_1': 1},
      is_numerical = False, ...)],
 [PtolemyCoordinates(
      {'c_0011_0': 1,
       'c_0011_1': -1,
       'c_0101_0': Mod(x, x^2 + x + 1),
       ...,
       's_3_1': 1},
      is_numerical = False, ...)]]

This behavior is specific to the ptolemy module. It works with many methods of the ptolemy module that can potentially return more than one object. These methods return a special kind of list (usually MethodMappingList, a subclass of python list) that tries to call the method of the given name (here retrieve_solutions) with the given arguments (here verbose=False) on each element in the list (here the two Ptolemy varieties).

Since retrieve_solutions itself actually returns a list, the result is a list of lists of solutions which are of type PtolemyCoordinates. The first level groups the solutions by obstruction class. The inner lists contain the different (non-Galois conjugate) solutions for each obstruction class (here, for m003, each inner lists contains only one element).

Using the Ptolemy list type recursively

The list type described in the previous example works recursively. Recall that an algebraic solution to a Ptolemy variety (of type PtolemyCoordinates) has a method volume_numerical that returns a list of volumes:

>>> M=Manifold("m003")
>>> p=M.ptolemy_variety(2,1)
>>> sol=p.retrieve_solutions(verbose=False)[0]
>>> sol.volume_numerical()
[0.E-19, 1.88267370443418 E-14]

We can chain these commands together to retrieve the volumes of all boundary-unipotent PSL(2, C) (that are generically decorated with respect to the triangulation) in just one line:

>>> Manifold("m003").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical()
[[[0.E-19, 1.88267370443418 E-14]], [[2.02988321281931, -2.02988321281931]]]

Note that the volumes of the representations are in a list of lists of lists. At the first level the volumes are grouped by obstruction class, then by Galois conjugacy.

Remark: There might be an extra level for witness points.

Remark: Unfortunately, this is not compatible with tab-autocompletion, see later.

A comparison of m003 and m004

We can now compare the set of volumes of m003 and m004:

>>> Manifold("m003").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical()
[[[0.E-19, 1.88267370443418 E-14]], [[2.02988321281931, -2.02988321281931]]]
>>> Manifold("m004").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical()
[[], [[-2.02988321281931, 2.02988321281931]]]

We see that the two manifolds are distinguished by their volumes of boundary-unipotent representations: m004 has no representation with trivial volume (this is not a proof as in theory, there could be such a representation which is not generically decorated with respect to the given triangulation) and no representation that can be lifted to a boundary-unipotent SL(2, C)-representation.

A non-hyperbolic example

We can also compute the volumes for a manifold that might be non-hyperbolic, here the complement of the 51 knot:

>>> Manifold("5_1").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical()
[[], [[1.52310839130992 E-14, 0.E-37]]]

Note that one of the Ptolemy varieties is non-empty which proves that all edges of the triangulation are essential. We also see that all volumes are 0 and thus smaller than the volume 2.029883… of the figure-eight knot complement that is proven to be the smallest volume of any orientable cusped manifold. Thus, it follows from Theorem 1.3 and Remark 1.4 of [GGZ2014] that 51 is not hyperbolic.

Remark: The ptolemy module does not (yet) support interval arithmetics, otherwise, this would be a proof that 51 is not hyperbolic.

Flattening nested structures

If we want to loose some of the grouping, we can call flatten on the results. Here the grouping by obstruction class is lost:

>>> Manifold("m003").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical().flatten()
[[0.E-19, 1.88267370443418 E-14], [2.02988321281931, -2.02988321281931]]

And now, the grouping by Galois conjugacy is lost as well, resulting in a flat list:

>>> Manifold("m003").ptolemy_variety(2,'all').retrieve_solutions(verbose=False).volume_numerical().flatten(2)
[0.E-19, 1.88267370443418 E-14, 2.02988321281931, -2.02988321281931]

So the result is just a flat list.

Remark: We cannot overflatten. If we give an even larger argument to flatten, the result will just stay a flat list.

Lack of tab-autocompletion for nested structures

Unfortunately, the autocompletion does not list all the desired results when we have a nested structure. For example:

>>> sols = Manifold("m003").ptolemy_variety(2,'all').retrieve_solutions(verbose=False)
>>> sols.

When we now hit the tab key:

>>> sols.
sols.append   sols.extend   sols.index    sols.pop      sols.reverse
sols.count    sols.flatten  sols.insert   sols.remove   sols.sort

… we only get list methods, but not the desired volume_numerical. One way to discover the available methods is to pick a leaf of the nested structure and hit the tab key:

>>> sol = sols.flatten(100)[0]
>>> sol.
sol.N                                   sol.keys
sol.check_against_manifold              sol.long_edge
...
sol.itervalues                          sol.volume_numerical

The overview diagram might also be helpful.

Converting exact solutions into numerical solutions

We can turn exact solutions into numerical solutions by calling numerical:

>>> sol = Manifold("m003").ptolemy_variety(2, 1).retrieve_solutions()[0]
>>> sol
PtolemyCoordinates(
    {'c_0011_0': 1,
     'c_0011_1': -1,
     'c_0101_0': Mod(x, x^2 + x + 1),
     ...
     's_3_1': 1},
    is_numerical = False, ...)
>>> sol.numerical()
[PtolemyCoordinates(
     {'c_0011_0': 1,
      'c_0011_1': -1,
      'c_0101_0': -0.500000000000000 - 0.866025403784439*I,
      ...,
      's_3_1': 1},
     is_numerical = True, ...),
 PtolemyCoordinates(
     {'c_0011_0': 1,
      'c_0011_1': -1,
      'c_0101_0': -0.500000000000000 + 0.866025403784439*I,
      ...,
      's_3_1': 1},
     is_numerical = True, ...)]

Note that the one exact (algebraic) solution turns into a list of numerical solutions which are Galois conjugates.

Remark: This uses the current pari precision. See the above example, in particular, the comment about interval arithmetics.

Remark: Calling numerical() on a numerical solution does nothing.

Remark: CrossRatios also support numerical.

Working with exact vs numerical solutions

Most methods such as evaluate_word or cross_ratios work just the same way on an exact solution:

>>> exact_sol = Manifold("m004").ptolemy_variety(2, 1).retrieve_solutions()[0]
>>> exact_sol
PtolemyCoordinates(
    {'c_0011_0': 1,
     'c_0011_1': -1,
     'c_0101_0': 1,
     'c_0101_1': Mod(x, x^2 + x + 1),
     ...,
     's_3_1': -1},
    is_numerical = False, ...)
>>> exact_sol.evaluate_word('a')
[[Mod(-2*x, x^2 + x + 1), Mod(-x - 1, x^2 + x + 1)],
 [Mod(x, x^2 + x + 1), Mod(x + 1, x^2 + x + 1)]]

… as they do on a numerical solution:

>>> numerical_sol = sol.numerical()[0]
>>> numerical_sol
PtolemyCoordinates(
    {'c_0011_0': 1,
     'c_0011_1': -1,
     'c_0101_0': 1,
     'c_0101_1': -0.500000000000000 - 0.866025403784439*I,
     ...,
     's_3_1': -1},
    is_numerical = False, ...)
>>> numerical_sol.evaluate_word('a')
[[1.00000000000000 + 1.73205080756888*I,
  -0.500000000000000 + 0.866025403784439*I],
 [-0.500000000000000 - 0.866025403784439*I,
  0.500000000000000 - 0.866025403784439*I]]

Methods with postfix _numerical are special: when applied to an exact solution, they implicitly convert it to a list of Galois conjugate numerical solutions first. volume_numerical is an example (because volume is a transcendental function):

>>> exact_sol.volume_numerical()
[-2.02988321281931, 2.02988321281931]
>>> numerical_sol.volume_numerical()
-2.02988321281931

Computing numerical solutions directly

We can also directly compute numerical solutions:

>>> M = Manifold("m004")
>>> sols = M.ptolemy_variety(2,'all').retrieve_solutions(numerical = True)
[[],
 [[PtolemyCoordinates(
       {'c_0011_0': 1.00000000000000 + 0.E-19*I,
        'c_0011_1': -1.00000000000000 + 0.E-19*I,
        'c_0101_0': 1.00000000000000 + 0.E-19*I,
        'c_0101_1': -0.500000000000000 - 0.866025403784439*I,
        ...,
        's_3_1': -1},
       is_numerical = True, ...),
   PtolemyCoordinates(
       {'c_0011_0': 1.00000000000000 + 0.E-19*I,
        'c_0011_1': -1.00000000000000 + 0.E-19*I,
        'c_0101_0': 1.00000000000000 + 0.E-19*I,
        'c_0101_1': -0.500000000000000 + 0.866025403784439*I,
        ...,
        's_3_1': -1},
       is_numerical = True, ...)]]]

The structure is as described earlier, a list of lists of lists: first solutions are grouped by obstruction class, then by Galois conjugacy.

The advantage over going through the exact solutions is that it might be much faster (because it can avoid computing the number field from the lexicographic Groebner basis, see later). For example, many PSL(3, C) examples only work when using numerical = True.

Computing cross ratios from Ptolemy coordinates

Given exact or numerical solutions to the Ptolemy variety, we can also compute the cross ratios/shape parameters:

>>> sols = Manifold("m004").ptolemy_variety(2,'all').retrieve_solutions(verbose=False)
>>> zs=sols.cross_ratios()
>>> zs
[[],
 [CrossRatios({'z_0000_0': Mod(x + 1, x^2 + x + 1),
               'z_0000_1': Mod(x + 1, x^2 + x + 1),
               'zp_0000_0': Mod(x + 1, x^2 + x + 1),
               'zp_0000_1': Mod(x + 1, x^2 + x + 1),
               'zpp_0000_0': Mod(x + 1, x^2 + x + 1),
               'zpp_0000_1': Mod(x + 1, x^2 + x + 1)},
              is_numerical = False, ...)]]

Remark: The shapes will be given as element in the Ptolemy field with defining polynomial being the second argument to Mod(..., ...), here, x2+x+1. The Ptolemy field is a (possibly trivial) extension of the shape field. For N =2, the Ptolemy field is the trace field [GGZ2014] and an iterated square extension of the shape field which is the invariant trace field for a cusped manifold.

And numerically, so that we can compare to SnapPy’s shapes:

>>> zs.numerical()
[[],
 [[CrossRatios(
       {'z_0000_0': 0.500000000000000 - 0.866025403784439*I,
        'z_0000_1': 0.500000000000000 - 0.866025403784439*I,
        'zp_0000_0': 0.500000000000000 - 0.866025403784439*I,
        'zp_0000_1': 0.500000000000000 - 0.866025403784439*I,
        'zpp_0000_0': 0.500000000000000 - 0.866025403784439*I,
        'zpp_0000_1': 0.500000000000000 - 0.866025403784439*I},
       is_numerical = True, ...),
   CrossRatios(
       {'z_0000_0': 0.500000000000000 + 0.866025403784439*I,
        'z_0000_1': 0.500000000000000 + 0.866025403784439*I,
        'zp_0000_0': 0.500000000000000 + 0.866025403784439*I,
        'zp_0000_1': 0.500000000000000 + 0.866025403784439*I,
        'zpp_0000_0': 0.500000000000000 + 0.866025403784439*I,
        'zpp_0000_1': 0.500000000000000 + 0.866025403784439*I},
       is_numerical = True, ...)]]]
>>> Manifold("m004").tetrahedra_shapes('rect')
[0.5000000000 + 0.8660254038*I, 0.5000000000 + 0.8660254038*I]

The result is of type CrossRatios and assigns z as well as z’=1/(1-z) and z’’=1-1/z a value.

The dimension of a component

A Ptolemy variety might have positively dimensional components (note that this might or might not be a positively dimensional family of representations, see here). For example, the Ptolemy variety for m371 and the trivial obstruction class has a 1-dimensional component. This is indicated by:

>>> M.ptolemy_variety(2).retrieve_solutions()
[NonZeroDimensionalComponent(dimension = 1)]

Or:

>>> M=Manifold("m371")
>>> M.ptolemy_variety(2).retrieve_solutions()
[[ PtolemyCoordinates(
       {'c_0011_0': 1,
        'c_0011_1': -1,
        'c_0011_2': -1,
        'c_0011_3': Mod(-x - 1, x^2 + x + 2),
        ...,
        's_3_4': 1},
       is_numerical = False, ...)
   (witnesses for NonZeroDimensionalComponent(dimension = 1, free_variables = ['c_0110_2'])) ]]

The latter actually also provides a sample point (witness which we will use later to determine whether this corresponds to a 1-dimensional family of representations or not) on the 1-dimensional component. A NonZeroDimensionalComponent as well as PtolemyCoordinates (that correspond to 0-dimensional components of the Ptolemy variety)) has a dimension attribute, so we can do:

>>> M=Manifold("m371")
>>> sols = M.ptolemy_variety(2,'all').retrieve_solutions()
>>> sols.dimension
[[1], [], [0], []]

This means that the Ptolemy variety for the trivial obstruction class has a 1-dimensional component and that the Ptolemy variety of one of the other obstruction classes a 0-dimensional component.

A NonZeroDimensionalComponent is actually again a list whose elements will be witness points if witnesses have been computed for this Ptolemy variety.

Warning: This implies that if we flatten too much, the reported dimension becomes 0 which is the dimension of the witness point instead of 1:

>>> sols.flatten()
[1, 0]

Too much flatten:

>>> sols.flatten()
[0, 0]

The advantage is that we can still call methods such as volume_numerical and actually see the volume of a witness point (it is known that the volume stays constant on a component of boundary-unipotent representations, so one witness point can tell us the volume of all representation in that component):

>>> sols.volume_numerical()
[[[ [0.E-38, 0.E-38] (witnesses for NonZeroDimensionalComponent(dimension = 1, free_variables = ['c_0110_2'])) ]],
 [],
 [[4.75170196551790,
   -4.75170196551790,
   4.75170196551790,
   -4.75170196551790,
   1.17563301006556,
   -1.17563301006556,
   1.17563301006556,
   -1.17563301006556]],
 []]