days22presentation

581 days ago by samlichtenstein

IntInfty: Input: z in the upper half plane (as a symbolic expression) A an irreducible Hecke submodule of a space of cusporms prec a precision parameter (decimal digits) Output: tuple of [\int_{\infty}^z f(z) dq/q to precision prec decimal places for various complex embeddings of K_f]
def IntInfty(z, A, prec=5): numterms = ZZ(round(prec/z.imag())+1) print numterms, "terms needed for", z f = A.q_eigenform(numterms+1, 'a') K = f[0].parent() C = ComplexField(100) embs = K.embeddings(C) return [sum([exp(2*C(pi*I*z*n))*emb(f[n])/n for n in [1..numterms]]) for emb in embs] 
       
Return a list of pairs (g, \{\infty,g(\infty)\}) such that the symbols \{\infty,g(\infty)\} are a basis over \mathbf{Q} for the cuspidal subspace. Over \mathbf{Q} ! Not over \mathbf{Z}. Also the index of the span of those pairs in the full cuspidal \mathbf{Z}-module. (William wrote this.)
def gamsym(N): """ Return a list of pairs (g, {oo,g(oo)}) such that the symbols {oo,g(oo)} are a basis over QQ for the cuspidal subspace. Over QQ ! Not over ZZ. Also the index of the span of those pairs in the full cuspidal ZZ-module. """ G = Gamma0(N) gens = G.gens() s = gens[0] M = ModularSymbols(N,2) B = [(g, M([oo, g[0,0]/g[1,0] if g[1,0] else oo])) for g in gens] C = [x.element() for _,x in B] P = matrix(C).pivot_rows() d = span(ZZ, [C[i] for i in P]).index_in(M.cuspidal_subspace().integral_structure()) return [B[i] for i in P], d 
       
On input g\in \Gamma_0(N) and A a Galois orbit of newforms, and a precision, return \int_\infty^{g\infty}f(z) dq/q to specified precision.
def PeriodIntegral_oo_goo(g, A, prec=5): N = A.level() gg = g.matrix() cN = gg[1:2][0][0] d = gg[1:2][0][1] z = -d/cN + I/abs(cN) gz = SL2Z(gg).acton(z) return [a - b for [a,b] in zip(IntInfty(gz, A, prec),IntInfty(z, A, prec))] 
       
Given x = \{\alpha,\beta\}\in \mathbb{S}_2(\Gamma_0(N)), A a Galois orbit of newforms f_1,f_2,\ldots, compute [\langle x, f_1\rangle,\langle x, f_2\rangle,\ldots].
def PeriodIntegral(x, A, prec, good=0): S = x.parent().cuspidal_subspace() rk = S.dimension() M = S.ambient() N = S.level() if good: good_basis = good[0] good_periods = good[1] else: X, index = gamsym(N) good_basis = [a[1].element() for a in X] good_periods = [PeriodIntegral_oo_goo(X[i][0],A,prec) for i in [0..rk-1]] # this is the good basis represented as a list # in QQ^(dim M) V = VectorSpace(QQ, M.dimension()) W = V.subspace_with_basis(good_basis) xcoords = W.coordinate_vector(x.element()) if good: return xcoords*matrix(good_periods) else: return (xcoords*matrix(good_periods),(good_basis, good_periods)) 
       
This function returns a basis for the period lattice of X_0(N) as a list of vectors in \mathbf{C}^g, to specified precision. (It's kind of stupid... It does a lot more work than it really needs to, so it's quite slow when N\gg 0. But we only really want to use it for N up to 109.)
def PeriodLattice(N, prec): # first compute a basis for cusp forms S_2(Gamma_0(N)) # this takes the form of a list of tuples, (A, d) # where A is a simple, new subspace in S_2(Gamma_0(M)), M|N, d|(N/M), # and that pair (A,d) corresponds to the image of # (a Galois orbit of newforms in) A, inside S_2(Gamma_0(N)) # under the d'th degeneracy map. if not N.is_prime(): print "Composite level not yet implemented!" return False S = ModularSymbols(N,2).cuspidal_subspace() D = S.decomposition() #basis = [] #for B in D: # for A in B.simple_factor(): # M = A.level() # n = N/M # for d in divisors(n): # basis.append((A,d)) basis = [A for A in S.decomposition()] # next compute a basis for H_1(X_0(N),ZZ) consisting of paths {oo, g.oo} X, index = gamsym(N) H = S.integral_basis() # periods = [] for A in basis: for g,x in X: if not periods: new_period_data = PeriodIntegral(x, A, prec) newperiods = new_period_data[0] good = new_period_data[1] else: newperiods = PeriodIntegral(x, A, prec, good) periods.append(newperiods) print "the", len(periods), "periods are", periods if index > 1: print "WARNING: they might not be a basis for the period lattice!" return periods 
       
