diff --git a/src/sage/modules/torsion_quadratic_module.py b/src/sage/modules/torsion_quadratic_module.py index a4dd2bc9a31..4e0eb49b900 100644 --- a/src/sage/modules/torsion_quadratic_module.py +++ b/src/sage/modules/torsion_quadratic_module.py @@ -20,7 +20,7 @@ from sage.modules.fg_pid.fgp_module import FGP_Module_class from sage.modules.fg_pid.fgp_element import DEBUG, FGP_Element from sage.arith.misc import gcd -from sage.rings.all import ZZ, QQ, IntegerModRing +from sage.rings.all import ZZ, Zp, QQ, IntegerModRing from sage.groups.additive_abelian.qmodnz import QmodnZ from sage.matrix.constructor import matrix from sage.misc.cachefunc import cached_method @@ -584,6 +584,40 @@ def normal_form(self, partial=False): EXAMPLES:: + sage: L1=IntegralLattice(matrix([[-2,0,0],[0,1,0],[0,0,4]])) + sage: L1.discriminant_group().normal_form() + Finite quadratic module over Integer Ring with invariants (2, 4) + Gram matrix of the quadratic form with values in Q/Z: + [1/2 0] + [ 0 1/4] + sage: L2=IntegralLattice(matrix([[-2,0,0],[0,1,0],[0,0,-4]])) + sage: L2.discriminant_group().normal_form() + Finite quadratic module over Integer Ring with invariants (2, 4) + Gram matrix of the quadratic form with values in Q/Z: + [1/2 0] + [ 0 1/4] + + We check that :trac:`24864` is fixed:: + + sage: L1=IntegralLattice(matrix([[-4,0,0],[0,4,0],[0,0,-2]])) + sage: AL1=L1.discriminant_group() + sage: L2=IntegralLattice(matrix([[-4,0,0],[0,-4,0],[0,0,2]])) + sage: AL2=L2.discriminant_group() + sage: AL1.normal_form() + Finite quadratic module over Integer Ring with invariants (2, 4, 4) + Gram matrix of the quadratic form with values in Q/2Z: + [1/2 0 0] + [ 0 1/4 0] + [ 0 0 5/4] + sage: AL2.normal_form() + Finite quadratic module over Integer Ring with invariants (2, 4, 4) + Gram matrix of the quadratic form with values in Q/2Z: + [1/2 0 0] + [ 0 1/4 0] + [ 0 0 5/4] + + Some exotic cases:: + sage: from sage.modules.torsion_quadratic_module import TorsionQuadraticModule sage: D4_gram = Matrix(ZZ,4,4,[2,0,0,-1,0,2,0,-1,0,0,2,-1,-1,-1,-1,2]) sage: D4 = FreeQuadraticModule(ZZ,4,D4_gram) @@ -599,26 +633,71 @@ def normal_form(self, partial=False): sage: T.normal_form() Finite quadratic module over Integer Ring with invariants (6, 6, 12, 12) Gram matrix of the quadratic form with values in Q/(1/3)Z: - [1/12 1/24 0 0 0 0 0 0] - [1/24 1/12 0 0 0 0 0 0] - [ 0 0 1/6 1/12 0 0 0 0] - [ 0 0 1/12 1/6 0 0 0 0] + [ 1/6 1/12 0 0 0 0 0 0] + [1/12 1/6 0 0 0 0 0 0] + [ 0 0 1/12 1/24 0 0 0 0] + [ 0 0 1/24 1/12 0 0 0 0] [ 0 0 0 0 1/9 0 0 0] [ 0 0 0 0 0 1/9 0 0] [ 0 0 0 0 0 0 1/9 0] [ 0 0 0 0 0 0 0 1/9] + + TESTS: + + A degenerate case:: + + sage: T = TorsionQuadraticModule((1/6)*D4dual, D4, modulus=1/36) + sage: T.normal_form() + Finite quadratic module over Integer Ring with invariants (6, 6, 12, 12) + Gram matrix of the quadratic form with values in Q/(1/18)Z: + [1/36 1/72 0 0 0 0 0 0] + [1/72 1/36 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0] """ gens = [] - from sage.quadratic_forms.genera.normal_form import p_adic_normal_form + from sage.quadratic_forms.genera.normal_form import p_adic_normal_form, _normalize for p in self.annihilator().gen().prime_divisors(): D_p = self.primary_part(p) q_p = D_p.gram_matrix_quadratic() q_p = q_p / D_p._modulus_qf + + # continue with the non-degenerate part + r = q_p.rank() + if r != q_p.ncols(): + U = q_p._clear_denom()[0].hermite_form(transformation=True)[1] + else: + U = q_p.parent().identity_matrix() + kernel = U[r:,:] + nondeg = U[:r,:] + q_p = nondeg * q_p * nondeg.T + + # the normal form is implemented for p-adic lattices + # so we should work with the lattice q_p --> q_p^-1 + q_p1 = q_p.inverse() prec = self.annihilator().gen().valuation(p) + 5 - D, U = p_adic_normal_form(q_p, p, precision=prec, partial=False) + D, U = p_adic_normal_form(q_p1, p, precision=prec + 5, partial=partial) + # if we compute the inverse in the p-adics everything explodes --> go to ZZ + U = U.change_ring(ZZ).inverse().transpose() + + # the inverse is in normal form - so to get a normal form for the original one + # it is enough to massage each 1x1 resp. 2x2 block. + U = U.change_ring(Zp(p, type='fixed-mod', prec=prec)).change_ring(ZZ) + D = U * q_p * U.T * p**q_p.denominator().valuation(p) + D = D.change_ring(Zp(p, type='fixed-mod', prec=prec)) + _, U1 = _normalize(D, normal_odd=False) + U = U1.change_ring(ZZ) * U + + # reattach the degenerate part + nondeg = U * nondeg + U = nondeg.stack(kernel) + #apply U to the generators n = U.ncols() - U = U.change_ring(ZZ) gens_p = [] for i in range(n): g = self.V().zero() diff --git a/src/sage/quadratic_forms/genera/normal_form.py b/src/sage/quadratic_forms/genera/normal_form.py index 1cbe3b9dd63..d4b11fcf937 100644 --- a/src/sage/quadratic_forms/genera/normal_form.py +++ b/src/sage/quadratic_forms/genera/normal_form.py @@ -755,7 +755,7 @@ def _min_nonsquare(p): if not R(i).is_square(): return i -def _normalize(G): +def _normalize(G, normal_odd=True): r""" Return the transformation to sums of forms of types `U`, `V` and `W`. @@ -763,8 +763,10 @@ def _normalize(G): INPUT: - - a symmetric matrix over `\ZZ_p` in jordan form -- + - ``G`` -- a symmetric matrix over `\ZZ_p` in jordan form -- the output of :meth:`p_adic_normal_form` or :meth:`_jordan_2_adic` + - ``normal_odd`` -- bool (default: True) if true and `p` is odd, + compute a normal form. OUTPUT: @@ -804,7 +806,7 @@ def _normalize(G): if D[i,i].valuation() > val: # a new block starts val = D[i,i].valuation() - if len(non_squares) != 0: + if normal_odd and len(non_squares) != 0: # move the non-square to # the last entry of the previous block j = non_squares.pop() @@ -816,7 +818,7 @@ def _normalize(G): else: D[i, i] = v B[i, :] *= (v * d.inverse_of_unit()).sqrt() - if len(non_squares) != 0: + if normal_odd and len(non_squares) != 0: # we combine two non-squares to get # the 2 x 2 identity matrix j = non_squares.pop() @@ -826,7 +828,7 @@ def _normalize(G): D[j,j] = 1 else: non_squares.append(i) - if len(non_squares) != 0: + if normal_odd and len(non_squares) != 0: j=non_squares.pop() B.swap_rows(j,n-1) else: