def connecting_ideal(O_1, O_2): """Returns an O_1, O_2-connecting ideal. Args: O_1: A maximal order in a rational quaternion algebra. O_2: A maximal order in the same quaternion algebra. Returns: An ideal I that is a left O_1 ideal and a right O_2 ideal. Moreover I is a subset of O_1. """ # There exists some integer d such that d*O_2 is a subset of O_1. We # first compute d. mat_1 = matrix([x.coefficient_tuple() for x in O_1.basis()]) mat_2 = matrix([x.coefficient_tuple() for x in O_2.basis()]) matcoeff = mat_2 * ~mat_1 d = lcm(x.denominator() for x in matcoeff.coefficients()) # J is a subset of O_1 and is a O_1, O_2-ideal by construction. J = left_ideal([d * x * y for x in O_1.basis() for y in O_2.basis()], O_1) assert J.left_order() == O_1 assert J.right_order() == O_2 assert all(x in O_1 for x in J.basis()) return J
def __div__(self, other): r""" Divide modular forms, rescaling if necessary. """ if isinstance(other, OrthogonalModularForm): if not self.gram_matrix() == other.gram_matrix(): raise ValueError('Incompatible Gram matrices') self_scale = self.scale() other_scale = other.scale() if self_scale != 1 or other_scale != 1: new_scale = lcm(self.scale(), other.scale()) X1 = self.rescale(new_scale // self_scale) X2 = other.rescale(new_scale // other_scale) return OrthogonalModularForm( self.__weight - other.weight(), self.__weilrep, X1.true_fourier_expansion() * X2.inverse(), scale=new_scale, weylvec=self.weyl_vector() - other.weyl_vector(), qexp_representation=self.qexp_representation()) return OrthogonalModularForm( self.weight() - other.weight(), self.__weilrep, self.true_fourier_expansion() * other.inverse(), scale=1, weylvec=self.weyl_vector() - other.weyl_vector(), qexp_representation=self.qexp_representation()) else: return OrthogonalModularForm( self.weight(), self.__weilrep, self.true_fourier_expansion() / other, scale=self.scale(), weylvec=self.weyl_vector(), qexp_representation=self.qexp_representation())
def __sub__(self, other): r""" Subtract modular forms, rescaling if necessary. """ if not other: return self if not self.gram_matrix() == other.gram_matrix(): raise ValueError('Incompatible Gram matrices') if not self.weight() == other.weight(): raise ValueError('Incompatible weights') self_v = self.weyl_vector() other_v = other.weyl_vector() if self_v or other_v: if not denominator(self_v - other_v) == 1: raise ValueError('Incompatible characters') self_scale = self.scale() other_scale = other.scale() if not self_scale == other_scale: new_scale = lcm(self_scale, other_scale) X1 = self.rescale(new_scale // self_scale) X2 = other.rescale(new_scale // other_scale) return OrthogonalModularForm( self.__weight, self.__weilrep, X1.true_fourier_expansion() - X2.true_fourier_expansion(), scale=new_scale, weylvec=self_v, qexp_representation=self.qexp_representation()) return OrthogonalModularForm( self.__weight, self.__weilrep, self.true_fourier_expansion() - other.true_fourier_expansion(), scale=self_scale, weylvec=self_v, qexp_representation=self.qexp_representation())
def __eq__(self, other): self_scale = self.scale() other_scale = other.scale() if self_scale == other_scale: return self.true_fourier_expansion( ) == other.true_fourier_expansion() else: new_scale = lcm(self_scale, other_scale) X1 = self.rescale(new_scale // self_scale) X2 = other.rescale(new_scale // other_scale) return X1.true_fourier_expansion() == X2.true_fourier_expansion()
def _numden(self, t, start=0, shift=0, offset=0, verbose=False): r""" Return f and g, such that P_{m+1} = f(k) / g(k) P_m where P_m = t^m \prod_i (alpha)_m / (beta)_m, shift mod p = floor(start*(p-1)) + offset, m = floor(start*(p-1)) + k, and 1 <= k < floor(end*(p-1)) - floor(start*(p-1)). EXAMPLES:: sage: H = AmortizingHypergeometricData(100, alpha_beta=([1/6,5/6],[0,0])) sage: start, end, p = 0, 1/6, 97 sage: shift, offset = H._starts_to_rationals[start][p % start.denominator()] sage: f, g = H._numden(t=1, shift=shift, offset=offset, start=start) sage: f 36*k^2 + 36*k + 5 sage: g 36*k^2 + 72*k + 36 TESTS:: sage: for cyca, cycb, start, end, p, t in [ ....: ([6], [1, 1], 0, 1/6, 97, 1), ....: ([4, 2, 2], [3, 1, 1], 1/3, 1/2, 97, 1), ....: ([22], [1, 1, 20], 3/20, 5/22, 1087, 1), ....: ([22], [1, 1, 20], 3/20, 5/22, 1087, 1337/507734), ....: ([22], [1, 1, 20], 3/20, 5/22, 1019, 1337/507734)]: ....: H = AmortizingHypergeometricData(p+40, cyclotomic=(cyca, cycb)) ....: shift, offset = H._starts_to_rationals[start][p % start.denominator()] ....: f, g = H._numden(t=t, shift=shift, offset=offset, start=start) ....: for k in range(1, floor(end * (p-1)) - floor(start * (p-1))): ....: m = floor(start * (p-1)) + k ....: quoval = GF(p)(t) * H.pochhammer_quotient(p, m+1)/H.pochhammer_quotient(p, m) ....: if GF(p)(f(k)/g(k)) != quoval: ....: print((cyca, cycb), offset, (start, end), (floor(start * (p-1)), floor(end * (p-1))), m, p, t, GF(p)(f(k)/g(k)), quoval) """ RQ = QQ['k'] RZ = ZZ['k'] k = RQ.gen() - offset # We shift the term corresponding to a or b by 1 because we're taking # the fractional part of a negative number when less than start. f = prod(a + shift + k + (1 if a <= start else 0) for a in self.alpha()) * t.numerator() g = prod(b + shift + k + (1 if b <= start else 0) for b in self.beta()) * t.denominator() d = lcm(f.denominator(), g.denominator()) return RZ(d * f), RZ(d * g)
def perm_order(p, n=None): r""" Return the multiplicative order of the permutation ``p``. EXAMPLES:: sage: from surface_dynamics.misc.permutation import perm_init, perm_order sage: p = perm_init('(1,3)(2,4,6)(5)') sage: perm_order(p) 6 """ return lcm(perm_cycle_type(p))
def __mul__(self, other): r""" Multiply modular forms, rescaling if necessary. """ if isinstance(other, OrthogonalModularForm): if not self.gram_matrix() == other.gram_matrix(): raise ValueError('Incompatible Gram matrices') self_scale = self.scale() other_scale = other.scale() if self_scale != 1 or other_scale != 1: new_scale = lcm(self.scale(), other.scale()) X1 = self.rescale(new_scale // self_scale) X2 = other.rescale(new_scale // other_scale) else: new_scale = 1 X1 = self X2 = other f1 = X1.true_fourier_expansion() f2 = X2.true_fourier_expansion() f = f1 * f2 if f1.valuation() < 0 or f2.valuation() < 0 and f.valuation >= 0: r = PowerSeriesRing(f.base_ring(), 't') f = r(f) return OrthogonalModularForm( self.__weight + other.weight(), self.__weilrep, f, scale=new_scale, weylvec=self.weyl_vector() + other.weyl_vector(), qexp_representation=self.qexp_representation()) else: return OrthogonalModularForm( self.weight(), self.__weilrep, self.true_fourier_expansion() * other, scale=self.scale(), weylvec=self.weyl_vector(), qexp_representation=self.qexp_representation())
def is_Q_curve(E, maxp=100, certificate=False, verbose=False): r""" Return whether ``E`` is a `\QQ`-curve, with optional certificate. INPUT: - ``E`` (elliptic curve) -- an elliptic curve over a number field. - ``maxp`` (int, default 100): bound on primes used for checking necessary local conditions. The result will not depend on this, but using a larger value may return ``False`` faster. - ``certificate`` (bool, default ``False``): if ``True`` then a second value is returned giving a certificate for the `\QQ`-curve property. OUTPUT: If ``certificate`` is ``False``: either ``True`` (if `E` is a `\QQ`-curve), or ``False``. If ``certificate`` is ``True``: a tuple consisting of a boolean flag as before and a certificate, defined as follows: - when the flag is ``True``, so `E` is a `\QQ`-curve: - either {'CM':`D`} where `D` is a negative discriminant, when `E` has potential CM with discriminant `D`; - otherwise {'CM': `0`, 'core_poly': `f`, 'rho': `\rho`, 'r': `r`, 'N': `N`}, when `E` is a non-CM `\QQ`-curve, where the core polynomial `f` is an irreducible monic polynomial over `QQ` of degree `2^\rho`, all of whose roots are `j`-invariants of curves isogenous to `E`, the core level `N` is a square-free integer with `r` prime factors which is the LCM of the degrees of the isogenies between these conjugates. For example, if there exists a curve `E'` isogenous to `E` with `j(E')=j\in\QQ`, then the certificate is {'CM':0, 'r':0, 'rho':0, 'core_poly': x-j, 'N':1}. - when the flag is ``False``, so `E` is not a `\QQ`-curve, the certificate is a prime `p` such that the reductions of `E` at the primes dividing `p` are inconsistent with the property of being a `\QQ`-curve. See the ALGORITHM section for details. ALGORITHM: See [CrNa2020]_ for details. 1. If `E` has rational `j`-invariant, or has CM, then return ``True``. 2. Replace `E` by a curve defined over `K=\QQ(j(E))`. Let `N` be the conductor norm. 3. For all primes `p\mid N` check that the valuations of `j` at all `P\mid p` are either all negative or all non-negative; if not, return ``False``. 4. For `p\le maxp`, `p\not\mid N`, check that either `E` is ordinary mod `P` for all `P\mid p`, or `E` is supersingular mod `P` for all `P\mid p`; if neither, return ``False``. If all are ordinary, check that the integers `a_P(E)^2-4N(P)` have the same square-free part; if not, return ``False``. 5. Compute the `K`-isogeny class of `E` using the "heuristic" option (which is faster, but not guaranteed to be complete). Check whether the set of `j`-invariants of curves in the class of `2`-power degree contains a complete Galois orbit. If so, return ``True``. 6. Otherwise repeat step 4 for more primes, and if still undecided, repeat Step 5 without the "heuristic" option, to get the complete `K`-isogeny class (which will probably be no bigger than before). Now return ``True`` if the set of `j`-invariants of curves in the class contains a complete Galois orbit, otherwise return ``False``. EXAMPLES: A non-CM curve over `\QQ` and a CM curve over `\QQ` are both trivially `\QQ`-curves:: sage: from sage.schemes.elliptic_curves.Qcurves import is_Q_curve sage: E = EllipticCurve([1,2,3,4,5]) sage: flag, cert = is_Q_curve(E, certificate=True) sage: flag True sage: cert {'CM': 0, 'N': 1, 'core_poly': x, 'r': 0, 'rho': 0} sage: E = EllipticCurve(j=8000) sage: flag, cert = is_Q_curve(E, certificate=True) sage: flag True sage: cert {'CM': -8} A non-`\QQ`-curve over a quartic field. The local data at bad primes above `3` is inconsistent:: sage: from sage.schemes.elliptic_curves.Qcurves import is_Q_curve sage: R.<x> = PolynomialRing(QQ) sage: K.<a> = NumberField(R([3, 0, -5, 0, 1])) sage: E = EllipticCurve([K([-3,-4,1,1]),K([4,-1,-1,0]),K([-2,0,1,0]),K([-621,778,138,-178]),K([9509,2046,-24728,10380])]) sage: is_Q_curve(E, certificate=True, verbose=True) Checking whether Elliptic Curve defined by y^2 + (a^3+a^2-4*a-3)*x*y + (a^2-2)*y = x^3 + (-a^2-a+4)*x^2 + (-178*a^3+138*a^2+778*a-621)*x + (10380*a^3-24728*a^2+2046*a+9509) over Number Field in a with defining polynomial x^4 - 5*x^2 + 3 is a Q-curve No: inconsistency at the 2 primes dividing 3 - potentially multiplicative: [True, False] (False, 3) A non-`\QQ`-curve over a quadratic field. The local data at bad primes is consistent, but the local test at good primes above `13` is not:: sage: K.<a> = NumberField(R([-10, 0, 1])) sage: E = EllipticCurve([K([0,1]),K([-1,-1]),K([0,0]),K([-236,40]),K([-1840,464])]) sage: is_Q_curve(E, certificate=True, verbose=True) Checking whether Elliptic Curve defined by y^2 + a*x*y = x^3 + (-a-1)*x^2 + (40*a-236)*x + (464*a-1840) over Number Field in a with defining polynomial x^2 - 10 is a Q-curve Applying local tests at good primes above p<=100 No: inconsistency at the 2 ordinary primes dividing 13 - Frobenius discriminants mod squares: [-1, -3] No: local test at p=13 failed (False, 13) A quadratic `\QQ`-curve with CM discriminant `-15` (`j`-invariant not in `\QQ`):: sage: from sage.schemes.elliptic_curves.Qcurves import is_Q_curve sage: R.<x> = PolynomialRing(QQ) sage: K.<a> = NumberField(R([-1, -1, 1])) sage: E = EllipticCurve([K([1,0]),K([-1,0]),K([0,1]),K([0,-2]),K([0,1])]) sage: is_Q_curve(E, certificate=True, verbose=True) Checking whether Elliptic Curve defined by y^2 + x*y + a*y = x^3 + (-1)*x^2 + (-2*a)*x + a over Number Field in a with defining polynomial x^2 - x - 1 is a Q-curve Yes: E is CM (discriminant -15) (True, {'CM': -15}) An example over `\QQ(\sqrt{2},\sqrt{3})`. The `j`-invariant is in `\QQ(\sqrt{6})`, so computations will be done over that field, and in fact there is an isogenous curve with rational `j`, so we have a so-called rational `\QQ`-curve:: sage: K.<a> = NumberField(R([1, 0, -4, 0, 1])) sage: E = EllipticCurve([K([-2,-4,1,1]),K([0,1,0,0]),K([0,1,0,0]),K([-4780,9170,1265,-2463]),K([163923,-316598,-43876,84852])]) sage: flag, cert = is_Q_curve(E, certificate=True) sage: flag True sage: cert {'CM': 0, 'N': 1, 'core_degs': [1], 'core_poly': x - 85184/3, 'r': 0, 'rho': 0} Over the same field, a so-called strict `\QQ`-curve which is not isogenous to one with rational `j`, but whose core field is quadratic. In fact the isogeny class over `K` consists of `6` curves, four with conjugate quartic `j`-invariants and `2` with quadratic conjugate `j`-invariants in `\QQ(\sqrt{3})` (but which are not base-changes from the quadratic subfield):: sage: E = EllipticCurve([K([0,-3,0,1]),K([1,4,0,-1]),K([0,0,0,0]),K([-2,-16,0,4]),K([-19,-32,4,8])]) sage: flag, cert = is_Q_curve(E, certificate=True) sage: flag True sage: cert {'CM': 0, 'N': 2, 'core_degs': [1, 2], 'core_poly': x^2 - 840064*x + 1593413632, 'r': 1, 'rho': 1} """ from sage.rings.number_field.number_field_base import is_NumberField if verbose: print("Checking whether {} is a Q-curve".format(E)) try: assert is_NumberField(E.base_field()) except (AttributeError, AssertionError): raise TypeError( "{} must be an elliptic curve defined over a number field in is_Q_curve()" ) from sage.rings.integer_ring import ZZ from sage.arith.functions import lcm from sage.libs.pari import pari from sage.rings.number_field.number_field import NumberField from sage.schemes.elliptic_curves.constructor import EllipticCurve from sage.schemes.elliptic_curves.cm import cm_j_invariants_and_orders, is_cm_j_invariant # Step 1 # all curves with rational j-invariant are Q-curves: jE = E.j_invariant() if jE in QQ: if verbose: print("Yes: j(E) is in QQ") if certificate: # test for CM for d, f, j in cm_j_invariants_and_orders(QQ): if jE == j: return True, {'CM': d * f**2} # else not CM return True, { 'CM': ZZ(0), 'r': ZZ(0), 'rho': ZZ(0), 'N': ZZ(1), 'core_poly': polygen(QQ) } else: return True # CM curves are Q-curves: flag, df = is_cm_j_invariant(jE) if flag: d, f = df D = d * f**2 if verbose: print("Yes: E is CM (discriminant {})".format(D)) if certificate: return True, {'CM': D} else: return True # Step 2: replace E by a curve defined over Q(j(E)): K = E.base_field() jpoly = jE.minpoly() if jpoly.degree() < K.degree(): if verbose: print("switching to smaller base field: j's minpoly is {}".format( jpoly)) f = pari(jpoly).polredbest().sage({'x': jpoly.parent().gen()}) K2 = NumberField(f, 'b') jE = jpoly.roots(K2)[0][0] if verbose: print("New j is {} over {}, with minpoly {}".format( jE, K2, jE.minpoly())) #assert jE.minpoly()==jpoly E = EllipticCurve(j=jE) K = K2 if verbose: print("New test curve is {}".format(E)) # Step 3: check primes of bad reduction NN = E.conductor().norm() for p in NN.support(): Plist = K.primes_above(p) if len(Plist) < 2: continue # pot_mult = potential multiplicative reduction pot_mult = [jE.valuation(P) < 0 for P in Plist] consistent = all(pot_mult) or not any(pot_mult) if not consistent: if verbose: print("No: inconsistency at the {} primes dividing {}".format( len(Plist), p)) print(" - potentially multiplicative: {}".format(pot_mult)) if certificate: return False, p else: return False # Step 4 check: primes P of good reduction above p<=B: if verbose: print("Applying local tests at good primes above p<={}".format(maxp)) res4, p = Step4Test(E, B=maxp, oldB=0, verbose=verbose) if not res4: if verbose: print("No: local test at p={} failed".format(p)) if certificate: return False, p else: return False if verbose: print("...all local tests pass for p<={}".format(maxp)) # Step 5: compute the (partial) K-isogeny class of E and test the # set of j-invariants in the class: C = E.isogeny_class(algorithm='heuristic', minimal_models=False) jC = [E2.j_invariant() for E2 in C] centrejpols = conjugacy_test(jC, verbose=verbose) if centrejpols: if verbose: print( "Yes: the isogeny class contains a complete conjugacy class of j-invariants" ) if certificate: for f in centrejpols: rho = f.degree().valuation(2) centre_indices = [i for i, j in enumerate(jC) if f(j) == 0] M = C.matrix() core_degs = [M[centre_indices[0], i] for i in centre_indices] level = lcm(core_degs) if level.is_squarefree(): r = len(level.prime_divisors()) cert = { 'CM': ZZ(0), 'core_poly': f, 'rho': rho, 'r': r, 'N': level, 'core_degs': core_degs } return True, cert print("No central curve found") else: return True # Now we are undecided. This can happen if either (1) E is not a # Q-curve but we did not use enough primes in Step 4 to detect # this, or (2) E is a Q-curve but in Step 5 we did not compute the # complete isogeny class. Case (2) is most unlikely since the # heuristic bound used in computing isogeny classes means that we # have all isogenous curves linked to E by an isogeny of degree # supported on primes<1000. # We first rerun Step 4 with a larger bound. xmaxp = 10 * maxp if verbose: print( "Undecided after first round, so we apply more local tests, up to {}" .format(xmaxp)) res4, p = Step4Test(E, B=xmaxp, oldB=maxp, verbose=verbose) if not res4: if verbose: print("No: local test at p={} failed".format(p)) if certificate: return False, p else: return False # Now we rerun Step 5 using a rigorous computation of the complete # isogeny class. This will probably contain no more curves than # before, in which case -- since we already tested that the set of # j-invariants does not contain a complete Galois conjugacy class # -- we can deduce that E is not a Q-curve. if verbose: print("...all local tests pass for p<={}".format(xmaxp)) print("We now compute the complete isogeny class...") Cfull = E.isogeny_class(minimal_models=False) jCfull = [E2.j_invariant() for E2 in Cfull] if len(jC) == len(jCfull): if verbose: print("...and find that we already had the complete class:so No") if certificate: return False, 0 else: return False if verbose: print( "...and find that the class contains {} curves, not just the {} we computed originally" .format(len(jCfull), len(jC))) centrejpols = conjugacy_test(jCfull, verbose=verbose) if cert: if verbose: print( "Yes: the isogeny class contains a complete conjugacy class of j-invariants" ) if certificate: return True, centrejpols else: return True if verbose: print( "No: the isogeny class does *not* contain a complete conjugacy class of j-invariants" ) if certificate: return False, 0 else: return False
def matrix_representation(K, F, v): r""" Return a matrix representation of a given list of elements. INPUT: - ``K`` -- a field - ``F`` -- a finitely generated field extension of `K` - ``v`` -- a vector over `F`, or a list with entries in `F` OUTPUT: a pair `(A, w)`, where `A` is a matrix with entries in `K` and `w` is a vector over `F`, such that the entries of `w` are `K`-linearly independent and .. MATH:: v = w*A^t. It follows that all the entries of `v` are contained in the `K`-span of the entries of `w`, and that the left kernel of `A` is the space of `K`-relations between the entries of `v`. EXAMPLES:: sage: from regular_models.RR_spaces.function_spaces import matrix_representation sage: K = GF(2) sage: F1.<a> = K.extension(3) sage: v = [a, a^2, a + a^2] sage: A, w = matrix_representation(K, F1, v) sage: w*A.transpose() (a, a^2, a^2 + a) sage: F2.<x> = FunctionField(F1) sage: v = vector(F2, [(x+1)/x, a*x + 1]) sage: A, w = matrix_representation(K, F2, v) sage: w*A.transpose() == v True sage: R.<y> = F2[] sage: F3.<y> = F2.extension(y^2 - x^3 - a) sage: v = vector(F3, [y*x, y*a, x]) sage: A, w = matrix_representation(K, F3, v) sage: w*A.transpose() == v True """ from sage.arith.functions import lcm from sage.modules.free_module_element import vector from sage.all import matrix assert F.has_coerce_map_from(K), "K must be a subfield of F" v = vector(F, [F(v[i]) for i in range(len(v))]) if F == K: return matrix(v).transpose(), vector(K, [K.one()]) elif is_finite_simple_extension(K, F): # F is a finite extension of its base field K n = F.degree() # the entries of w are the standard basis of F/K w = vector(F, [F.gen()**i for i in range(n)]) A = matrix( K, [vector_for_finite_extension(K, F, v[j]) for j in range(len(v))]) # test result # assert v == w*A.transpose() return A, w elif is_rational_function_field(K, F): # F is the rational function field over K x = F.gen() n = len(v) h = lcm([v[j].denominator() for j in range(n)]) g_list = [F._ring(v[j] * h) for j in range(n)] N = max([g.degree() for g in g_list]) + 1 w = vector(F, [x**i / h for i in range(N)]) A = matrix(K, n, N) for i in range(n): for j in range(N): A[i, j] = g_list[i][j] # vector_list = [] # for g in g_list: # g_vector = vector(K, N, [g[i] for i in range(N)]) # vector_list.append(g_vector) # A = matrix(K, vector_list) assert v == w * A.transpose() return A, w elif is_composite(K, F): # F is a finite extension of its base field, which properly contains K F0 = intermediate_field(K, F) A, w = matrix_representation(F0, F, v) m, n = A.dimensions() a = vector(F0, m * n) for i in range(m): for j in range(n): a[j + i * n] = A[i, j] B, u = matrix_representation(K, F0, a) _, r = B.dimensions() x = vector(F, r * n) C = matrix(K, m, r * n) for j in range(n): for k in range(r): mu = j + n * k x[mu] += w[j] * u[k] for i in range(m): ell = j + n * i C[i, mu] += B[ell, k] # test the result assert x * C.transpose() == v return C, x else: raise NotImplementedError()
def jacobian(*X): r""" Compute the Jacobian (Rankin--Cohen--Ibukiyama) operator. INPUT: - ``X`` -- a list [F_1, ..., F_N] of N orthogonal modular forms for the same Gram matrix, where N = 3 + (number of self's gram matrix rows) OUTPUT: OrthogonalModularForm. (If F_1, ..., F_N have weights k_1, ..., k_N then the result has weight k_1 + ... + k_N + N - 1.) EXAMPLES:: sage: from weilrep import * sage: jacobian([ParamodularForms(1).eisenstein_series(k, 7) for k in [4, 6, 10, 12]]) / (-589927441461779261030400000/2354734631251) #funny multiple (r^-1 - r)*q^3*s^2 + (-r^-1 + r)*q^2*s^3 + (-r^-3 - 69*r^-1 + 69*r + r^3)*q^4*s^2 + (r^-3 + 69*r^-1 - 69*r - r^3)*q^2*s^4 + O(q, s)^7 """ N = len(X) if N == 1: X = X[0] N = len(X) Xref = X[0] nvars = Xref.nvars() f = Xref.true_fourier_expansion() t, = f.parent().gens() rb_x = f.base_ring() x, = rb_x.gens() r_list = rb_x.base_ring().gens() if N != nvars + 1: raise ValueError('The Jacobian requires %d modular forms.' % (nvars + 1)) k = N - 1 v = vector([0] * nvars) r_deriv = [[] for _ in r_list] t_deriv = [] x_deriv = [] u = [] S = Xref.gram_matrix() new_scale = lcm(x.scale() for x in X) for y in X: if y.gram_matrix() != S: raise ValueError('These forms do not have the same Gram matrix.') f = y.rescale(new_scale // y.scale()).true_fourier_expansion() t_deriv.append(t * f.derivative()) if nvars > 1: x_deriv.append(f.map_coefficients(lambda a: x * a.derivative())) if nvars > 2: for i, r in enumerate(r_list): r_deriv[i].append( f.map_coefficients(lambda a: rb_x([ r * y.derivative(r) for y in list(a) ]) * (x**(a.polynomial_construction()[1])))) y_k = y.weight() k += y_k v += y.weyl_vector() u.append(y_k * f) L = [u, t_deriv] if nvars > 1: L.append(x_deriv) if nvars > 2: L.extend(r_deriv) return OrthogonalModularForm( k, Xref.weilrep(), matrix(L).determinant(), scale=new_scale, weylvec=v, qexp_representation=Xref.qexp_representation())
def torsion_free_gradings(L): r""" Return a complete list of gradings of the Lie algebra over torsion free abelian groups. The list is guaranteed to be complete in the following sense: If `\mathfrak{g} = \bigoplus_{a\in A} \mathfrak{g}_a` is any grading of the Lie algebra `\mathfrak{g}` over a torsion free abelian group `A`, then there exists - a grading `\mathfrak{g} = \bigoplus_{n\in \mathbb{Z}^m} \mathfrak{g}_n`, - an automorphism `\Phi\in\mathrm{Aut}(\mathfrak{g})`, and - a homomorphism `\varphi\colon\mathbb{Z}^m\to A` such that the grading `\mathfrak{g} = \bigoplus_{n\in \mathbb{Z}^m} \Phi(\mathfrak{g}_{\varphi(n)})` is exactly the same as the original `A`-grading. However, the list is not guaranteed to be reduced up to automorphism, so the above choices are not in general unique. EXAMPLES: We list all gradings of the Heisenberg Lie algebra over torsion free abelian groups:: sage: from lie_gradings.gradings.grading import torsion_free_gradings sage: L = lie_algebras.Heisenberg(QQ, 1) sage: torsion_free_gradings(L) [Grading over Additive abelian group isomorphic to Z + Z of Heisenberg algebra of rank 1 over Rational Field with nonzero layers (1, 0) : (p1,) (0, 1) : (q1,) (1, 1) : (z,) , Grading over Additive abelian group isomorphic to Z of Heisenberg algebra of rank 1 over Rational Field with nonzero layers (1) : (p1, z) (0) : (q1,) , Grading over Additive abelian group isomorphic to Z of Heisenberg algebra of rank 1 over Rational Field with nonzero layers (1) : (q1, z) (0) : (p1,) , Grading over Additive abelian group isomorphic to Z of Heisenberg algebra of rank 1 over Rational Field with nonzero layers (1) : (p1, q1) (2) : (z,) , Grading over Trivial group of Heisenberg algebra of rank 1 over Rational Field with nonzero layers () : (p1, q1, z) ] """ maxgrading = maximal_grading(L) V = FreeModule(ZZ, len(maxgrading.magma().gens())) weights = maxgrading.layers().keys() # The torsion-free gradings are enumerated by torsion-free quotients # of the grading group of the maximal grading. diffset = set([tuple(b - a) for a, b in combinations(weights, 2)]) subspaces = [] for d in range(len(diffset) + 1): for B in combinations(diffset, d): W = V.submodule(B) if W not in subspaces: subspaces.append(W) # for each subspace, define the quotient grading projected_gradings = [] for W in subspaces: Q = V.quotient(W) # check if quotient is not torsion-free if any(qi > 0 for qi in Q.invariants()): continue quot_layers = {} for n in weights: pi_n = tuple(Q(V(tuple(n)))) if pi_n not in quot_layers: quot_layers[pi_n] = [] quot_layers[pi_n].extend(maxgrading.layers()[n]) # expand away denominators to get an integer vector grading A = AdditiveAbelianGroup(Q.invariants()) denoms = [ pi_n_k.denominator() for pi_n in quot_layers for pi_n_k in pi_n ] mult = lcm(denoms) proj_layers = { tuple(mult * pi_nk for pi_nk in pi_n): l for pi_n, l in quot_layers.items() } proj_grading = grading(L, proj_layers, magma=A, projections=True) projected_gradings.append(proj_grading) return projected_gradings
def order_of_euler_class(delta, E): """ Given the coboundary operator delta and an Euler two-cocycle E, returns k if [E] is k--torsion. By convention, returns zero if [E] is non-torsion. Note that the trivial element is 1--torsion. """ delta = Matrix(delta) E = vector(E) # Note that E is a coboundary if there is a one-cocycle C solving # # E = C*delta # # We can find C (if it exists at all) using Smith normal form. D, U, V = delta.smith_form() assert D == U*delta*V # So we are trying to solve # # C*delta = C*U.inverse()*D*V.inverse() = E # # for a one-cochain C. Multiply by V to get # # C*delta*V = C*U.inverse()*D = E*V # # Now set # # B = C*U.inverse(), and so B*U = C # # and rewrite to get # # B*U*delta*V = B*D = E*V # # So define E' by: Ep = E*V # Finally we attempt to solve B * D = Ep. Note that D is # diagonal: so if we can solve all of the equations # B[i] * D[i][i] == Ep[i] # with B[i] integers, then [E] = 0 in cohomology. diag = diagonal(D) if any( (diag[i] == 0 and Ep[i] != 0) for i in range(len(Ep)) ): return 0 # All zeros are at the end in Smith normal form. Since we've # passed the above we can now remove them. first_zero = diag.index(0) diag = diag[:first_zero] Ep = Ep[:first_zero] # Since diag[i] is (now) never zero we can divide to get the # fractions Ep[i]/diag[i] and then find the scaling that makes # them simultaneously integral. denoms = [ diag[i] / gcd(Ep[i], diag[i]) for i in range(len(Ep)) ] return lcm(denoms)