Here is some evidence that we compute Period Lattices correctly.
Lattice11a1 = PeriodLattice(11,10) 
       
111 terms needed for 1/11*I - 5/11
111 terms needed for 1/11*I - 2/11
221 terms needed for 1/22*I - 7/22
221 terms needed for 1/22*I - 3/22
the 2 periods are [(-1.2692093042795534216887946168 -
5.7192415628523355892350320983e-30*I), (-0.63460465213977671084439730839
- 1.4588166169384952293308896130*I)]
111 terms needed for 1/11*I - 5/11
111 terms needed for 1/11*I - 2/11
221 terms needed for 1/22*I - 7/22
221 terms needed for 1/22*I - 3/22
the 2 periods are [(-1.2692093042795534216887946168 - 5.7192415628523355892350320983e-30*I), (-0.63460465213977671084439730839 - 1.4588166169384952293308896130*I)]
Lattice11a1ForReal = EllipticCurve('11a1').period_lattice().normalised_basis() Lattice11a1ForReal[0]/Lattice11a1ForReal[1] 
       
-0.500000000000000 + 1.14939010612325*I
-0.500000000000000 + 1.14939010612325*I
Lattice11a1[1][0]/Lattice11a1[0][0] 
       
0.50000000000000000000000000001 + 1.1493901061232523806876282122*I
0.50000000000000000000000000001 + 1.1493901061232523806876282122*I

That's good, because X_0(11) = E.

This is the Abel Jacobi map:

def AbelJacobi(A, tuple, prec=5): n = A.dimension() a = vector([0]*n) for i in range(n): a = a + vector(IntInfty(tuple[i],A,prec)) return a 
       
This function moves myvector in \mathbf{C}^g into the fundamental domain specified by the lattice basis lattice_tuple provided.
def LatticeRep(myvector, lattice_tuple): V = myvector.parent() reV = VectorSpace(RR, 2*V.dimension()) real_list = [] for j in range(V.dimension()): real_list.append(myvector[j].real()) real_list.append(myvector[j].imag()) real_vector = vector(real_list) n = len(lattice_tuple) real_lattice_tuple = [] for i in range(n): z = [] for j in range(V.dimension()): z.append(lattice_tuple[i][j].real()) z.append(lattice_tuple[i][j].imag()) real_lattice_tuple.append(vector(z)) W = reV.subspace_with_basis(real_lattice_tuple) coords = W.coordinates(tuple(real_vector)) translated_vector = [x - math.floor(x) for x in coords] complex_translated_vector = [CC(translated_vector[2*j] + translated_vector[2*j+1]*I) for j in [0..V.dimension()-1]] return vector(complex_translated_vector) 
       

This next one is a bit tricky. Here's the deal. Let f be a newform in S_2(\Gamma_0(N)). Let \lambda be a prime of K_f lying over \ell. We're trying to compute the splitting field of \bar\rho_{f,\lambda}. Where does this representation occur? It occurs inside the \ell-torsion of the Jacobian J_0(N) = Jac(X_0(N)). However, it is not the whole thing.

The \ell torsion J_0(N)[\ell](\mathbf{C}) \cong \frac{1}{\ell}\Lambda/\Lambda, where \Lambda is the period lattice for X_0(N), which we can compute using the functions above. Now J_0(N)[\ell] has compatible Hecke and Galois actions. The theorem is that

\bar\rho_{f,\lambda} = J_0(N)[\mathbf{m}],
where \mathbf{m} is the maximal ideal of the Hecke algebra obtained as the kernel of the eigenvalue map
\mathbb{T} \rightarrow \mathcal{O}_f \rightarrow \mathcal{O}_f/\lambda.
(So \mathbf{m} is generated by \lambda and T_n-a_n for all n, where f=\sum a_n q^n.)

So we have this \mathbf{F}_\ell-vector space J_0(N)[\ell]=\frac 1\ell \Lambda/\Lambda of dimension 2g. (g is the genus of X_0(N).) We need to pick out the part where \mathbf{m} acts trivially. This will be a subspace of dimension 2[\mathcal{O}_f/\lambda: \mathbf{F}_\ell]. Then we can use our complex approximations of the points in that subspace to recover the splitting field (see below).

The next function (a work in progress, but mostly done) picks out that subspace by finding a \mathbf{Z}-basis for the Hecke algebra, explicitly producing the ideal \mathbf{m} as a submodule, and computing the intersection of the kernels of the matrices of a basis for \mathbf{m} (as operators on \mathbb{S}_2(\Gamma_0(N))).

