def test_ellrains(pbound = 2**62, nbound = 100): import sys from sage.sets.primes import Primes for p in Primes(): if p < 4: continue if p > pbound: break for n in xrange(2, nbound): k = GF(p**n, name='z') K = FiniteField_flint_fq_nmod(p, k.modulus(), name='z') try: a, b = find_gens(K, K) print "Testing p = {} and n = {}".format(p, n) print "Computing minpol...", sys.stdout.flush() f = a.minpoly() assert f.degree() == n g = b.minpoly() assert f == g print "done" except RuntimeError: pass
def multiple_of_order_using_frobp(self, maxp=None): """ Return a multiple of the order of this torsion group. In the `Gamma_0` case, the multiple is computed using characteristic polynomials of Hecke operators of odd index not dividing the level. In the `Gamma_1` case, the multiple is computed by expressing the frobenius polynomial in terms of the characteristic polynomial of left multiplication by `a_p` for odd primes p not dividing the level. INPUT: - ``maxp`` - (default: None) If maxp is None (the default), return gcd of best bound computed so far with bound obtained by computing GCD's of orders modulo p until this gcd stabilizes for 3 successive primes. If maxp is given, just use all primes up to and including maxp. EXAMPLES:: sage: J = J0(11) sage: G = J.rational_torsion_subgroup() sage: G.multiple_of_order_using_frobp(11) 5 Increasing maxp may yield a tighter bound. If maxp=None, then Sage will use more primes until the multiple stabilizes for 3 successive primes. :: sage: J = J0(389) sage: G = J.rational_torsion_subgroup(); G Torsion subgroup of Abelian variety J0(389) of dimension 32 sage: G.multiple_of_order_using_frobp() 97 sage: [G.multiple_of_order_using_frobp(p) for p in prime_range(3,11)] [92645296242160800, 7275, 291] sage: [G.multiple_of_order_using_frobp(p) for p in prime_range(3,13)] [92645296242160800, 7275, 291, 97] sage: [G.multiple_of_order_using_frobp(p) for p in prime_range(3,19)] [92645296242160800, 7275, 291, 97, 97, 97] We can compute the multiple of order of the torsion subgroup for Gamma0 and Gamma1 varieties, and their products. :: sage: A = J0(11) * J0(33) sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp() 1000 sage: A = J1(23) sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp() 9406793 sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp(maxp=50) 408991 sage: A = J1(19) * J0(21) sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp() 35064 The next example illustrates calling this function with a larger input and how the result may be cached when maxp is None:: sage: T = J0(43)[1].rational_torsion_subgroup() sage: T.multiple_of_order_using_frobp() 14 sage: T.multiple_of_order_using_frobp(50) 7 sage: T.multiple_of_order_using_frobp() 7 This function is not implemented for general congruence subgroups unless the dimension is zero. :: sage: A = JH(13,[2]); A Abelian variety J0(13) of dimension 0 sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp() 1 sage: A = JH(15, [2]); A Abelian variety JH(15,[2]) of dimension 1 sage: A.rational_torsion_subgroup().multiple_of_order_using_frobp() Traceback (most recent call last): ... NotImplementedError: torsion multiple only implemented for Gamma0 and Gamma1 """ if maxp is None: try: return self.__multiple_of_order_using_frobp except AttributeError: pass A = self.abelian_variety() if A.dimension() == 0: T = ZZ(1) self.__multiple_of_order_using_frobp = T return T if not all((is_Gamma0(G) or is_Gamma1(G) for G in A.groups())): raise NotImplementedError("torsion multiple only implemented for Gamma0 and Gamma1") bnd = ZZ(0) N = A.level() cnt = 0 if maxp is None: X = Primes() else: X = prime_range(maxp+1) for p in X: if (2*N) % p == 0: continue if (len(A.groups()) == 1 and is_Gamma0(A.groups()[0])): f = A.hecke_polynomial(p) b = ZZ(f(p+1)) else: from .constructor import AbelianVariety D = [AbelianVariety(f) for f in A.newform_decomposition('a')] b = 1 for simple in D: G = simple.newform_level()[1] if is_Gamma0(G): f = simple.hecke_polynomial(p) b *= ZZ(f(p+1)) else: f = simple.newform('a') Kf = f.base_ring() eps = f.character() Qe = eps.base_ring() if Kf != QQ: # relativize number fields to compute charpoly of # left multiplication of ap on Kf as a Qe-vector # space. Lf = Kf.relativize(Qe.gen(), 'a') to_Lf = Lf.structure()[1] name = Kf._names[0] ap = to_Lf(f.modular_symbols(1).eigenvalue(p, name)) G_ps = ap.matrix().charpoly() b *= ZZ(Qe(G_ps(1 + to_Lf(eps(p))*p)).norm()) else: ap = f.modular_symbols(1).eigenvalue(p) b *= ZZ(1 + eps(p)*p - ap) if bnd == 0: bnd = b else: bnd_last = bnd bnd = ZZ(gcd(bnd, b)) if bnd == bnd_last: cnt += 1 else: cnt = 0 if maxp is None and cnt >= 2: break # The code below caches the computed bound and # will be used if this function is called # again with maxp equal to None (the default). if maxp is None: # maxp is None but self.__multiple_of_order_using_frobp is # not set, since otherwise we would have immediately # returned at the top of this function self.__multiple_of_order_using_frobp = bnd else: # maxp is given -- record new info we get as # a gcd... try: self.__multiple_of_order_using_frobp = \ gcd(self.__multiple_of_order_using_frobp, bnd) except AttributeError: # ... except in the case when # self.__multiple_of_order_using_frobp was never set. In this # case, we just set it as long as the gcd stabilized for 3 in a # row. if cnt >= 2: self.__multiple_of_order_using_frobp = bnd return bnd
def FinitelyGeneratedHeisenbergPresentation(n=1, p=0): r""" Return a finite presentation of the Heisenberg group. The Heisenberg group is the group of `(n+2) \times (n+2)` matrices over a ring `R` with diagonal elements equal to 1, first row and last column possibly nonzero, and all the other entries equal to zero. INPUT: - ``n`` -- the degree of the Heisenberg group - ``p`` -- (optional) a prime number, where we construct the Heisenberg group over the finite field `\ZZ/p\ZZ` OUTPUT: Finitely generated Heisenberg group over the finite field of order ``p`` or over the integers. .. SEEALSO:: :class:`~sage.groups.matrix_gps.heisenberg.HeisenbergGroup` EXAMPLES:: sage: H = groups.presentation.Heisenberg(); H Finitely presented group < x1, y1, z | x1*y1*x1^-1*y1^-1*z^-1, z*x1*z^-1*x1^-1, z*y1*z^-1*y1^-1 > sage: H.order() +Infinity sage: r1, r2, r3 = H.relations() sage: A = matrix([[1, 1, 0], [0, 1, 0], [0, 0, 1]]) sage: B = matrix([[1, 0, 0], [0, 1, 1], [0, 0, 1]]) sage: C = matrix([[1, 0, 1], [0, 1, 0], [0, 0, 1]]) sage: r1(A, B, C) [1 0 0] [0 1 0] [0 0 1] sage: r2(A, B, C) [1 0 0] [0 1 0] [0 0 1] sage: r3(A, B, C) [1 0 0] [0 1 0] [0 0 1] sage: p = 3 sage: Hp = groups.presentation.Heisenberg(p=3) sage: Hp.order() == p**3 True sage: Hnp = groups.presentation.Heisenberg(n=2, p=3) sage: len(Hnp.relations()) 13 REFERENCES: - :wikipedia:`Heisenberg_group` """ n = Integer(n) if n < 1: raise ValueError('n must be a positive integer') # generators' names are x1, .., xn, y1, .., yn, z vx = ['x' + str(i) for i in range(1, n + 1)] vy = ['y' + str(i) for i in range(1, n + 1)] str_generators = ', '.join(vx + vy + ['z']) F = FreeGroup(str_generators) x = F.gens()[0:n] # list of generators x1, x2, ..., xn y = F.gens()[n:2 * n] # list of generators x1, x2, ..., xn z = F.gen(n * 2) def commutator(a, b): return a * b * a**-1 * b**-1 # First set of relations: [xi, yi] = z r1 = [commutator(x[i], y[i]) * z**-1 for i in range(n)] # Second set of relations: [z, xi] = 1 r2 = [commutator(z, x[i]) for i in range(n)] # Third set of relations: [z, yi] = 1 r3 = [commutator(z, y[i]) for i in range(n)] # Fourth set of relations: [xi, yi] = 1 for i != j r4 = [commutator(x[i], y[j]) for i in range(n) for j in range(n) if i != j] rls = r1 + r2 + r3 + r4 from sage.sets.primes import Primes if p not in Primes() and p != 0: raise ValueError("p must be 0 or a prime number") if p > 0: rls += [w**p for w in F.gens()] return FinitelyPresentedGroup(F, tuple(rls))
def hilbert_symbol_negative_at_S(self, S, b, check=True): r""" Returns an integer that has a negative Hilbert symbol with respect to a given rational number and a given set of primes (or places). The function is algorithm 3.4.1 in [Kir2016]_. It finds an integer `a` that has negative Hilbert symbol with respect to a given rational number exactly at a given set of primes (or places). INPUT: - ``S`` -- a list of rational primes, the infinite place as real embedding of `\QQ` or as -1 - ``b`` -- a non-zero rational number which is a non-square locally at every prime in ``S``. - ``check`` -- ``bool`` (default:``True``) perform additional checks on input and confirm the output. OUTPUT: - An integer `a` that has negative Hilbert symbol `(a,b)_p` for every place `p` in `S` and no other place. EXAMPLES:: sage: QQ.hilbert_symbol_negative_at_S([-1,5,3,2,7,11,13,23], -10/7) -9867 sage: QQ.hilbert_symbol_negative_at_S([3, 5, QQ.places()[0], 11], -15) -33 sage: QQ.hilbert_symbol_negative_at_S([3, 5], 2) 15 TESTS:: sage: QQ.hilbert_symbol_negative_at_S(5/2, -2) Traceback (most recent call last): ... TypeError: first argument must be a list or integer :: sage: QQ.hilbert_symbol_negative_at_S([1, 3], 0) Traceback (most recent call last): ... ValueError: second argument must be nonzero :: sage: QQ.hilbert_symbol_negative_at_S([-1, 3, 5], 2) Traceback (most recent call last): ... ValueError: list should be of even cardinality :: sage: QQ.hilbert_symbol_negative_at_S([1, 3], 2) Traceback (most recent call last): ... ValueError: all entries in list must be prime or -1 for infinite place :: sage: QQ.hilbert_symbol_negative_at_S([5, 7], 2) Traceback (most recent call last): ... ValueError: second argument must be a nonsquare with respect to every finite prime in the list :: sage: QQ.hilbert_symbol_negative_at_S([1, 3], sqrt(2)) Traceback (most recent call last): ... TypeError: second argument must be a rational number :: sage: QQ.hilbert_symbol_negative_at_S([-1, 3], 2) Traceback (most recent call last): ... ValueError: if the infinite place is in the list, the second argument must be negative AUTHORS: - Simon Brandhorst, Juanita Duque, Anna Haensch, Manami Roy, Sandi Rudzinski (10-24-2017) """ from sage.rings.finite_rings.finite_field_constructor import FiniteField as GF from sage.rings.padics.factory import Qp from sage.modules.free_module import VectorSpace from sage.matrix.constructor import matrix from sage.sets.primes import Primes from sage.arith.misc import hilbert_symbol, is_prime # input checks if not type(S) is list: raise TypeError("first argument must be a list or integer") # -1 is used for the infinite place infty = -1 for i in range(len(S)): if S[i] == self.places()[0]: S[i] = -1 if not b in self: raise TypeError("second argument must be a rational number") b = self(b) if b == 0: raise ValueError("second argument must be nonzero") if len(S) % 2: raise ValueError("list should be of even cardinality") for p in S: if p != infty: if check and not is_prime(p): raise ValueError("all entries in list must be prime" " or -1 for infinite place") R = Qp(p) if R(b).is_square(): raise ValueError( "second argument must be a nonsquare with" " respect to every finite prime in the list") elif b > 0: raise ValueError("if the infinite place is in the list, " "the second argument must be negative") # L is the list of primes that we need to consider, b must have # nonzero valuation for each prime in L, this is the set S' # in Kirschmer's algorithm L = [] L = [p[0] for p in b.factor() if p[0] not in S] # We must also consider 2 to be in L if 2 not in L and 2 not in S: L.append(2) # This adds the infinite place to L if b < 0 and infty not in S: L.append(infty) P = S + L # This constructs the vector v in the algorithm. This is the vector # that we are searching for. It represents the case when the Hilbert # symbol is negative for all primes in S and positive # at all primes in S' V = VectorSpace(GF(2), len(P)) v = V([1] * len(S) + [0] * len(L)) # Compute the map phi of Hilbert symbols at all the primes # in S and S' # For technical reasons, a Hilbert symbol of -1 is # respresented as 1 and a Hilbert symbol of 1 # is represented as 0 def phi(x): v = [(1 - hilbert_symbol(x, b, p)) // 2 for p in P] return V(v) M = matrix(GF(2), [phi(p) for p in P + [-1]]) # We search through all the primes for q in Primes(): # Only look at this prime if it is not in our list if q in P: continue # The algorithm terminates when the vector v is in the # subspace of V generated by the image of the phi map # on the set of generators w = phi(q) W = M.stack(matrix(w)) if v in W.row_space(): break Pq = P + [-1] + [q] l = W.solve_left(v) a = self.prod([Pq[i]**ZZ(l[i]) for i in range(l.degree())]) if check: assert phi(a) == v, "oops" return a
def multiple_of_order(self, maxp=None): """ Return a multiple of the order of this torsion group. The multiple is computed using characteristic polynomials of Hecke operators of odd index not dividing the level. INPUT: - ``maxp`` - (default: None) If maxp is None (the default), return gcd of best bound computed so far with bound obtained by computing GCD's of orders modulo p until this gcd stabilizes for 3 successive primes. If maxp is given, just use all primes up to and including maxp. EXAMPLES:: sage: J = J0(11) sage: G = J.rational_torsion_subgroup() sage: G.multiple_of_order(11) 5 sage: J = J0(389) sage: G = J.rational_torsion_subgroup(); G Torsion subgroup of Abelian variety J0(389) of dimension 32 sage: G.multiple_of_order() 97 sage: [G.multiple_of_order(p) for p in prime_range(3,11)] [92645296242160800, 7275, 291] sage: [G.multiple_of_order(p) for p in prime_range(3,13)] [92645296242160800, 7275, 291, 97] sage: [G.multiple_of_order(p) for p in prime_range(3,19)] [92645296242160800, 7275, 291, 97, 97, 97] :: sage: J = J0(33) * J0(11) ; J.rational_torsion_subgroup().order() Traceback (most recent call last): ... NotImplementedError: torsion multiple only implemented for Gamma0 The next example illustrates calling this function with a larger input and how the result may be cached when maxp is None:: sage: T = J0(43)[1].rational_torsion_subgroup() sage: T.multiple_of_order() 14 sage: T.multiple_of_order(50) 7 sage: T.multiple_of_order() 7 """ if maxp is None: try: return self.__multiple_of_order except AttributeError: pass bnd = ZZ(0) A = self.abelian_variety() if A.dimension() == 0: T = ZZ(1) self.__multiple_of_order = T return T N = A.level() if not (len(A.groups()) == 1 and is_Gamma0(A.groups()[0])): # to generalize to this case, you'll need to # (1) define a charpoly_of_frob function: # this is tricky because I don't know a simple # way to do this for Gamma1 and GammaH. Will # probably have to compute explicit matrix for # <p> operator (add to modular symbols code), # then compute some charpoly involving # that directly... # (2) use (1) -- see my MAGMA code. raise NotImplementedError( "torsion multiple only implemented for Gamma0") cnt = 0 if maxp is None: X = Primes() else: X = prime_range(maxp + 1) for p in X: if (2 * N) % p == 0: continue f = A.hecke_polynomial(p) b = ZZ(f(p + 1)) if bnd == 0: bnd = b else: bnd_last = bnd bnd = ZZ(gcd(bnd, b)) if bnd == bnd_last: cnt += 1 else: cnt = 0 if maxp is None and cnt >= 2: break # The code below caches the computed bound and # will be used if this function is called # again with maxp equal to None (the default). if maxp is None: # maxp is None but self.__multiple_of_order is # not set, since otherwise we would have immediately # returned at the top of this function self.__multiple_of_order = bnd else: # maxp is given -- record new info we get as # a gcd... try: self.__multiple_of_order = gcd(self.__multiple_of_order, bnd) except AttributeError: # ... except in the case when self.__multiple_of_order # was never set. In this case, we just set # it as long as the gcd stabilized for 3 in a row. if cnt >= 2: self.__multiple_of_order = bnd return bnd
def p_saturation(Plist, p, sieve=True, lin_combs=dict(), verbose=False): r""" Checks whether the list of points is `p`-saturated. INPUT: - ``Plist`` (list) - a list of independent points on one elliptic curve. - ``p`` (integer) - a prime number. - ``sieve`` (boolean) - if True, use a sieve (when there are at least 2 points); otherwise test all combinations. - ``lin_combs`` (dict) - a dict, possibly empty, with keys coefficient tuples and values the corresponding linear combinations of the points in ``Plist``. .. note:: The sieve is much more efficient when the points are saturated and the number of points or the prime are large. OUTPUT: Either (``True``, ``lin_combs``) if the points are `p`-saturated, or (``False``, ``i``, ``newP``) if they are not `p`-saturated, in which case after replacing the i'th point with ``newP``, the subgroup generated contains that generated by ``Plist`` with index `p`. Note that while proving the points `p`-saturated, the ``lin_combs`` dict may have been enlarged, so is returned. EXAMPLES:: sage: from sage.schemes.elliptic_curves.saturation import p_saturation sage: E = EllipticCurve('389a') sage: K.<i> = QuadraticField(-1) sage: EK = E.change_ring(K) sage: P = EK(1+i,-1-2*i) sage: p_saturation([P],2) (True, {}) sage: p_saturation([2*P],2) (False, 0, (i + 1 : -2*i - 1 : 1)) sage: Q = EK(0,0) sage: R = EK(-1,1) sage: p_saturation([P,Q,R],3) (True, {}) Here we see an example where 19-saturation is proved, with the verbose flag set to True so that we can see what is going on:: sage: p_saturation([P,Q,R],19, verbose=True) Using sieve method to saturate... There is 19-torsion modulo Fractional ideal (i + 14), projecting points --> [(184 : 27 : 1), (0 : 0 : 1), (196 : 1 : 1)] --rank is now 1 There is 19-torsion modulo Fractional ideal (i - 14), projecting points --> [(15 : 168 : 1), (0 : 0 : 1), (196 : 1 : 1)] --rank is now 2 There is 19-torsion modulo Fractional ideal (-2*i + 17), projecting points --> [(156 : 275 : 1), (0 : 0 : 1), (292 : 1 : 1)] --rank is now 3 Reached full rank: points were 19-saturated (True, {}) An example where the points are not 11-saturated:: sage: res = p_saturation([P+5*Q,P-6*Q,R],11); res (False, 0, (-5783311/14600041*i + 1396143/14600041 : 37679338314/55786756661*i + 3813624227/55786756661 : 1)) That means that the 0'th point may be replaced by the displayed point to achieve an index gain of 11:: sage: p_saturation([res[2],P-6*Q,R],11) (True, {}) TESTS: See :trac:`27387`:: sage: K.<a> = NumberField(x^2-x-26) sage: E = EllipticCurve([a,1-a,0,93-16*a, 3150-560*a]) sage: P = E([65-35*a/3, (959*a-5377)/9]) sage: T = E.torsion_points()[0] sage: from sage.schemes.elliptic_curves.saturation import p_saturation sage: p_saturation([P,T],2,True, {}, True) Using sieve method to saturate... ... -- points were not 2-saturated, gaining index 2 (False, 0, (-1/4*a + 3/4 : 59/8*a - 317/8 : 1)) sage: p_saturation([P,T],2,False, {}) (False, 0, (-1/4*a + 3/4 : 59/8*a - 317/8 : 1)) """ # This code does a lot of residue field construction and elliptic curve # group structure computations, and would benefit immensely if those # were sped up. n = len(Plist) if n == 0: return (True, lin_combs) if n == 1: try: return (False, 0, Plist[0].division_points(p)[0]) except IndexError: return (True, lin_combs) E = Plist[0].curve() if not sieve: from sage.groups.generic import multiples from sage.schemes.projective.projective_space import ProjectiveSpace mults = [list(multiples(P, p)) for P in Plist[:-1]] + [list(multiples(Plist[-1], 2))] E0 = E(0) for v in ProjectiveSpace(GF(p), n - 1): # an iterator w = tuple(int(x) for x in v) try: P = lin_combs[w] except KeyError: P = sum([m[c] for m, c in zip(mults, w)], E0) lin_combs[w] = P divisors = P.division_points(p) if divisors: if verbose: print(" points not saturated at %s, increasing index" % p) # w will certainly have a coordinate equal to 1 return (False, w.index(1), divisors[0]) # we only get here if no linear combination is divisible by p, # so the points are p-saturated: return (True, lin_combs) # Now we use the more sophisticated sieve to either prove # p-saturation, or compute a much smaller list of possible points # to test for p-divisibility: E = Plist[0].curve() K = E.base_ring() def projections(Q, p): r""" Project points onto (E mod Q)(K mod Q) \otimes \F_p. Returns a list of 0, 1 or 2 vectors in \F_p^n """ # NB we do not do E.reduction(Q) since that may change the # model which will cause problems when we reduce points. # see trac #27387 Ek = E.change_ring(K.residue_field(Q)) G = Ek.abelian_group() # cached! if not p.divides(G.order()): return [] if verbose: print("There is %s-torsion modulo %s, projecting points" % (p, Q)) def proj1(pt): try: return Ek(pt) except ZeroDivisionError: return Ek(0) projPlist = [proj1(pt) for pt in Plist] if verbose: print(" --> %s" % projPlist) gens = G.gens() orders = G.generator_orders() from sage.groups.generic import discrete_log_lambda from sage.modules.all import vector if len(gens) == 1: # cyclic case n = orders[0] m = n.prime_to_m_part(p) # prime-to-p part of order p2 = n // m # p-primary order g = (m * gens[0]).element() # generator of p-primary part assert g.order() == p2 # if verbose: # print(" cyclic, %s-primary part generated by %s of order %s" % (p,g,p2)) # multiplying any point by m takes it into the p-primary # part -- this way we do discrete logs in a cyclic group # of order p2 (which will often just be p) and not order n v = [ discrete_log_lambda(m * pt, g, (0, p2), '+') for pt in projPlist ] # entries of v are well-defined mod p2, hence mod p return [vector(GF(p), v)] # Now the reduction is not cyclic, but we still handle # separately the case where the p-primary part is cyclic, # where a similar discrete log computation suffices; in the # remaining case (p-rank 2) we use the Weil pairing instead. # Note the code makes no assumption about which generator # order divides the other, since conventions differ! mm = [ni.prime_to_m_part(p) for ni in orders] pp = [ni // mi for ni, mi in zip(orders, mm)] # p-powers gg = [(mi * gi).element() for mi, gi in zip(mm, gens)] # p-power order gens m = max(mm) # = lcm(mm), the prime-to-p exponent of G; # multiply by this to map onto the p-primary part. p2 = max(pp) # p-exponent of G p1 = min(pp) # == 1 iff p-primary part is cyclic if p1 == 1: # p-primary part is cyclic of order p2 g = gg[pp.index(p2)] # g generates the p-primary part # and we do discrete logs there: assert g.order() == p2 # if verbose: # print(" non-cyclic but %s-primary part cyclic generated by %s of order %s" % (p,g,p2)) v = [ discrete_log_lambda(m * pt, g, (0, p2), '+') for pt in projPlist ] # entries of v are well-defined mod p2, hence mod p return [vector(GF(p), v)] # Now the p-primary part is non-cyclic of exponent p2; we use # Weil pairings of this order, whose values are p1'th roots of unity. # if verbose: # print(" %s-primary part non-cyclic generated by %s of orders %s" % (p,gg,pp)) zeta = gg[0].weil_pairing(gg[1], p2) # a primitive p1'th root of unity if zeta.multiplicative_order() != p1: if verbose: print("Ek = ", Ek) print("(p1,p2) = (%s,%s)" % (p1, p2)) print("gg = (%s,%s)" % (gg[0], gg[1])) raise RuntimeError("Weil pairing error during saturation.") # these are the homomorphisms from E to F_p (for g in gg): def a(pt, g): """Return the zeta-based log of the Weil pairing of ``m*pt`` with ``g``. """ w = (m * pt).weil_pairing(g, p2) # result is a p1'th root of unity return discrete_log_lambda(w, zeta, (0, p1), '*') return [vector(GF(p), [a(pt, gen) for pt in projPlist]) for gen in gg] if verbose: print("Using sieve method to saturate...") from sage.matrix.all import matrix A = matrix(GF(p), 0, n) rankA = 0 count = 0 from sage.sets.primes import Primes Edisc = E.discriminant() for q in Primes(): for Q in K.primes_above(q, degree=1): if not E.is_local_integral_model(Q) or Edisc.valuation(Q) != 0: continue vecs = projections(Q, p) if len(vecs) == 0: continue for v in vecs: A = matrix(A.rows() + [v]) newrank = A.rank() if verbose: print(" --rank is now %s" % newrank) if newrank == n: if verbose: print("Reached full rank: points were %s-saturated" % p) return (True, lin_combs) if newrank == rankA: count += 1 if count == 10: if verbose: print("! rank same for 10 steps, checking kernel...") # no increase in rank for the last 10 primes Q # find the points in the kernel and call the no-sieve version vecs = A.right_kernel().basis() if verbose: print("kernel vectors: %s" % vecs) Rlist = [ sum([int(vi) * Pi for vi, Pi in zip(v, Plist)], E(0)) for v in vecs ] if verbose: print("points generating kernel: %s" % Rlist) res = p_saturation(Rlist, p, sieve=False, lin_combs=dict(), verbose=verbose) if res[0]: # points really were saturated if verbose: print("-- points were %s-saturated" % p) return (True, lin_combs) else: # they were not, and Rlist[res[1]] can be # replaced by res[2] to enlarge the span; we # need to find a point in Plist which can be # replaced: any one with a nonzero coordinate in # the appropriate kernel vector will do. if verbose: print( "-- points were not %s-saturated, gaining index %s" % (p, p)) j = next(i for i, x in enumerate(vecs[res[1]]) if x) return (False, j, res[2]) else: # rank stayed same; carry on using more Qs pass else: # rank went up but is <n; carry on using more Qs rankA = newrank count = 0 # We reach here only if using all good primes of norm<1000 the # rank never stuck for 10 consecutive Qs but is still < n. That # means that n is rather large, or perhaps that E has a large # number of bad primes. So we fall back to the naive method, # still making use of any partial information about the kernel we # have acquired. vecs = A.right_kernel().basis() Rlist = [ sum([int(vi) * Pi for vi, Pi in zip(v, Plist)], E(0)) for v in vecs ] res = p_saturation(Rlist, p, sieve=False, lin_combs=dict(), verbose=verbose) if res[0]: # points really were saturated return (True, lin_combs) else: j = next(i for i, x in enumerate(vecs[res[1]]) if x) return (False, j, res[2])
def p_saturation(self, Plist, p, sieve=True): r"""Checks whether the list of points is `p`-saturated. INPUT: - ``Plist`` (list) - a list of independent points on one elliptic curve. - ``p`` (integer) - a prime number. - ``sieve`` (boolean) - if True, use a sieve (when there are at least 2 points); otherwise test all combinations. .. note:: The sieve is much more efficient when the points are saturated and the number of points or the prime are large. OUTPUT: Either ``False`` if the points are `p`-saturated, or (``i``, ``newP``) if they are not `p`-saturated, in which case after replacing the i'th point with ``newP``, the subgroup generated contains that generated by ``Plist`` with index `p`. EXAMPLES:: sage: from sage.schemes.elliptic_curves.saturation import EllipticCurveSaturator sage: E = EllipticCurve('389a') sage: K.<i> = QuadraticField(-1) sage: EK = E.change_ring(K) sage: P = EK(1+i,-1-2*i) sage: saturator = EllipticCurveSaturator(EK) sage: saturator.p_saturation([P],2) False sage: saturator.p_saturation([2*P],2) (0, (i + 1 : -2*i - 1 : 1)) sage: Q = EK(0,0) sage: R = EK(-1,1) sage: saturator.p_saturation([P,Q,R],3) False Here we see an example where 19-saturation is proved, with the verbose flag set to True so that we can see what is going on:: sage: saturator = EllipticCurveSaturator(EK, verbose=True) sage: saturator.p_saturation([P,Q,R],19) Using sieve method to saturate... E has 19-torsion over Finite Field of size 197, projecting points --> [(15 : 168 : 1), (0 : 0 : 1), (196 : 1 : 1)] --rank is now 1 E has 19-torsion over Finite Field of size 197, projecting points --> [(184 : 27 : 1), (0 : 0 : 1), (196 : 1 : 1)] --rank is now 2 E has 19-torsion over Finite Field of size 293, projecting points --> [(139 : 16 : 1), (0 : 0 : 1), (292 : 1 : 1)] --rank is now 3 Reached full rank: points were 19-saturated False An example where the points are not 11-saturated:: sage: saturator = EllipticCurveSaturator(EK, verbose=False) sage: res = saturator.p_saturation([P+5*Q,P-6*Q,R],11); res (0, (-5783311/14600041*i + 1396143/14600041 : 37679338314/55786756661*i + 3813624227/55786756661 : 1)) That means that the 0'th point may be replaced by the displayed point to achieve an index gain of 11:: sage: saturator.p_saturation([res[1],P-6*Q,R],11) False TESTS: See :trac:`27387`:: sage: K.<a> = NumberField(x^2-x-26) sage: E = EllipticCurve([a,1-a,0,93-16*a, 3150-560*a]) sage: P = E([65-35*a/3, (959*a-5377)/9]) sage: T = E.torsion_points()[0] sage: from sage.schemes.elliptic_curves.saturation import EllipticCurveSaturator sage: saturator = EllipticCurveSaturator(E, True) sage: saturator.p_saturation([P,T], 2) Using sieve method to saturate... ... -- points were not 2-saturated, gaining index 2 (0, (-1/4*a + 3/4 : 59/8*a - 317/8 : 1)) A CM example where large siecing primes are needed (LMFDB label 2.0.3.1-50625.1-CMb2):: sage: K.<a> = NumberField(x^2-x+1) sage: E = EllipticCurve(K, [0, 0, 1, -750, 7906]) sage: E.has_rational_cm() True sage: E.cm_discriminant() -27 sage: points = [E([10, -38]), E([15/49*a + 760/49, 675/343*a - 884/343])] sage: E.saturation(points, verbose=True) # long time (17s) Computing lower height bound.. ..done: 7.168735020029907e-06 p-saturating for primes p < 132 ... Saturating at p=131 --starting full 131-saturation Using sieve method to saturate... E has 131-torsion over Finite Field of size 617011, projecting points --> [(10 : 616973 : 1), (592083 : 192224 : 1)] --rank is now 1 --rank is now 2 Reached full rank: points were 131-saturated Points were 131-saturated --already 131-saturated ([(10 : -38 : 1), (15/49*a + 760/49 : 675/343*a - 884/343 : 1)], 1, 0.123378097374749) """ verbose = self._verbose # This code does a lot of elliptic curve group structure # computations, and would benefit immensely if those were sped # up. n = len(Plist) if n == 0: return False if n == 1 and p == 2: pts = Plist[0].division_points(p) if pts: return (0, pts[0]) else: return False E = self._curve if not sieve: from sage.groups.generic import multiples from sage.schemes.projective.projective_space import ProjectiveSpace mults = [list(multiples(P, p)) for P in Plist[:-1]] + [list(multiples(Plist[-1], 2))] E0 = E(0) for v in ProjectiveSpace(GF(p), n - 1): # an iterator w = tuple(int(x) for x in v) P = sum([m[c] for m, c in zip(mults, w)], E0) pts = P.division_points(p) if pts: if verbose: print( " points not saturated at {}, increasing index by {}" .format(p, p)) # w will certainly have a coordinate equal to 1 return (w.index(1), pts[0]) # we only get here if no linear combination is divisible by p, # so the points are p-saturated: return False # Now we use the more sophisticated sieve to either prove # p-saturation, or compute a much smaller list of possible points # to test for p-divisibility: if verbose: print("Using sieve method to saturate...") from sage.matrix.all import matrix from sage.sets.primes import Primes A = matrix(GF(p), 0, n) rankA = 0 count = 0 # We reduce the curve modulo primes Q of degree 1 above # rational primes q chosen as follows: we avoid primes of bad # reduction and primes dividing the discriminant of the # defining polynomial of the field, so that we can avoid # constructing number field primes entirely and just look for # roots amodq of the defining polynomial mod q, then reduction # of the curve is easy. # We also avoid primes dividing the denominators of any of the # points: the points would reduce to 0 modulo such primes # anyway, and this way reduction of the points is also easy. # Lastly, there is a special case when E has rational CM by # some discriminant D. In the non-split case (kro(D,p)=-1) # the image of the mod-p Galois representation is cyclic of # order p^2-1 and there will be no p-torsion mod Q unless # Frob(Q) has trivial image; a necessary condition for this is # q=1 (mod p) so we restrict to such q. That way the density # of q for which there is a point of order p is 1/(p+1) # instead of 1/(p^2-1). This test was put in after running # many tests: for example, LMFDB curve 2.0.3.1-50625.1-CMb2 # has saturation index bound 132 and to saturate at p=131 # requires q=617011. (In the split case the density is 1/(p-1) # and there is no simple test.) avoid = [self._N, self._D ] + [P[0].denominator_ideal().norm() for P in Plist] cm_test = E.has_rational_cm() and kro(E.cm_discriminant(), p) == -1 for q in Primes(): if any(q.divides(m) for m in avoid): continue if cm_test and not p.divides(q - 1): continue self.add_reductions(q) # does nothing if key q is already there for amodq in self._reductions[q]: (nq, Eq) = self._reductions[q][amodq] if not p.divides(nq): continue if verbose: print("E has %s-torsion over %s, projecting points" % (p, GF(q))) projPlist = [ Eq([reduce_mod_q(c, amodq) for c in pt]) for pt in Plist ] if verbose: print(" --> %s" % projPlist) try: vecs = p_projections(Eq, projPlist, p) except ValueError: vecs = [] for v in vecs: A = matrix(A.rows() + [v]) newrank = A.rank() if verbose: print(" --rank is now %s" % newrank) if newrank == n: if verbose: print( "Reached full rank: points were %s-saturated" % p) return False if newrank == rankA: count += 1 if count == 15: if verbose: print( "! rank same for 15 steps, checking kernel..." ) # no increase in rank for the last 15 primes Q # find the points in the kernel and call the no-sieve version vecs = A.right_kernel().basis() if verbose: print("kernel vectors: %s" % vecs) Rlist = [ sum([int(vi) * Pi for vi, Pi in zip(v, Plist)], E(0)) for v in vecs ] if verbose: print("points generating kernel: %s" % Rlist) # If the nullity of A were 1 (the usual # case) we take any nonzero vector in its # kernel and compute that linear # combination of the points, giving a # point which is certainly a p-multiple # modulo 15 primes Q, and we test if it # actually is a p-multiple: if len(Rlist) == 1: R = Rlist[0] pts = R.division_points(p) if pts: pt = pts[0] v = vecs[0] # replace one of the original # points with this one; we can # replace any for which the # coefficient of v is nonzero if verbose: print( "-- points were not {}-saturated, gaining index {}" .format(p, p)) j = next(i for i, x in enumerate(v) if x) return (j, pt) else: # R is not a p-multiple so the # points were p-saturated return False # Else we call the non-sieve version with # a list of points which are all # p-multiples modulo 15 primes, and we # will just try to divide all linear # combinations of them res = self.p_saturation(Rlist, p, sieve=False) if res: jj, R = res v = vecs[jj] # The v-linear combination of Rlist # really is p*R. Now to enlarge the # span, we may replce the j'th point # in Plist with R, where v[j] is # non-zero. if verbose: print( "-- points were not {}-saturated, gaining index {}" .format(p, p)) j = next(i for i, x in enumerate(v) if x) return (j, R) else: # points really were saturated if verbose: print("-- points were %s-saturated" % p) return False else: # rank went up but is <n; carry on using more Qs rankA = newrank count = 0