diff options
| author | Ryan Rueger <rrueger@velleto.com> | 2024-08-01 19:32:07 +0200 |
|---|---|---|
| committer | Ryan Rueger <rrueger@velleto.com> | 2024-08-01 19:40:09 +0200 |
| commit | a8d238506f9edbf1afbd2ba582deea8a6db252e0 (patch) | |
| tree | ba40f4dc7ca6dbc40776eecc976ee72634e746eb /utils.py | |
| download | ordinaryicga-main.tar.gz ordinaryicga-main.tar.bz2 ordinaryicga-main.zip | |
Diffstat (limited to 'utils.py')
| -rw-r--r-- | utils.py | 1267 |
1 files changed, 1267 insertions, 0 deletions
diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..cdaec4b --- /dev/null +++ b/utils.py @@ -0,0 +1,1267 @@ +#!/usr/bin/env python + +# Silence warnings for now +# https://stackoverflow.com/a/15778297 +from warnings import simplefilter + +import sage.all +from sage.arith.misc import ( + divisors, + factor, + gcd, + is_prime_power, + is_square, + jacobi_symbol, + kronecker_symbol, + xgcd, +) +from math import prod +from sage.all import pari +from sage.arith.misc import is_power_of_two +from sage.functions.other import ceil, floor +from sage.matrix.constructor import Matrix +from sage.misc.functional import sqrt +from sage.modules.free_module_element import vector +from sage.rings.finite_rings.finite_field_constructor import GF +from sage.rings.integer import Integer +from sage.rings.integer_ring import ZZ +from sage.rings.number_field.number_field import QuadraticField +from sage.schemes.elliptic_curves.cm import hilbert_class_polynomial +from sage.schemes.elliptic_curves.constructor import EllipticCurve +from sage.sets.primes import Primes +from sage.structure.factorization_integer import IntegerFactorization + +from math import log + +from random import randint +from itertools import product, cycle, islice + +# Silence warnings about slow implementations in the pre-processing step e.g. +# verbose 0 (4176: multi_polynomial_ideal.py, groebner_basis) +# Warning: falling back to very slow toy implementation. +try: + from sage.misc.verbose import set_verbose + set_verbose(-1) +except ImportError: + try: + from sage.misc.misc import set_verbose + set_verbose(-1) + except ImportError: + pass + +simplefilter(action='ignore', category=FutureWarning) +simplefilter(action='ignore', category=DeprecationWarning) + + +def padicval(p, N): + if N == 0: + return 0 + + if N == 1: + return 0 + + k = 0 + while N % p**k == 0: + k += 1 + return k - 1 + + +class Cideal(): + def __init__(self, O, a, b, m): + self.O = O + self.a = a + self.b = b % a + self.m = m + self.f = O.conductor() + self.D = O.ambient().discriminant() + + if (self.b**2 + self.f**2*(self.D**2 - self.D)//4 + self.f*self.b*self.D) % self.a != 0: + print(f"{a}, {b} not good choices") + raise ValueError + + def norm(self): + return self.m**2 * self.a + + def discriminant(self): + return self.D + + def gens(self): + return [self.m*self.a, self.m*(self.b % self.a)] + + def __mul__(self, other): + if isinstance(other, int) or isinstance(other, sage.rings.integer.Integer): + return Cideal(self.O, self.a, self.b, other*self.m) + + if isinstance(other, list) or isinstance(other, tuple) or isinstance(other, sage.modules.vector_integer_dense.Vector_integer_dense): + assert len(other) == 2 + + # We expect other to be in the [1, fw] basis! + + if other[1] == 0: + # In other words, we are multiplying by a + 0*f*w + return self.__mul__(other[0]) + + assert other in Cideal(self.O, 1, 0, 1) + + f, D = self.f, self.D + K = (D**2 - D)//4 + + a, b = self.a, self.b + c, d = other + + f, D = self.f, self.D + K = (D**2 - D)//4 + + # Generators of product written as rows in the basis [fw, 1] + # i.e. a + b*fw is written as [b, a] + # The hermite row reduced matrix with no zero rows will have the + # form + # [ C, B ] + # [ 0, A ] + # This is because Sage's hermite form does row reduction + G = Matrix(ZZ, [ + [a*d, a*c], + [c + b*d + d*D*f, c*b - d*f**2*K], + ]) + + G = G.hermite_form(include_zero_rows=False) + # print() + # print(G) + # print(f"Multiplying ideal I = [{a}, {b} + f*w] by ({c}, {d//f}*f*w)") + A = G[1, 1] + B = G[0, 1] + C = G[0, 0] + # print(G) + M = self.m + + # alpha = a*c + # beta = a*d + # gamma = c*b - d*f**2*K + # delta = b*d + c + d*D*f + + # C, u, v = xgcd(beta, delta) + # A = gcd(alpha*delta, beta*gamma)/C + # B = u*alpha + v*gamma + # M = self.m + + if C != 1: + if A % C != 0 or B % C != 0: + print("There has been an error in the representation of the ideal") + exit(148) + A = A//C + B = B//C + M = M*C + + assert G[1, 0] == 0 + + return Cideal(self.O, A, B, M) + + if isinstance(other, Cideal): + assert self.O == other.O + + a1, b1 = self.a, self.b + a2, b2 = other.a, other.b + f, D = self.f, self.D + K = (D**2 - D)//4 + + # Generators of product written as rows in the basis [fw, 1] + # i.e. a + b*fw is written as [b, a] + # This is because Sage's hermite form does row reduction + # (we want columns, so transposing the bases gives the "right" + # result) + G = Matrix(ZZ, [ + [0, a1*a2 ], + [a1, a1*b2 ], + [a2, a2*b1 ], + [b1 + b2 + f*D, b1*b2 - f**2*K], + ]) + + G = G.hermite_form(include_zero_rows=False) + + if G.dimensions() != (2, 2): + print("Ideal multiplication failed") + + C = G[0, 0] + B = G[0, 1] + A = G[1, 1] + M = self.m * other.m + + if C != 1: + if A % C != 0 or B % C != 0: + print("There has been an error in the representation of the ideal") + A = A//C + B = B//C + M = M*C + + return Cideal(self.O, A, B, M) + + __rmul__ = __mul__ + + + def __pow__(self, other): + if isinstance(other, int) or isinstance(other, sage.rings.integer.Integer): + if other == 0: + return Cideal(self.O, 1, 0, 1) + # Use double and add method + result = Cideal(self.O, 1, 0, 1) + power = self + while other >= 1: + # print(other) + if other %2 == 1: + result = result * power + other = other - 1 + power = power*power + other = other //2 + return result + + def __repr__(self): + # b_str = f"{self.b} + " if self.b != 0 else "" + # m_str = f"{self.m}*" if self.m != 1 else "" + # return f"{m_str}[{self.a}, {b_str}{self.f}*w]" + + # True repr + def _factor(n): return 0 if n == 0 else factor(n) + return f"Cideal(QuadraticField({self.D}).order_of_conductor({_factor(self.f)}), {_factor(self.a)}, {_factor(self.b)}, {_factor(self.m)})" + + def __shortrepr__(self): + b_str = f"{self.b} + " if self.b != 0 else "" + m_str = f"{self.m}*" if self.m != 1 else "" + return f"{m_str}[{self.a}, {b_str}{self.f}*w]" + + def __eq__(self, other): + return self.a == other.a \ + and self.b == other.b \ + and self.m == other.m \ + and self.O == other.O + + def __hash__(self): + return hash((self.O, self.a, self.b, self.m)) + + def __contains__(self, other): + + # Cast lists, tuples, vectors all as lists + other = list(other) + + if len(other) == 2: + # In the [1, fw] basis! + c, d = other + # So mathematically + # other = c + dfw + # This lies in O = m[a, b + fw] = [ma, m(b + fw)] if and only if + # c + dfw = mxa + myb + myfw + # for some integers x, y. In other words, + # (i) d = my for some integer y + # <=> d \in m\Z + # (ii) c = mxa + myb for some integer x (and same y from (i)) + # <=> c = mxa + db + # <=> c - db = mxa + # <=> c - db \in ma\Z + return (d % self.m == 0) and (c - self.b*d) % (self.m*self.a) == 0 + + def new_basis(self, other): + + # Cast lists, tuples, vectors all as lists + other = list(other) + + if len(other) == 2: + # In the [1, fw] basis! + c, d = other + + # So mathematically + # other = c + dfw + # + # this lies in m[a, b + fw] if and only if c and d are multiples + # of m and (c/m) + (d/m) fw lies in [a, b + fw] + + assert c % self.m == 0 and d % self.m == 0 + + c = c // self.m + d = d // self.m + + # Now we check that c + d fw lies in [a, b + fw] + # + # c + d fw = c + d (b + fw) - db = (c - db)/a * a + d (b + fw) + assert (c - self.b*d) % self.a == 0 + + alpha = (c - self.b*d) // self.a + beta = d + + print(f"{c} + {d} fw = {self.m} * [{alpha} * {self.a} + {beta} * ({self.b} + fw)]") + + def __floordiv__(self, other): + if isinstance(other, int) or isinstance(other, sage.rings.integer.Integer): + if self.m % other == 0: + return Cideal(self.O, self.a, self.b, self.m // other) + print("Could not divide exactly!") + exit(123) + + def conjugate(self): + return Cideal(self.O, self.a, -(self.b + self.f*self.D), self.m) + + def scalar_product(self, v, u): + # In the [1, fw] basis! + a, b = v + c, d = u + return ((2*a + b*self.f*self.D)*(2*c + d*self.f*self.D) - b*d*self.f**2*self.D)/4 + + def norm_sq(self, v): + x, y = v + return x**2 + x*y*self.f*self.D + y**2 * self.f**2 * (self.D**2-self.D)//4 + # return self.scalar_product(v, v) + + def reduced_basis(self): + + def scalar_product(v, w): return self.scalar_product(v, w) + def norm_sq(v): return self.norm_sq(v) + + def rround(n): + return -1*round(-n) if n < 0 else round(n) + + # Using Galbraith's Algorithm 23 to compute a minimal basis + # In the [1, fw_D] basis! + A = self.m*vector(ZZ, [self.a, 0]) + B = self.m*vector(ZZ, [self.b, 1]) + + b_1 = A + b_2 = B + + B_1 = norm_sq(b_1) + mu = scalar_product(b_1, b_2)/B_1 + b_2 = b_2 - rround(mu)*b_1 + B_2 = norm_sq(b_2) + + while B_2 < B_1: + b_1, b_2 = b_2, b_1 + B_1 = norm_sq(b_1) + mu = scalar_product(b_1, b_2) / B_1 + b_2 = b_2 - rround(mu)*b_1 + B_2 = norm_sq(b_2) + + # # Wikipedia's variant + # if norm_sq(b_1) <= norm_sq(b_2): + # b_1, b_2 = b_2, b_1 + + # # b_2 has the smaller norm here + # while norm_sq(b_2) <= norm_sq(b_1): + # # Get the projection factor of b_2 on b_1 + # mu = rround(scalar_product(b_2, b_1)/norm_sq(b_2)) + # r = b_1 - mu*b_2 + # b_1 = b_2 + # b_2 = r + + if b_1 not in self: + print(self.a, self.b, self.m) + print(b_1) + assert b_1 in self + + if b_2 not in self: + print(self.a, self.b, self.m) + print(b_2) + assert b_2 in self + + if b_1 != b_2: + # Some easy sanity checks + assert norm_sq(b_1) <= norm_sq(b_2) + assert norm_sq(b_1) <= norm_sq(A) + assert norm_sq(b_1) <= norm_sq(B) + assert norm_sq(b_1 - b_2) >= norm_sq(b_1) + assert norm_sq(b_1 + b_2) >= norm_sq(b_1) + + return b_1, b_2 + + def is_principal(self): + # if self.a == 1 and self.b == 0: + # # This is an integer multiple of the whole ring + # return True + + b_1, b_2 = self.reduced_basis() + + # print(f"Shortest vector in {self}: {b_1}, self.m = {self.m}") + # Now b_1 is the element of the lattice with the smallest norm + # Now we check whether this element generates the whole ring + + return self == b_1 * Cideal(self.O, 1, 0, 1) + + def is_equivalent(self, other): + return (self.conjugate()*other).is_principal() + + +def find_ideals(O, norm, equivalent_to=None): + f = O.conductor() + D = O.ambient().discriminant() + n_w_D = (D**2 - D) // 4 + for a in divisors(norm): + if Integer(norm//a).is_square(): + for b in range(a): + if (b**2 + b*f*D + f**2*n_w_D) % a == 0: + m = Integer(sqrt(norm//a)) + I = Cideal(O, a, b, m) + if not equivalent_to: + yield I + else: + if I.is_equivalent(equivalent_to): + yield I + + +def find_ideal(O, norm, equivalent_to=None): + ideals_iterator = find_ideals(O, norm, equivalent_to=equivalent_to) + return next(ideals_iterator) + + +def qfbsolve(a, b, c, enn, nonzero=False, not2power=False, flag=3): + if isinstance(enn, list): + enn = IntegerFactorization(enn) + # From Pari's documentation (Page 218) + # https://pari.math.u-bordeaux.fr/pub/pari/manuals/2.15.5/users.pdf + # The flag is a bitmask + # Bit 1: decides if all solutions (1) or only one solution (0) are returned + # Bit 2: decides if the returned solutions are primitive (0) or not (1) + # That is + # flag=0, Return only one primitive solution + # flag=1, Return all primitive solutions + # flag=2, Return only one (possibly imprimitive) solution + # flag=3, Return all primitive and imprimitive solutions + # + # qfbsolve returns objects of type + # <class 'cypari2.gen.Gen'> <class 'cypari2.gen.Gen'> + # ... + + if flag == 0 or flag == 2: + # Pari returns a single pair in this case (_not_ a 1 element list) + _solutions = pari.qfbsolve(pari.Qfb(a, b, c), enn, flag=flag) + if _solutions: + solutions = [_solutions] + else: + solutions = [] + else: + # Here pari returns a list of pairs + solutions = pari.qfbsolve(pari.Qfb(a, b, c), enn, flag=flag) + + # ... we want them as integers + + if b == 0: + # Some normalisation + solutions = [(abs(int(x)), abs(int(y))) for (x, y) in solutions] + else: + solutions = [(int(x), int(y)) for (x, y) in solutions] + + if nonzero: + solutions = [(x, y) for (x, y) in solutions if x != 0 and y != 0] + + if not2power: + solutions = [(x, y) for (x, y) in solutions if not is_power_of_two(x) and not is_power_of_two(y)] + + # Stupid dedupe + solutions = list(set(solutions)) + + return solutions + + +def has_torsion(E, n=2, card=False): + # Try to avoid counting points multiple times by calling abelian_group once + Eab = E.abelian_group() + invs = Eab.invariants() + if len(invs) == 2: + m1, m2 = invs + if card: + cardinality = Eab.cardinality() + return min(padicval(n, m1), padicval(n, m2)), cardinality + return min(padicval(n, m1), padicval(n, m2)) + else: + # E is cyclic + if card: + return 0, Eab.cardinality() + return 0 + + +def get_point(E, order): + while True: + P = E.random_point() + if P == E.zero: + continue + if P.order() % order == 0: + yield (P.order()//order)*P + + +def info(E, D=None, N=None, T=None, name=None, tries=None, indent=-1): + """Pretty prints information about Elliptic Curves""" + # N is the target torsion + # T is the actual torsion + # D is the discriminant (of End(E)) + + ret_str = [] + + C = E.cardinality() + p = E.base_field().characteristic() + q = E.base_field().cardinality() + k = padicval(p, q) + + if not name: + name = 'E' + + if N: + (n, l) = is_prime_power(N, get_data=True) + + q_str = f"{p}^{k}" if k != 1 else f"{p}" + + E_str = f"{name} = EllipticCurve(GF({q_str}), {E.a_invariants()})" + ret_str += [E_str] + + C_str = f"card({name}) = {factor(C)}" + ret_str += [C_str] + + if N: + if not T: + t = has_torsion(E, n) + else: + t = padicval(n, T) + + tors_str = f"{n}-Torsion: {n}^{t} (Target {n}^{l})" + ret_str += [tors_str] + + if tries: + ret_str += [f"Iterations to find {name}: {tries}"] + + if N and D: + v = int(sqrt(((q+1-C)**2 - 4*q)/D)) + v_val = padicval(-D, v) + two_val = padicval(2, v) + # if two_val < l: + # print("Found one!") + # exit(3) + v_str = f"v = {factor(v)}, Torsion losses: v_{-D}(v) = {v_val}, v_2(v) = {two_val}" + + # cls_no = Integer(D).class_number() + # ret_str += [f"{D = }, h(D) = {cls_no}"] + + ret_str += [v_str] + + if indent == -1: + return " | ".join(ret_str) + else: + return " "*indent + ("\n"+" "*indent).join(ret_str) + + +def find_E(D, N, cls_no=None, cls_poly=None, max_multiple=None, rand=False): + """Finds ordinary curve E whose N-torsion is rational + + Assumes N % 4 == 0 + + Since the N-torsion is a subgroup of order N^2, our general strategy is to + first find curves with cardinality m*N^2 and then check whether this + constitutes the N-torsion + + Passing cls_no and cls_poly is useful when finding many curves with a fixed + discriminant. + + Passing rand may be beneficial when the method gets "stuck" on smaller + cardinality attempts. + """ + + if not max_multiple: + max_multiple = N**4 + + if not cls_no: + # Must cast as sage integer for the class_number() method to exist + cls_no = Integer(D).class_number() + + if not cls_poly: + cls_poly = hilbert_class_polynomial(D) + + if rand: + # Generator expression for lazy sampling + # There is probably a better inbuilt solution for this + ms = (randint(1, max_multiple) for x in range(1, max_multiple)) + else: + ms = range(1, max_multiple) + + for m in ms: + # If the N-torsion is to be rational, the curve must have + # F_q-cardinality that is a multiple of N^2 + C = m*N**2 + + # Now searching for prime powers q that are of the form + # q = C + 1 \pm sqrt(4C + v^2 D) + # We do this by first finding v's so that 4C - v^2 D is a square + # In other words, + # x^2 = 4C + v^2 D <=> 4C = x^2 - v^2 D + for x, v in qfbsolve(1, 0, -D, 4*C): + for sign in [-1, 1]: + q = C + 1 + sign * x + # Verify that this is a prime power + (p, k) = is_prime_power(q, get_data=True) + if k == 0: + continue + + # Ensure the resulting curve is ordinary + if jacobi_symbol(D, p) != 1: + continue + + # Get roots over F_q (listed as pairs (root, multiplicity)) + for j, _ in cls_poly.roots(GF(q)): + E = EllipticCurve(GF(q), j=j) + + assert E.is_ordinary() + + M = E.cardinality() + + if M % N**2 != 0: + # Got the wrong twist! Choose the other... + # (assume j != 0, 1728) + E = E.quadratic_twist() + + if not has_torsion(E, N): + continue + + # Because there is a point of order 4, there should be a + # montgomery model for this curve + E = E.montgomery_model() + return E + + +def positive_bezout_coefficients(x, y, N, squares=False, allowzero=False): + """Compute all positive integer pairs (a, b) so that ax + by = N + + Assumes that x, y, N > 0 + """ + + if x == 0 and y == 0: + return () + + if y == 0: + if allowzero: + return ((N // x, 0)) if N % x == 0 else () + return () + + if x == 0: + if allowzero: + return ((0, N // y)) if N % y == 0 else () + return () + + (g, a, b) = xgcd(x, y) + + if N % g != 0: + return () + + _N = N // g + _y = y // g + _x = x // g + + coeffs = ((N*a + k*y, N*b - k*x) for k in range(ceil(float(-N*a/y)), floor(float(N*b/x))+1)) + + for A, B in coeffs: + + if squares and not is_square(A) and not is_square(B): + continue + + if not allowzero and (A == 0 or B == 0): + continue + + yield A, B + + +def compute_good_norms(O, T, prime_at_least=2): + f = O.conductor() + D = O.discriminant()//f**2 + + P = Primes() + good_primes = [] + p = P.first() # p = 2 + # Not allowing even numbers for now, getting next prime + p = P.next(p) # p = 3 + + while p < T: + if f % 2 == 0: + if kronecker_symbol(f**2*D//4, p) != -1: + good_primes += [p] + else: + if kronecker_symbol(f**2*D, 4*p) != -1: + good_primes += [p] + + p = P.next(p) + + print(f"There are {len(good_primes)} good primes to choose from") + # This could be done way, way faster using some "reverse sieving" technique + # For now, our numbers are small enough that it doesn't make a big + # difference + good_primes = set(good_primes) + good_norms = reverse_sieve(good_primes, T, prime_at_least=prime_at_least) + good_norms = sorted(good_norms) + print(f"This gives us {len(good_norms)} many possible norms") + + return good_norms + + +def get_interesting_ideals(O, T, at_most_ideals=0, prime_at_least=2): + """Return pairs of coprime integers (m, j) for which there exist (x, y) such + that m*x^2 + j*y^2 = T + """ + good_norms = compute_good_norms(O, T) + + ideal_combinations = [] + + for i, m in enumerate(good_norms): + if i % 50 == 0: + print(f"\rFinding an ideal for every possible norm up to {T}: {i/(len(good_norms))*100:.2f}% Done", end='') + + ideals_norm_m = list(find_ideals(O, m)) + + if len(ideals_norm_m) == 0: + print("No ideals found, even though {m = } was a good norm!") + continue + + for j in good_norms: + if j >= m: + break + + if gcd(m, j) != 1: + continue + + sols = qfbsolve(m, 0, j, T, nonzero=True, not2power=True) + + if not sols: + continue + + ideals_norm_j = list(find_ideals(O, j)) + + if len(ideals_norm_m) == 0: + print("No ideals found, even thought {j = } was a good norm!") + continue + + success = False + for I, J in product(ideals_norm_m, ideals_norm_j): + assert I.norm() == m and J.norm() == j + if I.is_equivalent(J): + success = True + break + + if not success: + continue + + print("Success!") + + # print(f"norm(I) = {I.norm()}, I = {I}") + # print(f"norm(J) = {J.norm()}, J = {J}") + # print(f"I.conjugate()*J = {I.conjugate()*J}") + # print(f"Are equivalent = {I.is_equivalent(J)}") + # print(f"Smallest elements of I.conjugate()*J = {(I.conjugate()*J).reduced_basis()}") + + for x, y in sols: + ideal_combinations += [(I, x, J, y)] + if at_most_ideals != 0 and len(ideal_combinations) >= at_most_ideals: + print() + return ideal_combinations + + print() + return ideal_combinations + + + +# def sample(a, b, c, D, f): + + +# def get_interesting_ideals(O, T, at_most_ideals=0, prime_at_least=1): +# """Return pairs of coprime integers (m, j) for which there exist (x, y) such +# that m*x^2 + j*y^2 = T +# """ +# f = O.conductor() +# D = O.discriminant()//f**2 + +# P = Primes() +# good_primes = [] +# p = P.first() # p = 2 +# # Not allowing even numbers for now, getting next prime +# p = P.next(p) # p = 3 + +# while p < T: +# if f % 2 == 0: +# if kronecker_symbol(f**2*D//4, p) != -1: +# good_primes += [p] +# else: +# if kronecker_symbol(f**2*D, 4*p) != -1: +# good_primes += [p] + +# p = P.next(p) + +# print(f"There are {len(good_primes)} good primes to choose from") +# good_primes = set(good_primes) +# good_primes = sorted(list(good_primes)) +# print(good_primes[-1]) +# good_norms = reverse_sieve(good_primes, 10*T, prime_at_least=prime_at_least) +# good_norms = sorted(good_norms) +# print(good_norms[-1]) +# exit(123) +# print(f"This gives us {len(good_norms)} many possible norms") + +# for _m in range(len(good_norms)): +# for _j in range(_m): +# m = good_norms[_m] +# j = good_norms[_j] + +# if not is_square(4*m*j + f**2*D): +# print(f"{4*m*j + f**2*D} is not a square") +# continue + +# # Since f is even, b is even +# b = int(sqrt(4*m*j + f**2*D)) + +# I = Cideal(O, a, -(b + f**2*D)//2, 1) +# J = Cideal(O, c, (b - f**2*D)//2, 1) + +# print("The ideals are equivalent:", I.is_equivalent(J)) +# print("{I.norm()}, {J.norm()}") +# assert I.is_equivalent(J) + +# ideal_combinations = [] + +# return ideal_combinations + + +def reverse_sieve(primes, bound, prime_at_least=1): + if prime_at_least >= bound: + print("Starting point is greater than the upper bound") + raise ValueError + # This is very dumb + # Could be done nicer with some constructive (recusive even) algorithm to + # multiply numbers together from the primes + # The hard part is to know when to terminate + return [m for m in range(prime_at_least, bound) if set([p for p, _ in factor(m)]).issubset(set(primes))] + + +def prod(it): + result = 1 + for i in it: + result = result * i + return result + + +def wD(E, D): + """Computing the isogeny corresponding to w_D, i.e. the generator of the + endomorphism ring with conductor f + """ + q = E.base_field().cardinality() + C = E.cardinality() + t = q + 1 - C + # Throws error if not coercable to int + v = ZZ(sqrt((t**2 - 4*q)/D)) + + assert (v*D - t) % 2 == 0 + assert gcd(v, D) == 1 + + deg_wD = (D**2 - D)//4 + + # Try smaller extensions first, before going to the full extension + for extension in [deg_wD - 1, deg_wD + 1, deg_wD**2 - 1]: + base_field = E.base_field() + irr_element = base_field['x'].irreducible_element(extension) + extension_field = base_field.extension(modulus=irr_element, names='v') + + R = E.change_ring(extension_field) + + pi_E = E.frobenius_endomorphism() + pi_R = R.frobenius_endomorphism() + + wD_kernel = [] + for P in R.zero().division_points(deg_wD): + if P.order() == deg_wD and (v*D - t)//2 * P + pi_R(P) == R.zero(): + wD_kernel = [P] + wD_R = R.isogeny(wD_kernel) + if wD_R.codomain().j_invariant() == E.j_invariant(): + break + + if len(wD_kernel) == 0: + continue + + wD_R = wD_R.codomain().isomorphism_to(R) * wD_R + + if wD_R.degree() != deg_wD: + continue + + # Cheap method to restrict wD_R to E + def wD(P): + # Assumes P lies in E + return E(wD_R(R(P))) + + # Ensure the map we created is the correct one. Mathematically + # wD = ((vD - t)/2 + \pi)/v + if not all([v*wD(P) == (v*D - t)//2 * P + pi_E(P) for P in [E.random_point() for _ in range(100)]]): + continue + + # w_D satisfies w_D^2 - D w_D + (D^2 - D)/4 = 0 + if not all([wD(wD(P)) - D * wD(P) + (D**2 - D)//4 * P == E.zero() for P in [E.random_point() for _ in range(100)]]): + continue + + return wD + + +def good_coprime_norm_pairs(n, D, f): + for norm_I in range(2, f**2 * (int(1 + sqrt(-D)))): + if norm_I % D == 0 or norm_I % f == 0: + continue + for norm_J in range(2, min(norm_I, 2**n - norm_I)): + if norm_J % D == 0 or norm_J % f == 0: + continue + if gcd(norm_I, norm_J) != 1: + continue + + # If + # norm_I*a^2 + norm_J*b^2 = 2^m for some m <= n with m + 2k = n + # then + # norm_I*(2^k a)^2 + norm_J*(2^k b)^2 = 2^m * 2^(2k) = 2^n + # Conversely + # norm_I*a^2 + norm_J*b^2 = 2^m for some m <= n with m + 2k + 1 = n + # then + # norm_I*(2^k a)^2 + norm_J*(2^k b)^2 = 2^m * 2^(2k) = 2^{n - 1} + # + # So by finding a solution for + # norm_I*a^2 + norm_J*b^2 = 2^n + # or + # norm_I*a^2 + norm_J*b^2 = 2^{n-1} + # we have verified that there exists a solution to (*) + + # Use flag=2 to return after finding one solution + # We only care that there is one solution + + sols = qfbsolve(norm_I, 0, norm_J, [(2, n)], flag=2) + + if not sols: + sols = qfbsolve(norm_I, 0, norm_J, [(2, n - 1)], flag=2) + + if sols: + a, b = sols[0] + yield norm_I, a, norm_J, b + + +def find_class_group(d, f): + D = d if d % 4 == 1 else 4 * d + K = QuadraticField(D, names="i") + O = K.order_of_conductor(f) + (_f, e_f) = is_prime_power(f, get_data=True) + class_number = f - (f // _f) * kronecker_symbol(D, _f) + assert class_number == Integer(D * f**2).class_number() + + # int() rounds down, which is what we want here + maxnorm = f * int(sqrt(-D)) + class_group = [Cideal(O, 1, 0, 1)] + for norm in range(1, maxnorm): + for I in find_ideals(O, norm): + # For simplicity, only find ideals with m = 1 + # (Means coprimality conditions are easier to meet later on) + if I.m > 1: + continue + + alpha = I.a + beta = 2 * I.b + I.f * I.D + gamma = (I.b**2 + I.b * I.f * I.D + I.f**2 * (I.D**2 - I.D) // 4) // I.a + + if gcd([alpha, beta, gamma]) > 1: + continue + + if not any([I.is_equivalent(_) for _ in class_group]): + class_group += [I] + print(f"Found ideal: {len(class_group)}/{class_number} norm = {I.norm()} (maxnorm: {maxnorm})") + if len(class_group) >= class_number: + break + if len(class_group) >= class_number: + break + if len(class_group) >= class_number: + break + + assert len(class_group) == class_number + + print(f"Computed class group of size {class_number}") + return class_group + + +def roundrobin(*iterables): + # https://docs.python.org/3/library/itertools.html#recipes + "Visit input iterables in a cycle until each is exhausted." + # roundrobin('ABC', 'D', 'EF') → A D E B F C + # Algorithm credited to George Sakkis + iterators = map(iter, iterables) + for num_active in range(len(iterables), 0, -1): + iterators = cycle(islice(iterators, num_active)) + yield from map(next, iterators) + + +def find_pairs_squares(d, n, f): + D = d if d % 4 == 1 else 4*d + K = QuadraticField(D, names='i') + (i, ) = K._first_ngens(1) + O = K.order_of_conductor(f) + + found = [] + count_pairs = 0 + (_f, e_f) = is_prime_power(f, get_data=True) + class_number = f - (f//_f)*kronecker_symbol(D, _f) + + for norm_I, a, norm_J, b in good_coprime_norm_pairs(n, D, f): + count_pairs += 1 + if len(found) >= class_number: + return found + + # Iter over the ideals with norm_I and find J with norm(J) = norm_J + # Since norm(m[a, b + fw]) = m^2a == norm_I, there are only + # finitely many such ideals + for I in find_ideals(O, norm_I): + # We are only interested in finding classes of ideals + if any([I.is_equivalent(_) for _ in found]): + continue + + alpha = I.a + beta = 2*I.b + f*D + gamma = (I.b**2 + I.b*I.f*I.D + I.f**2*(I.D**2 - I.D)//4) // I.a + + # This returns the coefficients of beta, if they exist + # We only care if there is one solution (flag=2) + equiv_sol = qfbsolve(alpha, beta, gamma, norm_J, flag=2) + + if equiv_sol: + (x, y) = equiv_sol[0] + + # Recall that conjugate(w) = D - w. Hence + # conjugate(x + yfw) = x + yfD - yfw + # so in the [1, fw] basis we get + # conjugate([x, y]) = [x + yfD, -y] + beta = I.m*vector(ZZ, [x*I.a + y*I.b, y]) + beta_conj = vector(ZZ, [beta[0] + beta[1]*I.f*I.D, -beta[1]]) + J = (beta_conj*I) // I.norm() + + # I or J, doesn't matter. We are interested in the class + found += [I] + + kani_endo = (J.conjugate()*I).reduced_basis()[0] + vstring = [] + vstring += [f"(v1)"] + vstring += [f"Norm pairs tried: {count_pairs:>10}"] + vstring += [f"Distinct classes: {len(found)}/{class_number}"] + vstring += [f"2^{padicval(2, a**2*norm_I + b**2*norm_J)} == {a}^2*{norm_I} + {b}^2*{norm_J}"] + vstring += [f"kani_endo == {kani_endo[0]} + {kani_endo[1]} fw"] + print(" | ".join(vstring)) + + print("Could not find pairs for the whole class group") + return found + +def find_pairs_full(class_group, d, n, f): + + D = d if d % 4 == 1 else 4 * d + + found = [] + for n_H, H in enumerate(class_group): + tries = 0 + + # Norm form of equivalent ideals to H + alpha = H.a + beta = 2 * H.b + H.f * H.D + gamma = (H.b**2 + H.b * H.f * H.D + H.f**2 * (H.D**2 - H.D) // 4) // H.a + + # Should already be true, because of how the class group was computed + assert gcd([alpha, beta, gamma]) == 1 + + # Norm form of O = [1, fw] + delta = 1 + epsilon = f * D + phi = f**2 * (D**2 - D) // 4 + + finish = False + + while True: + + if finish: + break + + upper_bound = 2 ** (n // 6) + + x_I = randint(-upper_bound, upper_bound) + y_I = randint(-upper_bound, upper_bound) + norm_I = alpha * x_I**2 + beta * x_I * y_I + gamma * y_I**2 + + x_J = randint(-upper_bound, upper_bound) + y_J = randint(-upper_bound, upper_bound) + norm_J = alpha * x_J**2 + beta * x_J * y_J + gamma * y_J**2 + + if gcd(norm_I, norm_J) > 1: + continue + + tries += 1 + # print(f"{tries} Coprime pair") + + if tries > 2**12: + # Don't get stuck in infinite loop + print(f"Class {n_H+1:>2}/{len(class_group)} ({len(found)/(n_H+1):.1%}): Giving up...") + finish = True + break + + bezout_coefficients = 0 + for a, b in roundrobin( + positive_bezout_coefficients(norm_I, norm_J, 2 ** (n - 1)), + positive_bezout_coefficients(norm_I, norm_J, 2**n), + ): + bezout_coefficients += 1 + + # g = gcd(a, b) + # a, b = a//gcd(a, b), b//gcd(a, b) + + if finish: + break + + if bezout_coefficients > 2**10: + break + + if gcd(a*norm_I, b*norm_J) > 1: + continue + + if is_square(a): + end_a = (int(sqrt(a)), 0) + end_a_str = f"{end_a[0]}^2" + elif sols := qfbsolve(delta, epsilon, phi, a, flag=2): + end_a = sols[0] + end_a_str = f"deg({end_a[0]} + {end_a[1]} fw)" + else: + continue + + # Assert that the degree of the endmorphism is correct + assert delta*end_a[0]**2 + epsilon*end_a[0]*end_a[1] + phi*end_a[1]**2 == a + + if is_square(b): + end_b = (int(sqrt(b)), 0) + end_b_str = f"{int(sqrt(b))}^2" + elif sols := qfbsolve(delta, epsilon, phi, b, flag=2): + end_b = sols[0] + end_b_str = f"deg({end_b[0]} + {end_b[1]} fw)" + else: + continue + + # Assert that the degree of the endmorphism is correct + assert delta*end_b[0]**2 + epsilon*end_b[0]*end_b[1] + phi*end_b[1]**2 == b + + finish = True + + # Recover the ideals + beta_I_conj = H.m * vector( + ZZ, [x_I * H.a + y_I * (H.b + H.f * H.D), -y_I] + ) + I = beta_I_conj * H + I = I // H.norm() + + assert norm_I == I.norm() + + # beta_J = H.m*vector(ZZ, [x_J*H.a + y_J*H.b, y_J]) + beta_J_conj = H.m * vector( + ZZ, [x_J * H.a + y_J * (H.b + H.f * H.D), -y_J] + ) + J = beta_J_conj * H + J = J // H.norm() + + assert norm_J == J.norm() + + # assert gcd(a * norm_I, b * norm_J) == 1 + assert I.is_equivalent(H) + assert J.is_equivalent(H) + assert I.is_equivalent(J) + assert J.is_equivalent(I) + + print() + + kani = (J.conjugate() * I).reduced_basis()[0] + + assert delta*kani[0]**2 + epsilon*kani[0]*kani[1] + phi*kani[1]**2 == J.norm()*I.norm() + + found += [(H, a, I, b, J, end_a, end_b, kani)] + + verb_str = [] + verb_str += [f"Class {n_H+1:>2}/{len(class_group)}"] + verb_str += [f"({len(found)/(n_H+1):.1%})"] + verb_str += [f"(tries: {tries:>4}, b_coefs: {bezout_coefficients:>4})"] + # Class group computed so that H.m = 1 + verb_str += [f"Found for class [H] = [{H.a}, {H.b} + fw]"] + verb_str += [f"{norm_I}*{end_a_str} + {norm_J}*{end_b_str} = 2^{padicval(2, norm_I*a + norm_J*b)}"] + verb_str += [f"(Kani end = {kani[0]} + {kani[1]} fw)"] + print(" ".join(verb_str)) + + return found + + +def volcano_walk(d, n, _f, e_f): + # # Chosen parameters + # # Discriminant of the Elliptic curve we find + # d = -7 + # # Available 2 torsion on middle curve + # n = 32 + # # Conductor of endomorphism ring of middle curve + # _f = 3 + # e_f = 5 + + # Derived parameters + # Discriminant, called \Delta_D in the thesis + D = d if d % 4 == 1 else 4*d + K = QuadraticField(D, names='i') + (i, ) = K._first_ngens(1) + f = _f**e_f + N = f*2**n + + # print(f"Pre-computing class polynomial and class number of {D}...") + cls_no = Integer(D).class_number(proof=False) + cls_poly = hilbert_class_polynomial(D) + # print("Done") + # print() + + sim_cls_no = f - (f//_f)*kronecker_symbol(D, _f) + + cases = {1: 'Elkies', -1: 'Atikin', 0: 'Ramified'} + + print("Setup:") + print(f" {d = }, ({log(-d, 2):.2f} bits)") + print(f" {D = }, Discriminant of {K = }") + print(f" h(D) = {cls_no} ({log(cls_no, 2):.2f} bits)") + print(f" h(f**2D) = {sim_cls_no} ({log(sim_cls_no, 2):.2f} bits)") + print(f" N = {factor(N)}, Target rational torsion of curve") + print(f" In the {cases[kronecker_symbol(D, 2)]} case") + print() + + print("Looking for curve...") + print() + result = find_E(D, N, cls_no=cls_no, cls_poly=cls_poly) + if result: + E = result + print(info(E)) + else: + print() + print("No curve found!") + exit(1) + + q = E.base_field().cardinality() + p, k = q.is_prime_power(get_data=True) + q_str = f"{p}^{k}" if k != 1 else f"{p}" + + assert has_torsion(E, 2) >= n + assert has_torsion(E, _f) >= e_f + + # Walk down the _f-isogeny volcano to the floor. + + P, Q = E.torsion_basis(f) + _P, _Q = (_f)**(e_f-1)*P, (_f)**(e_f-1)*Q + + one_away_success = False + for kernel in [_P + k*_Q for kernel in range(_f)] + [_Q]: + if E.isogeny(kernel).codomain().j_invariant() != E.j_invariant(): + one_away_success = True + correct_torsion_point = P + k*Q + break + + assert one_away_success + + phi = E.isogeny(correct_torsion_point, algorithm='factored') + F = phi.codomain() + + assert has_torsion(F, _f) == 0 + + assert E == phi.domain() + assert F == phi.codomain() + + print(f"Found curve with conductor {f = } and rational 2^{has_torsion(F, 2)}-torsion") + print(f"F = EllipticCurve(GF({q_str}), {F.a_invariants()})") + + assert has_torsion(F, 2) >= n + + return E, F, phi |