Modular symbols by numerical integration

We describe here the method for computing modular symbols by numerical approximations of the integral of the modular form on a path between cusps.

More precisely, let \(E\) be an elliptic curve and \(f\) the newform associated to the isogeny class of \(E\). If

\[\lambda(r\to r') = 2\pi i \int_{r}^{r'} f(\tau) d\tau\]

then the modular symbol \([r]^{+}\) is defined as the quotient of the real part of \(\lambda(\infty\to r)\) by the least positive real period of \(E\). Similarly for the negative modular symbol, it is the quotient of the imaginary part of the above by the smallest positive imaginary part of a period on the imaginary axis.

The theorem of Manin-Drinfeld shows that the modular symbols are rational numbers with small denominator. They are used for the computation of special values of the \(L\)-function of \(E\) twisted by Dirichlet characters and for the computation of \(p\)-adic \(L\)-functions.

ALGORITHM:

The implementation of modular symbols in eclib and directly in sage uses the algorithm described in Cremona’s book [Cre1997] and Stein’s book [St2007]. First the space of all modular symbols of the given level is computed, then the space corresponding to the given newform is determined. Even if these initial steps may take a while, the evaluation afterwards is instantaneous. All computations are done with rational numbers and hence are exact.

Instead the method used here (see [Wu2018] for details) is by evaluating the above integrals \(\lambda(r\to r')\) by numerical approximation. Since we know precise bounds on the denominator, we can make rigorous estimates on the error to guarantee that the result is proven to be the correct rational number.

The paths over which we integrate are split up and Atkin-Lehner operators are used to compute the badly converging part of the integrals by using the Fourier expansion at other cusps than \(\infty\).

Note

There is one assumption for the correctness of these computations: The Manin constant for the \(X_0\)-optimal curve should be \(1\) if the curve lies outside the Cremona tables. This is known for all curves in the Cremona table, but only conjectured for general curves.

EXAMPLES:

The most likely usage for the code is through the functions modular_symbol with implementation set to “num” and through modular_symbol_numerical:

sage: E = EllipticCurve("5077a1")
sage: M = E.modular_symbol(implementation='num')
sage: M(0)
0
sage: M(1/123)
4
sage: Mn = E.modular_symbol_numerical(sign=-1, prec=30)
sage: Mn(3/123)       # abs tol 1e-11
3.00000000000018
>>> from sage.all import *
>>> E = EllipticCurve("5077a1")
>>> M = E.modular_symbol(implementation='num')
>>> M(Integer(0))
0
>>> M(Integer(1)/Integer(123))
4
>>> Mn = E.modular_symbol_numerical(sign=-Integer(1), prec=Integer(30))
>>> Mn(Integer(3)/Integer(123))       # abs tol 1e-11
3.00000000000018

In more details. A numerical modular symbols M is created from an elliptic curve with a chosen sign (though the other sign will also be accessible, too):

sage: E = EllipticCurve([101,103])
sage: E.conductor()
35261176
sage: M = E.modular_symbol(implementation='num', sign=-1)
sage: M
Numerical modular symbol attached to
 Elliptic Curve defined by y^2 = x^3 + 101*x + 103 over Rational Field
>>> from sage.all import *
>>> E = EllipticCurve([Integer(101),Integer(103)])
>>> E.conductor()
35261176
>>> M = E.modular_symbol(implementation='num', sign=-Integer(1))
>>> M
Numerical modular symbol attached to
 Elliptic Curve defined by y^2 = x^3 + 101*x + 103 over Rational Field

We can then compute the value \([13/17]^{-}\) and \([1/17]^{+}\) by calling the function M. The value of \([0]^{+}=0\) tells us that the rank of this curve is positive:

sage: M(13/17)
-1/2
sage: M(1/17,sign=+1)
-3
sage: M(0, sign=+1)
0
>>> from sage.all import *
>>> M(Integer(13)/Integer(17))
-1/2
>>> M(Integer(1)/Integer(17),sign=+Integer(1))
-3
>>> M(Integer(0), sign=+Integer(1))
0

One can compute the numerical approximation to these rational numbers to any proven binary precision:

sage: M.approximative_value(13/17, prec=2) # abs tol 1e-4
-0.500003172770455
sage: M.approximative_value(13/17, prec=4) # abs tol 1e-6
-0.500000296037388
sage: M.approximative_value(0, sign=+1, prec=6) # abs tol 1e-8
0.000000000000000
>>> from sage.all import *
>>> M.approximative_value(Integer(13)/Integer(17), prec=Integer(2)) # abs tol 1e-4
-0.500003172770455
>>> M.approximative_value(Integer(13)/Integer(17), prec=Integer(4)) # abs tol 1e-6
-0.500000296037388
>>> M.approximative_value(Integer(0), sign=+Integer(1), prec=Integer(6)) # abs tol 1e-8
0.000000000000000

There are a few other things that one can do with M. The Manin symbol \(M(c:d)\) for a point \((c:d)\) in the projective line can be computed.:

sage: M.manin_symbol(1,5)
-1
>>> from sage.all import *
>>> M.manin_symbol(Integer(1),Integer(5))
-1

In some cases useful, there is a function that returns all \([a/m]^{+}\) for a fixed denominator \(m\). This is rather quicker for small \(m\) than computing them individually:

sage: M.all_values_for_one_denominator(7)
{1/7: 0, 2/7: 3/2, 3/7: 3/2, 4/7: -3/2, 5/7: -3/2, 6/7: 0}
>>> from sage.all import *
>>> M.all_values_for_one_denominator(Integer(7))
{1/7: 0, 2/7: 3/2, 3/7: 3/2, 4/7: -3/2, 5/7: -3/2, 6/7: 0}

Finally a word of warning. The algorithm is fast when the cusps involved are unitary. If the curve is semistable, all cusps are unitary. But rational numbers with a prime \(p\) dividing the denominator once, but the conductor more than once, are very difficult. For instance for the above example, a seemingly harmless command like M(1/2) would take a very very long time to return a value. However it is possible to compute them for smaller conductors:

sage: E = EllipticCurve("664a1")
sage: M = E.modular_symbol(implementation='num')
sage: M(1/2)
0
>>> from sage.all import *
>>> E = EllipticCurve("664a1")
>>> M = E.modular_symbol(implementation='num')
>>> M(Integer(1)/Integer(2))
0

The problem with non-unitary cusps is dealt with rather easily when one can twist to a semistable curve, like in this example:

sage: C = EllipticCurve("11a1")
sage: E = C.quadratic_twist(101)
sage: M = E.modular_symbol(implementation='num')
sage: M(1/101)
41
>>> from sage.all import *
>>> C = EllipticCurve("11a1")
>>> E = C.quadratic_twist(Integer(101))
>>> M = E.modular_symbol(implementation='num')
>>> M(Integer(1)/Integer(101))
41

AUTHORS:

  • Chris Wuthrich (2013-16)

class sage.schemes.elliptic_curves.mod_sym_num.ModularSymbolNumerical[source]

Bases: object

This class assigning to an elliptic curve over \(\QQ\) a modular symbol. Unlike the other implementations this class does not precompute a basis for this space. Instead at each call, it evaluates the integral using numerical approximation. All bounds are very strictly implemented and the output is a correct proven rational number.

INPUT:

  • E – an elliptic curve over the rational numbers

  • sign – either -1 or +1 (default). This sets the default value of sign throughout the class. Both are still accessible.

OUTPUT: a modular symbol

EXAMPLES:

sage: E = EllipticCurve("5077a1")
sage: M = E.modular_symbol(implementation='num')
sage: M(0)
0
sage: M(77/57)
-1
sage: M(33/37, -1)
2
sage: M = E.modular_symbol(sign=-1, implementation='num')
sage: M(2/7)
2

sage: from sage.schemes.elliptic_curves.mod_sym_num \
....: import ModularSymbolNumerical
sage: M = ModularSymbolNumerical(EllipticCurve("11a1"))
sage: M(1/3, -1)
1/2
sage: M(1/2)
-4/5
>>> from sage.all import *
>>> E = EllipticCurve("5077a1")
>>> M = E.modular_symbol(implementation='num')
>>> M(Integer(0))
0
>>> M(Integer(77)/Integer(57))
-1
>>> M(Integer(33)/Integer(37), -Integer(1))
2
>>> M = E.modular_symbol(sign=-Integer(1), implementation='num')
>>> M(Integer(2)/Integer(7))
2

>>> from sage.schemes.elliptic_curves.mod_sym_num import ModularSymbolNumerical
>>> M = ModularSymbolNumerical(EllipticCurve("11a1"))
>>> M(Integer(1)/Integer(3), -Integer(1))
1/2
>>> M(Integer(1)/Integer(2))
-4/5
all_values_for_one_denominator(m, sign=0)[source]

Given an integer m and a sign, this returns the modular symbols \([a/m]\) for all \(a\) coprime to \(m\) using partial sums. This is much quicker than computing them one by one.

This will only work if \(m\) is relatively small and if the cusps \(a/m\) are unitary.

INPUT:

  • m – a natural number

  • sign – either +1 or -1, or 0 (default), in which case the sign passed to the class is taken

OUTPUT: a dictionary of fractions with denominator \(m\) giving rational numbers.

EXAMPLES:

sage: E = EllipticCurve('5077a1')
sage: M = E.modular_symbol(implementation='num')
sage: M.all_values_for_one_denominator(7)
{1/7: 3, 2/7: 0, 3/7: -3, 4/7: -3, 5/7: 0, 6/7: 3}
sage: [M(a/7) for a in [1..6]]
[3, 0, -3, -3, 0, 3]
sage: M.all_values_for_one_denominator(3,-1)
{1/3: 4, 2/3: -4}

sage: E = EllipticCurve('11a1')
sage: M = E.modular_symbol(implementation='num')
sage: M.all_values_for_one_denominator(12)
{1/12: 1/5, 5/12: -23/10, 7/12: -23/10, 11/12: 1/5}
sage: M.all_values_for_one_denominator(12, -1)
{1/12: 0, 5/12: 1/2, 7/12: -1/2, 11/12: 0}

sage: E = EllipticCurve('20a1')
sage: M = E.modular_symbol(implementation='num')
sage: M.all_values_for_one_denominator(4)
{1/4: 0, 3/4: 0}
sage: M.all_values_for_one_denominator(8)
{1/8: 1/2, 3/8: -1/2, 5/8: -1/2, 7/8: 1/2}
>>> from sage.all import *
>>> E = EllipticCurve('5077a1')
>>> M = E.modular_symbol(implementation='num')
>>> M.all_values_for_one_denominator(Integer(7))
{1/7: 3, 2/7: 0, 3/7: -3, 4/7: -3, 5/7: 0, 6/7: 3}
>>> [M(a/Integer(7)) for a in (ellipsis_range(Integer(1),Ellipsis,Integer(6)))]
[3, 0, -3, -3, 0, 3]
>>> M.all_values_for_one_denominator(Integer(3),-Integer(1))
{1/3: 4, 2/3: -4}

>>> E = EllipticCurve('11a1')
>>> M = E.modular_symbol(implementation='num')
>>> M.all_values_for_one_denominator(Integer(12))
{1/12: 1/5, 5/12: -23/10, 7/12: -23/10, 11/12: 1/5}
>>> M.all_values_for_one_denominator(Integer(12), -Integer(1))
{1/12: 0, 5/12: 1/2, 7/12: -1/2, 11/12: 0}

>>> E = EllipticCurve('20a1')
>>> M = E.modular_symbol(implementation='num')
>>> M.all_values_for_one_denominator(Integer(4))
{1/4: 0, 3/4: 0}
>>> M.all_values_for_one_denominator(Integer(8))
{1/8: 1/2, 3/8: -1/2, 5/8: -1/2, 7/8: 1/2}
approximative_value(r, sign=0, prec=20, use_twist=True)[source]

The numerical modular symbol evaluated at rational.

It returns a real number, which should be equal to a rational number to the given binary precision prec. In practice the precision is often much higher. See the examples below.

INPUT:

  • r – a rational (or integer)

  • sign – either +1 or -1, or 0 (default), in which case the sign passed to the class is taken

  • prec – integer (default: 20)

  • use_twistTrue (default) allows to use a quadratic twist of the curve to lower the conductor

OUTPUT: a real number

EXAMPLES:

sage: E = EllipticCurve("5077a1")
sage: M = E.modular_symbol(implementation='num')
sage: M.approximative_value(123/567)  # abs tol 1e-11
-4.00000000000845
sage: M.approximative_value(123/567,prec=2) # abs tol 1e-9
-4.00002815242902

sage: E = EllipticCurve([11,88])
sage: E.conductor()
1715296
sage: M = E.modular_symbol(implementation='num')
sage: M.approximative_value(0,prec=2)   # abs tol 1e-11
-0.0000176374317982166
sage: M.approximative_value(1/7,prec=2)  # abs tol 1e-11
0.999981178147778
sage: M.approximative_value(1/7,prec=10) # abs tol 1e-11
0.999999972802649
>>> from sage.all import *
>>> E = EllipticCurve("5077a1")
>>> M = E.modular_symbol(implementation='num')
>>> M.approximative_value(Integer(123)/Integer(567))  # abs tol 1e-11
-4.00000000000845
>>> M.approximative_value(Integer(123)/Integer(567),prec=Integer(2)) # abs tol 1e-9
-4.00002815242902

>>> E = EllipticCurve([Integer(11),Integer(88)])
>>> E.conductor()
1715296
>>> M = E.modular_symbol(implementation='num')
>>> M.approximative_value(Integer(0),prec=Integer(2))   # abs tol 1e-11
-0.0000176374317982166
>>> M.approximative_value(Integer(1)/Integer(7),prec=Integer(2))  # abs tol 1e-11
0.999981178147778
>>> M.approximative_value(Integer(1)/Integer(7),prec=Integer(10)) # abs tol 1e-11
0.999999972802649
clear_cache()[source]

Clear the cached values in all methods of this class.

EXAMPLES:

sage: E = EllipticCurve("11a1")
sage: M = E.modular_symbol(implementation='num')
sage: M(0)
1/5
sage: M.clear_cache()
sage: M(0)
1/5
>>> from sage.all import *
>>> E = EllipticCurve("11a1")
>>> M = E.modular_symbol(implementation='num')
>>> M(Integer(0))
1/5
>>> M.clear_cache()
>>> M(Integer(0))
1/5
elliptic_curve()[source]

Return the elliptic curve of this modular symbol.

EXAMPLES:

sage: E = EllipticCurve("15a4")
sage: M = E.modular_symbol(implementation='num')
sage: M.elliptic_curve()
Elliptic Curve defined by y^2 + x*y + y = x^3 + x^2 + 35*x - 28 over Rational Field
>>> from sage.all import *
>>> E = EllipticCurve("15a4")
>>> M = E.modular_symbol(implementation='num')
>>> M.elliptic_curve()
Elliptic Curve defined by y^2 + x*y + y = x^3 + x^2 + 35*x - 28 over Rational Field
manin_symbol(u, v, sign=0)[source]

Given a pair \((u,v)\) presenting a point in \(\mathbb{P}^1(\mathbb{Z}/N\mathbb{Z})\) and hence a coset of \(\Gamma_0(N)\), this computes the value of the Manin symbol \(M(u:v)\).

INPUT:

  • u – integer

  • v – integer such that \((u:v)\) is a projective point modulo \(N\)

  • sign – either +1 or -1, or 0 (default), in which case the sign passed to the class is taken

EXAMPLES:

sage: E = EllipticCurve('11a1')
sage: M = E.modular_symbol(implementation='num')
sage: M.manin_symbol(1,3)
-1/2
sage: M.manin_symbol(1,3, sign=-1)
-1/2
sage: M.manin_symbol(1,5)
1
sage: M.manin_symbol(1,5)
1

sage: E = EllipticCurve('14a1')
sage: M = E.modular_symbol(implementation='num')
sage: M.manin_symbol(1,2)
-1/2
sage: M.manin_symbol(17,6)
-1/2
sage: M.manin_symbol(-1,12)
-1/2
>>> from sage.all import *
>>> E = EllipticCurve('11a1')
>>> M = E.modular_symbol(implementation='num')
>>> M.manin_symbol(Integer(1),Integer(3))
-1/2
>>> M.manin_symbol(Integer(1),Integer(3), sign=-Integer(1))
-1/2
>>> M.manin_symbol(Integer(1),Integer(5))
1
>>> M.manin_symbol(Integer(1),Integer(5))
1

>>> E = EllipticCurve('14a1')
>>> M = E.modular_symbol(implementation='num')
>>> M.manin_symbol(Integer(1),Integer(2))
-1/2
>>> M.manin_symbol(Integer(17),Integer(6))
-1/2
>>> M.manin_symbol(-Integer(1),Integer(12))
-1/2
transportable_symbol(r, rr, sign=0)[source]

Return the symbol \([r']^+ - [r]^+\) where \(r'=\gamma(r)\) for some \(\gamma\in\Gamma_0(N)\). These symbols can be computed by transporting the path into the upper half plane close to one of the unitary cusps. Here we have implemented it only to move close to \(i\infty\) and \(0\).

INPUT:

  • r, rr – two rational numbers

  • sign – either +1 or -1, or 0 (default), in which case the sign passed to the class is taken

OUTPUT: a rational number

EXAMPLES:

sage: E = EllipticCurve("11a1")
sage: M = E.modular_symbol(implementation='num')
sage: M.transportable_symbol(0/1,-2/7)
-1/2

sage: E = EllipticCurve("37a1")
sage: M = E.modular_symbol(implementation='num')
sage: M.transportable_symbol(0/1,-1/19)
0
sage: M.transportable_symbol(0/1,-1/19,-1)
0

sage: E = EllipticCurve("5077a1")
sage: M = E.modular_symbol(implementation='num')
sage: M.transportable_symbol(0/1,-35/144)
-3
sage: M.transportable_symbol(0/1,-35/144,-1)
0
sage: M.transportable_symbol(0/1, -7/31798)
0
sage: M.transportable_symbol(0/1, -7/31798, -1)
-5
>>> from sage.all import *
>>> E = EllipticCurve("11a1")
>>> M = E.modular_symbol(implementation='num')
>>> M.transportable_symbol(Integer(0)/Integer(1),-Integer(2)/Integer(7))
-1/2

>>> E = EllipticCurve("37a1")
>>> M = E.modular_symbol(implementation='num')
>>> M.transportable_symbol(Integer(0)/Integer(1),-Integer(1)/Integer(19))
0
>>> M.transportable_symbol(Integer(0)/Integer(1),-Integer(1)/Integer(19),-Integer(1))
0

>>> E = EllipticCurve("5077a1")
>>> M = E.modular_symbol(implementation='num')
>>> M.transportable_symbol(Integer(0)/Integer(1),-Integer(35)/Integer(144))
-3
>>> M.transportable_symbol(Integer(0)/Integer(1),-Integer(35)/Integer(144),-Integer(1))
0
>>> M.transportable_symbol(Integer(0)/Integer(1), -Integer(7)/Integer(31798))
0
>>> M.transportable_symbol(Integer(0)/Integer(1), -Integer(7)/Integer(31798), -Integer(1))
-5