# THIS STILL NEEDS TO BE DEBUGGED def RepresentationSpace(A,lam,prec=5): ell = lam.residue_field().characteristic() N = A.level() print "Computing period lattice..." L = PeriodLattice(N,prec) sturm = N//6 + 1 S = ModularSymbols(N,2).cuspidal_subspace() X, index = gamsym(N) Sgoodbasis = [x[1].element() for x in X] Sbasis = S.integral_basis() space = VectorSpace(QQ,S.ambient().dimension()) subspace = space.subspace_with_basis(Sgoodbasis) change_of_basis = matrix([subspace.coordinate_vector(x.element()) for x in Sbasis]) print "Making Hecke algebra..." T = [[],[]] # T[0] = basis for T, expressed as matrices # acting on the RIGHT on elements of S expressed as row vectors # wrt Sgoodbasis # (S.integral_hecke_matrix() uses Sbasis instead!) # T[1] = which Tns are in T[0] for n in [1..sturm]: if gcd(n, N) == 1: Tn = change_of_basis.inverse()*S.integral_hecke_matrix(n)*change_of_basis T[0].append(Tn) T[1].append(n) # start with Tn for all n up to Sturm bound # then remove superfluous generators until T[0] # is ZZ-linearly independent in End_ZZ(S) vects = [m.list() for m in T[0]] rk = len(vects[0]) ambientmod = FreeModule(ZZ,rk) num_inds = len(T[0]) inds = [1..num_inds] inds.reverse() for i in inds: if len(vects) > 1: mod = ambientmod.span(vects[:i-1]+vects[i:]) if ambientmod(vects[i-1]) in mod: vects = vects[:i-1]+vects[i:] T[0] = T[0][:i-1]+T[0][i:] T[1] = T[1][:i-1]+T[1][i:] print "Creating Hecke map and ideal for newforms in", A, "..." f = A.q_eigenform(sturm,'a') K = f[0].parent() OK = K.maximal_order() OKmod = FreeModule(ZZ, len(OK.basis())) lammod = OKmod.span([x.list() for x in lam.basis()]) Tmod = FreeModule(ZZ, len(T[0])) hecke_map = Tmod.hom([f[n].list() for n in T[1]], OKmod) hecke_ideal = hecke_map.inverse_image(lammod) print "Computing representation space..." Fell = GF(ell) V = VectorSpace(Fell, len(Sgoodbasis)) MatAlg = MatrixSpace(Fell, len(Sgoodbasis)) print "OK" for b in hecke_ideal.basis(): mat = MatAlg(sum([b[i]*T[0][i] for i in [0..len(b)-1]])) V = V.intersection(mat.kernel()) print "OK" basis = V.basis_matrix().rows() dim = len(basis) zero = [0 for a in [1..len(L)]] print "OK" vectors = V.list() vectors.remove(V(zero)) print "OK" return [sum([ZZ(e[i])*L[i] for i in [0..len(L)-1]]) for e in vectors] #fixme: RepresentationSpace should return the approximate # torsion points in Vlambda, GROUPED IN LINES!!!! for the purposes # of writing downing the projective representation 
       

What remains to be done? The remaining ingredients in the algorithm are the following.

First, now we know complex approximations of the \bar{\mathbf{Q}}-points of J_0(N) where our representation \bar\rho_{f,\lambda} lives. The next step is to obtain good approximations to divisors on X_0(N) which map to those points under the Abel Jacobi map

X_0(N)^g(\mathbf{C}) \rightarrow J_0(N)(\mathbf{C}).
This uses a variation of Newton's method (start with a guess, then keep improving it), which Jon has implemented.

From the last step we now have a bunch of algebraic points on X_0(N)(\mathbf{C}) = \Gamma_0(N)\backslash \mathbb{H}, expressed as a bunch of approximately known points in the upper half plane. Now we apply a rational function (expressed in terms of modular forms) on X_0(N) to get a bunch of points in \mathbf{P}^1. These algebraic numbers should (if we made our choices correctly) be the roots of the defining polynomial for the splitting field we seek.

Finally, we recover the actual polynomial from the approximately-known coefficients. The actual coefficients are rational numbers of reasonably small height, so this is not a problem, provided we have approximated them to sufficient precision.

Final comment: that polynomial will have (in the case of interest [\mathcal{O}_f/\lambda: \mathbf{F}_\ell]=1) degree \ell^2-1, which is big when \ell is, say, 7. More tractable is the polynomial corresponding to the projectivized representation into PGL_2(\mathbf{F}_\ell), which has degree only \ell+1. So we should really group the points of the \mathcal{O}_f/\lambda-vector space J_0(N)[\mathbf{m}] into lines...