-
Notifications
You must be signed in to change notification settings - Fork 140
/
Copy pathembeddings.jl
2103 lines (1848 loc) · 92.2 KB
/
embeddings.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
###############################################################################
#
# Orthogonal direct sums and embeddings of orthogonal groups
#
###############################################################################
# This function is called whenever A and B are in orthogonal direct sum in a
# bigger torsion module. Let D be this sum. Since A and B are in orthogonal
# direct sum in D, we can embed O(A) and O(B) in O(D) by setting the identity on
# the complement.
#
# This function returns D, the embeddings A\to D and B\to D, as well as O(D)
# together with the embeddings O(A) \to O(D) and O(B) \to O(D)
function _sum_with_embeddings_orthogonal_groups(A::TorQuadModule, B::TorQuadModule)
D = A+B
gensDA = elem_type(D)[D(lift(a)) for a in gens(A)]
gensDB = elem_type(D)[D(lift(b)) for b in gens(B)]
AinD = hom(A, D, gensDA)
BinD = hom(B, D, gensDB)
@hassert :ZZLatWithIsom 1 all(a*b == 0 for a in gensDA, b in gensDB)
OD = orthogonal_group(D)
OA = orthogonal_group(A)
OB = orthogonal_group(B)
gene = data.(union(gensDA, gensDB))
geneOAinOD = elem_type(OD)[]
for f in gens(OA)
imgf = data.(union!(AinD.(f.(gens(A))), gensDB))
fab = hom(abelian_group(D), abelian_group(D), gene, imgf)
fD = OD(hom(D, D, matrix(fab)); check=false)
push!(geneOAinOD, fD)
end
geneOBinOD = elem_type(OD)[]
for f in gens(OB)
imgf = data.(union(gensDA, BinD.(f.(gens(B)))))
fab = hom(abelian_group(D), abelian_group(D), gene, imgf)
fD = OD(hom(D, D, matrix(fab)); check=false)
push!(geneOBinOD, fD)
end
OAtoOD = hom(OA, OD, geneOAinOD; check=false)
OBtoOD = hom(OB, OD, geneOBinOD; check=false)
return D, AinD, BinD, OD, OAtoOD, OBtoOD
end
# Construct the direct sum D of A and B. Since the images of A and B are in
# orthogonal direct sum, we can embed O(A) in O(D) and O(B) in O(D).
#
# This function returns D together with the injections A \to D and B \to D, as
# well as O(D) with the embeddings O(A) \to O(D) and O(B) \to O(D).
function _direct_sum_with_embeddings_orthogonal_groups(A::TorQuadModule, B::TorQuadModule)
D, inj = direct_sum(A, B)
AinD, BinD = inj
OD = orthogonal_group(D)
OA = orthogonal_group(A)
OB = orthogonal_group(B)
IA = identity_matrix(ZZ, ngens(A))
IB = identity_matrix(ZZ, ngens(B))
geneOAinOD = elem_type(OD)[]
for f in gens(OA)
m = block_diagonal_matrix(ZZMatrix[matrix(f), IB])
fD = OD(hom(D, D, m); check=false)
push!(geneOAinOD, fD)
end
geneOBinOD = elem_type(OD)[]
for f in gens(OB)
m = block_diagonal_matrix(ZZMatrix[IA, matrix(f)])
fD = OD(hom(D, D, m); check=false)
push!(geneOBinOD, fD)
end
OAtoOD = hom(OA, OD, geneOAinOD; check=false)
OBtoOD = hom(OB, OD, geneOBinOD; check=false)
return D, AinD, BinD, OD, OAtoOD, OBtoOD
end
###############################################################################
#
# Local tools
#
###############################################################################
# If fq is an isometry of the torsion module q, we compute the kernel of mu(fq)
# restricted to the p-elementary part of q (i.e. the submodule of q generated by
# all the elements of order p)
#
# This object is defined in Algorithm 2 of [BH23], the glue domain we aim to use
# for gluing lattices in a p-admissible triples are actually submodules of these
# V's.
function _get_V(fq::TorQuadModuleMap, mu::PolyRingElem, p::IntegerUnion)
q = domain(fq)
V, _ = primary_part(q, p)
_, Vinq = sub(q, elem_type(q)[q(lift(divexact(order(g), p)*g)) for g in gens(V) if !(order(g)==1)])
fpV = restrict_endomorphism(fq, Vinq; check=false)
fpV = evaluate(mu, fpV)
V, _ = kernel(fpV)
Vinq = hom(V, q, elem_type(q)[q(lift(a)) for a in gens(V)])
@hassert :ZZLatWithIsom 1 is_injective(Vinq)
return V, Vinq
end
# This is the rho function as defined in Definition 4.8 of [BH23].
function _rho_functor(q::TorQuadModule, p::IntegerUnion, l::IntegerUnion; quad::Bool=(p == 2))
pq, pqtoq = primary_part(q, p)
pq = rescale(pq, QQ(p)^(l-1))
Nv = cover(pq)
N = relations(pq)
if quad && p == 2
mqf = _is_free(q, p, l) ? QQ(2) : QQ(1)
else
mqf = QQ(1)
end
if l == 0
Gl = N
Gm = intersect(1//p*N, Nv)
rholN = torsion_quadratic_module(Gl, p*Gm; modulus=QQ(1), modulus_qf=mqf)
else
k = l-1
m = l+1
Gk = intersect((1//(p^k))*N, Nv)
Gl = intersect((1//(p^l))*N, Nv)
Gm = intersect((1//(p^m))*N, Nv)
B = Gk+p*Gm
rholN = torsion_quadratic_module(Gl, B; modulus=QQ(1), modulus_qf=mqf)
end
return rholN
end
# A finite bilinear module over the 2-adic integers is even if all square are
# zeros.
function _is_even(T::TorQuadModule, p::IntegerUnion, l::IntegerUnion)
B = gram_matrix_bilinear(_rho_functor(T, p, l; quad=false))
is_empty(B) && return true
mul!(B, B, 2)
any(!iszero, diagonal(B)) && return false
return all(is_integral, B)
end
function _is_free(T::TorQuadModule, p::IntegerUnion, l::IntegerUnion)
return _is_even(T, p, l-1) && _is_even(T, p, l+1)
end
###############################################################################
#
# Overlattices
#
###############################################################################
# Compute the overlattice corresponding to the glue map gamma, in the ambient
# space of the covering lattice of the finite bilinear module D. As inputs,
# gamma must be an anti-isometry between HA and HB, both should embed in D and
# gamma commutes with the actions of fA and fB on HA and HB respectively.
#
# If `same_ambient = true`, then we consider all the problem in a same ambient
# quadratic space. In particular, the covering lattices of HA, HB and D are all
# in that same space.
#
# fA and fB here are considered as isometries of the relations lattices of HA and
# HB, respectively.
function _overlattice(gamma::TorQuadModuleMap,
HAinD::TorQuadModuleMap,
HBinD::TorQuadModuleMap,
fA::QQMatrix = identity_matrix(QQ, rank(relations(domain(HAinD)))),
fB::QQMatrix = identity_matrix(QQ, rank(relations(domain(HBinD))));
same_ambient::Bool=false)
HA = domain(HAinD)
HB = domain(HBinD)
A = relations(HA)
B = relations(HB)
D = codomain(HAinD)
if same_ambient
bAB = reduce(vcat, basis_matrix.([A, B]))
_glue = Vector{QQFieldElem}[lift(g) + lift(gamma(g)) for g in gens(domain(gamma))]
else
bAB = block_diagonal_matrix(basis_matrix.([A, B]))
_glue = Vector{QQFieldElem}[lift(HAinD(a)) + lift(HBinD(gamma(a))) for a in gens(domain(gamma))]
end
z = zero_matrix(QQ, 0, degree(cover(D)))
glue = reduce(vcat, QQMatrix[matrix(QQ, 1, degree(cover(D)), g) for g in _glue]; init=z)
glue = vcat(bAB, glue)
Fakeglue = Hecke.FakeFmpqMat(glue)
_FakeB = hnf(Fakeglue)
_B = QQ(1, denominator(Fakeglue))*change_base_ring(QQ, numerator(_FakeB))
C = lattice(ambient_space(cover(D)), _B[end-rank(A)-rank(B)+1:end, :])
fC = block_diagonal_matrix(QQMatrix[fA, fB])
_B = solve(bAB, basis_matrix(C); side=:left)
fC = _B*fC*inv(_B)
@hassert :ZZLatWithIsom 1 fC*gram_matrix(C)*transpose(fC) == gram_matrix(C)
_, graph = sub(D, D.(_glue))
return C, fC, graph
end
# Same as above where we glue along the trivial subgroups of HA and HB. In that
# particular case, HA and HB are the discriminant groups of the lattices
# considered (so HA = L^{\vee}/L for some lattice integral lattice L, and same
# for HB), and D is the orthogonal direct sum of HA and HB in an appropriate
# quadratic space.
function _overlattice(HAinD::TorQuadModuleMap,
HBinD::TorQuadModuleMap,
fA::QQMatrix = identity_matrix(QQ, rank(relations(domain(HAinD)))),
fB::QQMatrix = identity_matrix(QQ, rank(relations(domain(HBinD))));
same_ambient::Bool=false)
HA = domain(HAinD)
HB = domain(HBinD)
zA, _ = sub(HA, TorQuadModuleElem[])
zB, _ = sub(HB, TorQuadModuleElem[])
gamma = hom(zA, zB, zero_matrix(ZZ, 0, 0))
return _overlattice(gamma, HAinD, HBinD, fA, fB; same_ambient)
end
###############################################################################
#
# Generic primitive extensions method
#
###############################################################################
# Construct the module where we perform the gluing, with the embeddings of
# orthogonal groups if one needs to glue stabilizers afterwards.
#
# If M and N happen to sit in the same quadratic space we can forward all the
# computations there and avoid to create a possibly (very) large new ambient
# space where to embed them.
#
# If one wants to compute the representation of the centralizer on the
# discriminant group for the equivariant primitive extensions computed, then
# we need to keep track of the embeddings of the orthogonal groups.
function _gluing_context(qM::TorQuadModule,
qN::TorQuadModule,
compute_bar_Gf::Bool,
same_ambient::Bool)
if compute_bar_Gf
# We pushforward the orthogonal groups along the (orthogonal) sum
# (this is Witt's theorem).
if same_ambient
return _sum_with_embeddings_orthogonal_groups(qM, qN)
else
return _direct_sum_with_embeddings_orthogonal_groups(qM, qN)
end
else
if same_ambient
D = qM+qN
qMinD = hom(qM, D, TorQuadModuleElem[D(lift(x)) for x in gens(qM)])
qNinD = hom(qN, D, TorQuadModuleElem[D(lift(x)) for x in gens(qN)])
else
D, inj = direct_sum(qM, qN)
qMinD, qNinD = inj
end
OD = orthogonal_group(D)
return D, qMinD, qNinD, OD, id_hom(OD), id_hom(OD)
end
end
# We forget about the quadratic form on qM if there is one. If
# M is the original lattice, it means that we forget about the
# quadratic form on M and see it as a bilinear module.
#
# Here GM is our classifying group, and fqM is the representation
# on qM of the isometry (possibly the identity) associated to the
# original lattice M.
function _change_to_bilinear_module(qM::TorQuadModule,
GM::AutomorphismGroup{TorQuadModule},
fqM::TorQuadModuleMap)
qM = Hecke._as_finite_bilinear_module(qM)
OqM = orthogonal_group(qM)
GM, _ = sub(OqM, elem_type(OqM)[OqM(matrix(g); check=false) for g in gens(GM)])
fqM = hom(qM, qM, matrix(fqM))
return qM, OqM, GM, fqM
end
# Get the possible glue orders given qM and qN. Can also set a default value.
#
# If glue_order is known, the user has fixed the order so we just look for glue
# domains of that size. Otherwise, we have to go through all the possibilities
# depending on the abelian group structures of qM and qN (to have isomorphic
# abelian subgroups)
function _possible_glue_orders(qM::TorQuadModule,
qN::TorQuadModule,
glue_order::Union{Nothing, IntegerUnion})
if !isnothing(glue_order)
pos_ord = typeof(glue_order)[glue_order]
else
_gcd = ZZ(1)
snM = reverse!(elementary_divisors(qM))
snN = reverse!(elementary_divisors(qN))
k = min(length(snM), length(snN))
for i in 1:k
mul!(_gcd, _gcd, gcd(snM[i], snN[i]))
end
pos_ord = divisors(_gcd)
end
return pos_ord
end
# Check whether `OHN` contains an isometry to be composed to `phi` in order
# to turn it into a `(fHM, fHN)-`equivariant gluing.
#
# phi: HM -> HN is equivariant if the following diagram commutes
#
# phi
# HM ---> HN
# fHM ↓ ↓ fHN
# HM ---> HN
# phi
#
# So if \phi is not equivariant, we try to find an isometry in OHN
# to compose \phi with in order to make it equivariant (mathematically
# this is correct: \phi was created to be any anti-isometry. The new
# one computed is another anti-isometry, and we can take any isometry of
# HN to make the change)
function _can_be_made_equivariant(phi::TorQuadModuleMap,
fHM::TorQuadModuleMap,
fHN::TorQuadModuleMap)
OHN = orthogonal_group(domain(fHN))
fHMinOHN = OHN(compose(inv(phi), compose(fHM, phi)); check=false)
bool, g0 = is_conjugate_with_data(OHN, fHMinOHN, OHN(fHN; check=false))
!bool && return false, phi
# Now the gluing is equivariant so the direct sum of the isometries
# extend to the primitive extension
phi = compose(phi, hom(OHN(g0)))
@hassert :ZZLatWithIsom 1 OHN(compose(inv(phi), compose(fHM, phi)); check=false) == OHN(fHN; check=false)
return true, phi
end
# In the case of extension type (:equivariant, :plain), we look for
# representatives of orbits of isometries on the second lattice which
# "fit" in the gluing, i.e. for which the gluing is equivariant. These
# isometries all live in a same coset, and we identity two isometries in this
# coset if they are conjugate by an isometry from the classifying group.
#
# Let N be the associated lattice.
# - OqfN here is the image of O(N) -> O(qN) where qN is the discriminant group.
# - HNinqN is the embedding of the glue domain HN into qN.
# - phig is the glue map.
# - fHM is the representation of the isometry we aim to extend on the first
# lattice.
# - discrep is the map O(N) -> O(qN).
# - stabN is the stabilizer of HN in GN, where GN is the classifying group
# for N (depending on the classification type chosen by the user).
# - if `first == true`, we return only one isometry since we only want the first
# extension in output of the global extension algorithm.
function _fitting_isometries(OqfN::AutomorphismGroup{TorQuadModule},
HNinqN::TorQuadModuleMap,
phig::TorQuadModuleMap,
fHM::TorQuadModuleMap,
discrep::GAPGroupHomomorphism,
stabN::AutomorphismGroup{TorQuadModule},
N::ZZLat,
first::Bool)
OHN = orthogonal_group(domain(HNinqN)) # This is normally cached
_stabN, _ = stabilizer(OqfN, HNinqN) # A priori, this could be different from stabN
_actN = hom(_stabN, OHN, elem_type(OHN)[OHN(restrict_automorphism(x, HNinqN; check=false); check=false) for x in gens(_stabN)]; check=false)
_imN, _ = image(_actN) # This group consists of isometry of HN which can be lifted to O(N)
_fHN = OHN(compose(inv(phig), compose(fHM, phig)); check=false)
_fHN in _imN || return QQMatrix[] # Now phig is (fHM, _fHN)-equivariant
_fqN = _actN\_fHN
_fN = discrep\_fqN
# _fN is one of the fitting isometries. Now there are two cases:
# - either we only want one of such, and we are done (we just make it into a
# honest isometry of N);
# - or we want all such isometries up to the action of the classifying group.
#
# For the latter, we remark that: the set of isometries of N restricting to
# _fHN is the coset _fN*KN where KN is the preimage by discrep of the kernel
# of _imN. Now, inside this coset, some isometries could still give rise to
# isomorphic equivariant primitive extensions for our classifying group. Thus
# we need to identity isometries which are conjugate by an isometry of our
# classifying group stabilizing HN (otherwise it does not make sense). This
# group of isometries is exactly the preimage of stabN by discrep, which we
# call CN here.
#
# To summarize, in the general case, we obtain representatives of fitting
# isometries by identifying CN-conjugate isometries in the coset fNKN.
if first
reporb = QQMatrix[solve(basis_matrix(N), basis_matrix(N)*matrix(_fN); side=:left)]
else
KNhat, _ = discrep\(kernel(_actN)[1])
fNKN = _fN*KNhat
CN, _ = discrep\stabN
m = gset(CN, (a, g) -> inv(g)*a*g, fNKN)
orb_and_rep = Tuple{typeof(_fN), typeof(m)}[]
for p in fNKN
if all(o -> !(p in o[2]), orb_and_rep)
push!(orb_and_rep, (p, orbit(m, p)))
end
end
reporb = QQMatrix[solve(basis_matrix(N), basis_matrix(N)*matrix(a[1]); side=:left) for a in orb_and_rep]
end
return reporb
end
# We have a primitive extension `M\oplus N \to L`: we want to see M and N now
# as sublattices of L.
#
# If they are in the same ambient space, there is nothing to do.
function _as_sublattices(Lf::ZZLatWithIsom, M::ZZLat, N::ZZLat, same_ambient::Bool)
if same_ambient
M2 = lattice_in_same_ambient_space(Lf, basis_matrix(M))
N2 = lattice_in_same_ambient_space(Lf, basis_matrix(N))
@hassert :ZZLatWithIsom 1 M == M2.Lb
@hassert :ZZLatWithIsom 1 N == N2.Lb
else
M2 = lattice_in_same_ambient_space(Lf, hcat(basis_matrix(M), zero_matrix(QQ, rank(M), degree(Lf) - degree(M))))
N2 = lattice_in_same_ambient_space(Lf, hcat(zero_matrix(QQ, rank(N), degree(Lf) - degree(N)), basis_matrix(N)))
@hassert :ZZLatWithIsom 1 genus(M) == genus(M2)
@hassert :ZZLatWithIsom 1 genus(N) == genus(N2)
end
return M2, N2
end
# Compute the image of the representation of $O(L, f)$ on $D_L$
# using stabilizers at the level of the gluing.
#
# Here:
# - Lf is our equivariant primitive extension already computed
# - ext_type keeps track of which kind of extension we considered first
# - OqfM and OqfN are the respective images of O(M, fM) -> O(qM) and
# O(N) -> O(qN) (or O(N, fN) -> O(qN) in the case where ext_type[2] ==
# :equivariant).
# - HMinqM and HNinqN are the embeddings of the glue domains.
# - discrep is the representation map O(N) -> O(qN) in the case where
# ext_type = (:equivariant. :plain).
# - b is a fitting isometry on N in the case ext_type[2] == :plain.
# - phig is the glue map.
# - OqMinOD and OqNinOD are the embeddings of the orthogonal groups of
# qM and qN in the orthogonal group of D (which is possible since M and N
# are orthogonal is cover(D)).
# - graph is the embedding of the graph of phig in D.
#
# To compute `image_centralizer_in_Oq(Lf)` along the equivariant gluing, we need
# the representation of the centralizers of (M, fM) and (N, fN) on the
# respective discriminant groups. Then, we look at which among the represented
# isometries stabilize the glue domains, and collect the action they induce.
#
# Once this is done, we use `_glue_stabilizers` which manages the rest of the
# algorithmic part.
function _compute_image_stabilizer_in_Oq!(Lf::ZZLatWithIsom,
ext_type::Tuple{Symbol, Symbol},
OqfM::AutomorphismGroup{TorQuadModule},
OqfN::AutomorphismGroup{TorQuadModule},
HMinqM::TorQuadModuleMap,
HNinqN::TorQuadModuleMap,
discrep::Union{Nothing, GAPGroupHomomorphism},
b::QQMatrix,
phig::TorQuadModuleMap,
OqMinOD::GAPGroupHomomorphism,
OqNinOD::GAPGroupHomomorphism,
graph::TorQuadModuleMap)
@assert ext_type[1] != :plain
OHM = orthogonal_group(domain(HMinqM))
OHN = orthogonal_group(domain(HNinqN))
_stabM, _ = stabilizer(OqfM, HMinqM)
_actM = hom(_stabM, OHM, elem_type(OHM)[OHM(restrict_automorphism(x, HMinqM; check=false); check=false) for x in gens(_stabM)]; check=false)
if ext_type[2] == :plain
_GN, _ = discrep(centralizer(domain(discrep), b)[1])
else
_GN = OqfN
end
_stabN, _ = stabilizer(_GN, HNinqN)
_actN = hom(_stabN, OHN, elem_type(OHN)[OHN(restrict_automorphism(x, HNinqN; check=false); check=false) for x in gens(_stabN)]; check=false)
disc, stab = _glue_stabilizers(phig, _actM, _actN, OqMinOD, OqNinOD, graph)
qL, fqL = discriminant_group(Lf)
OqL = orthogonal_group(qL)
# disc and qL are the same object so phi2 is basically the identity, use to
# transport stab from our module to the other.
phi2 = hom(qL, disc, TorQuadModuleElem[disc(lift(x)) for x in gens(qL)])
@hassert :ZZLatWithIsom 1 is_isometry(phi2)
stab = sub(OqL, elem_type(OqL)[OqL(compose(phi2, compose(g, inv(phi2))); check=false) for g in stab])
@hassert :ZZLatWithIsom 1 fqL in stab[1]
set_attribute!(Lf, :image_centralizer_in_Oq, stab)
end
# This function is a generic implementation for primitive extensions of integral
# integer lattices. It works in the even and odd cases, in the equivariant case
# and also depending on which kind of classification one intends to do.
#
# The arguments are the following:
# - `M` and `N` are the lattices we aim to glue;
# - `GM` and `GN` are the respective classifying groups (which should be contained
# in the image of the centralizer of fM and fN in O(qM) and O(qN) respectively);
# - `ext_type` is the type of extension: :plain means "without isometry" and
# :equivariant means "with isometry". The pair of symbol depends on whether we
# consider M and N as equipped with an isometry which we aim to extend. Note that for
# simplicity, if N has an isometry, M has one too; so we force M to be the one
# equipped with an isometry if only one lattice has an isometry to extend
# (since everything is symmetric);
# - `even` forces the primitive extensions to be even;
# - `exist_only` is meant only to state about the existence of a primitive
# extension without doing further computations once it is proved to exist;
# - `first` asks to return the first primitive extensions computed which
# satisfies all the requirements;
# - `fM` and `fN` are isometries of M and N we aim to extend. If no such
# isometries are specified, we set them to be the identity because we can always
# extend the identity along any extensions;
# - `fqM` and `fqN` are the representation of fM and fN on the respective
# discriminant groups. Since a priori fM and fN are seen as proper isometries of
# M and N, and fqM and fqN are constructed from an ambient isometry, we require
# to mention both;
# - `glue_order` is the index of the primitive extension (which is also the size of a
# glue domain);
# - `q` is the expected discriminant form of a primitive extension;
# - `compute_bar_Gf`, in the equivariant cases (so ext_type != (:plain, :plain)) asks to
# compute the representation of the centralizer of the isometries constructed
# on the discriminant group of the associated primitive extensions (can be computed
# at the level of glue map by "gluing stabilizers");
# - `OqfM`: if `compute_bar_Gf == true`, then this is the image of the representation
# of the centralizer `O(M, fM)` on the discriminant group `qM`. It is needed
# to reconstruct the one of the extension (since a priori GM could be
# smaller);
# - `OqfN`: same as before. Also, when the extension type of `(:equivariant, :plain)`,
# `OqfN` is the image of the representation of `O(N)` on the discriminant group `qN`
# used to reconstruct fitting isometries in the equivariant gluings;
# - `discrep` in the case where M has an isometry but not N, N has to be
# definite and we want to find an isometry of N which coincide with the fixed
# isometry of M on the gluing. In that way, we can extend the isometry of M, and
# we classify all such isometries (this can be expensive)
#
# TODO: add another argument to classify equivariant primitive extensions, but
# for each such, consider only one isometry on N (it happens often that we do this
# classification up to conjugacy of a factor group of O(N), and thus at the end
# for each equivariant gluing, each extension of fM give rises to the same
# cosets in the orthogonal group of the primitive extension).
#
# This generic function is then called by the different methods for primitive
# extensions later.
function _primitive_extensions_generic(
M::ZZLat,
N::ZZLat,
GM::AutomorphismGroup{TorQuadModule},
GN::AutomorphismGroup{TorQuadModule},
ext_type::Tuple{Symbol, Symbol} = (:plain, :plain);
even::Bool=(is_even(M) && is_even(N)),
exist_only::Bool=false,
first::Bool=false,
fM::QQMatrix=identity_matrix(QQ, rank(M)),
fqM::TorQuadModuleMap=id_hom(domain(GM)),
chiM::QQPolyRingElem=minimal_polynomial(fM),
fN::QQMatrix=identity_matrix(QQ, rank(N)),
fqN::TorQuadModuleMap=id_hom(domain(GN)),
chiN::QQPolyRingElem=minimal_polynomial(fN),
glue_order::Union{IntegerUnion, Nothing}=nothing,
q::Union{TorQuadModule, Nothing}=nothing,
compute_bar_Gf::Bool=false,
OqfM::Union{Nothing, AutomorphismGroup{TorQuadModule}}=nothing,
OqfN::Union{Nothing, AutomorphismGroup{TorQuadModule}}=nothing,
discrep::Union{GAPGroupHomomorphism, Nothing}=nothing
)
@assert ext_type[2] == :plain || ext_type[1] == :equivariant
if ext_type[1] == :equivariant
@assert !isnothing(OqfM)
@assert !isnothing(OqfN)
end
if ext_type[1] != ext_type[2]
@assert !isnothing(discrep)
end
results = Tuple{ZZLatWithIsom, ZZLatWithIsom, ZZLatWithIsom}[]
even && (!is_even(M) || !is_even(N)) && return false, results
parity = even ? 2 : 1
# We check the initial conditions for having a primitive
# extension with the potential given requirements
if !isnothing(glue_order)
@req glue_order > 0 "Order of glue groups must be a positive integer"
!is_divisible_by(numerator(gcd(det(M), det(N))), glue_order) && return false, results
if !isnothing(q)
@req modulus_bilinear_form(q) == 1 "q does not define the discriminant form of an integral lattice"
glue_order^2*order(q) == det(M)*det(N) || return false, results
aM, _, bM = signature_tuple(M)
aN, _, bN = signature_tuple(N)
!is_genus(q, (aM+aN, bM+bN); parity) && return false, results
G = genus(q, (aM+aN, bM+bN); parity)
end
elseif !isnothing(q)
@req modulus_bilinear_form(q) == 1 "q does not define the discriminant form of an integral lattice"
aM, _, bM = signature_tuple(M)
aN, _, bN = signature_tuple(N)
!is_genus(q, (aM+aN, bM+bN); parity) && return false, results
G = genus(q, (aM+aN, bM+bN); parity)
ok, x = divides(numerator(det(M)*det(N)), order(q))
!ok && return false, results
ok, glue_order = is_square_with_sqrt(abs(x))
!ok && return false, results
end
# Methods are simpler if we work in a fixed space
same_ambient = ambient_space(M) === ambient_space(N)
@req !same_ambient || iszero(basis_matrix(M)*gram_matrix(ambient_space(M))*transpose(basis_matrix(N))) "Lattices in same ambient space must be orthogonal"
qM = domain(GM)
qN = domain(GN)
# If we want an odd extension, then we consider M and N as odd lattices. In
# particular, we forget about the quadratic forms on the discriminant groups
# which we see as a finite bilinear module.
if !even && is_even(M)
qM, OqM, GM, fqM = _change_to_bilinear_module(qM, GM, fqM)
if !isnothing(OqfM)
OqfM, _ = sub(OqM, elem_type(OqM)[OqM(matrix(g); check=false) for g in gens(OqfM)])
end
end
if !even && is_even(N)
qN, OqN, GN, fqN = _change_to_bilinear_module(qN, GN, fqN)
if !isnothing(discrep)
OtoOqN = hom(codomain(discrep), OqN, elem_type(OqN)[OqN(matrix(g); check=false) for g in gens(codomain(discrep))]; check=false)
discrep = compose(discrep, OtoOqN)
end
if !isnothing(OqfN)
OqfN, _ = sub(OqN, elem_type(OqN)[OqN(matrix(g); check=false) for g in gens(OqfN)])
end
end
# We perform the gluing in D
D, qMinD, qNinD, OD, OqMinOD, OqNinOD = _gluing_context(qM, qN, compute_bar_Gf, same_ambient)
# Depending the abelian group structure on qM and qN, if glue_order is not
# know, we have restriction on the order of possible common subgroups.
#
# #TODO: we could improve more the collection of common anti-isometric
# subgroups of qM and qN by working with common abelian group substructures
# for each possible order.
pos_ord = _possible_glue_orders(qM, qN, glue_order)
# In the primary and elementary case, we can make things faster
prM, pM = is_primary_with_prime(M)
elM = is_elementary(M, pM)
prN, pN = is_primary_with_prime(N)
elN = is_elementary(N, pN)
# We do everything in the good elementary parts
all_elem = (elM && pM != 1) || (elN && pN != 1)
# We do everything in the good primary parts
all_prim = (prM && pM != 1) || (prN && pN != 1)
for k in pos_ord
ok, ek, pk = is_prime_power_with_data(k)
@vprintln :ZZLatWithIsom 1 "Glue order: $(k)"
# If k is a prime power, then we check whether any of the pk-primary part
# of qM or qN is elementary (to make things faster)
if ok
flag_elem = (ek == 1) || (valuation(elementary_divisors(qM)[end], pk) == 1) || (valuation(elementary_divisors(qN)[end], pk) == 1)
end
if all_elem || (ok && flag_elem)
# We look for a glue domain which is an elementary p-group
_p = max(pM, pN, pk)
_, VMinqM = _get_V(fqM, chiN, _p)
subsM = _subgroups_orbit_representatives_and_stabilizers_elementary(VMinqM, GM, k, _p, fqM)
elseif all_prim || ok
# We look for a glue domain which is a p-group
TM, TMinqM = kernel(evaluate(chiN, fqM))
_, VMinTM = primary_part(TM, max(pM, pN, pk))
VMinqM = compose(VMinTM, TMinqM)
subsM = _subgroups_orbit_representatives_and_stabilizers(VMinqM, GM, k, fqM)
else
# Remaining case
_, VMinqM = kernel(evaluate(chiN, fqM))
subsM = _subgroups_orbit_representatives_and_stabilizers(VMinqM, GM, k, fqM)
end
isempty(subsM) && continue
for (HMinqM, stabM) in subsM
HM = domain(HMinqM)
# We have fixed a glue domain on the side of M, so we need an
# anti-isometric one on the side of N.
subsN = _classes_isomorphic_subgroups(qN, GN, fqN; H=rescale(HM, -1), mu=chiM)
isempty(subsN) && continue
for (HNinqN, stabN) in subsN
HN = domain(HNinqN)
ok, phi = is_anti_isometric_with_anti_isometry(HM, HN)
@hassert :ZZLatWithIsom 1 ok
HMinD = compose(HMinqM, qMinD)
OHM = orthogonal_group(HM)
if ext_type[1] == :equivariant
fHM = restrict_endomorphism(fqM, HMinqM; check=false)
end
HNinD = compose(HNinqN, qNinD)
OHN = orthogonal_group(HN)
if ext_type[2] == :equivariant
fHN = restrict_endomorphism(fqN, HNinqN; check=false)
end
if ext_type[1] == ext_type[2] == :equivariant
ok, phi = _can_be_made_equivariant(phi, fHM, fHN)
!ok && continue
end
actM = hom(stabM, OHM, elem_type(OHM)[OHM(restrict_automorphism(x, HMinqM; check=false); check=false) for x in gens(stabM)]; check=false)
actN = hom(stabN, OHN, elem_type(OHN)[OHN(restrict_automorphism(x, HNinqN; check=false); check=false) for x in gens(stabN)]; check=false)
imN, _ = image(actN)
if ext_type[2] == :equivariant
C, _ = centralizer(OHN, OHN(fHN; check=false))
SN, _ = intersect(C, imN)
else
C = OHN
SN = imN
end
_stabHMphi = AutomorphismGroupElem{TorQuadModule}[OHN(compose(inv(phi), compose(hom(actM(g)), phi)); check=false) for g in gens(stabM)]
stabHMphi, _ = sub(OHN, _stabHMphi)
SM, _ = intersect(C, stabHMphi)
elHN = elementary_divisors(HN)
if (k != 1) && (elHN[1] == elHN[end])
iso = isomorphism(PermGroup, C)
else
iso = id_hom(C)
end
# This set of double cosets correspond to the different classes of
# primitive extensions we consider, i.e. it is in bijection with the set
# of admissible gluings.
reps = double_cosets(codomain(iso), iso(SM)[1], iso(SN)[1])
@vprintln :ZZLatWithIsom 1 "$(length(reps)) isomorphism class(es) of primitive extensions"
for _g in reps
g = iso\(representative(_g))
phig = compose(phi, hom(g))
if ext_type[1] == :equivariant && ext_type[2] == :plain
# We need to see whether there exists an isometry of N which
# stabilizes HN and agrees with fHM along the gluing phig.
reporb = _fitting_isometries(OqfN, HNinqN, phig, fHM, discrep, stabN, N, first)
else
reporb = QQMatrix[fN]
end
for b in reporb
L, fL, graph = _overlattice(phig, HMinD, HNinD, fM, b; same_ambient)
(!isnothing(q) && genus(L) == G) || (is_even(L) == even) || continue
exist_only && return true, results
Lf = integer_lattice_with_isometry(L, fL; ambient_representation=false)
M2, N2 = _as_sublattices(Lf, M, N, same_ambient)
compute_bar_Gf && _compute_image_stabilizer_in_Oq!(Lf, ext_type, OqfM, OqfN, HMinqM, HNinqN, discrep, b, phig, OqMinOD, OqNinOD, graph)
push!(results, (Lf, M2, N2))
first && return true, results
end
end
end
end
end
return length(results) > 0, results
end
###############################################################################
#
# Orbits and stabilizers of discriminant subgroups
#
###############################################################################
# Given an embedding of an `(O, f)`-stable finite quadratic module `V` of `q`,
# compute representatives of `O`-orbits of `f`-stable submodules of `V` of order
# `ord`. The stabilizers in `O` is also computed.
#
# Note that any torsion quadratic module `H` in output is given by an embedding
# of `H` in `q`.
function _subgroups_orbit_representatives_and_stabilizers(Vinq::TorQuadModuleMap,
O::AutomorphismGroup{TorQuadModule},
ord::IntegerUnion = -1,
f::Union{TorQuadModuleMap, AutomorphismGroupElem{TorQuadModule}} = id_hom(codomain(Vinq)))
res = Tuple{TorQuadModuleMap, AutomorphismGroup{TorQuadModule}}[]
V = domain(Vinq)
q = codomain(Vinq)
if !is_divisible_by(order(V), ord)
return res
end
fV = f isa TorQuadModuleMap ? restrict_endomorphism(f, Vinq; check=false) : restrict_endomorphism(hom(f), Vinq; check=false)
if ord == -1
subs = collect(stable_submodules(V, TorQuadModuleMap[fV]))
else
subs = collect(submodules(V; order=ord))
filter!(s -> is_invariant(fV, s[2]), subs)
end
to_gap = get_attribute(O, :to_gap)
to_oscar = get_attribute(O, :to_oscar)
qgap = codomain(to_gap)
sgap = typeof(qgap)[sub(qgap, elem_type(qgap)[to_gap(q(lift(s[2](a)))) for a in gens(s[1])])[1] for s in subs]
m = gset(O, on_subgroups, sgap)
orbs = orbits(m)
for orb in orbs
_repgap = representative(orb)
_, rep = sub(q, TorQuadModuleElem[to_oscar(qgap(a)) for a in gens(_repgap)])
stab, _ = stabilizer(O, rep)
push!(res, (rep, stab))
end
return res
end
# The underlying abelian groups of H and V are elementary abelian p-groups, f is
# an automorphism of V fixing H, so in particular it acts on the quotient V/H
# whose abelian structure actually defines a finite dimensional Fp-vector space.
#
# This function returns Qp := V/H as an Fp-vector space, the map which transforms
# V into a Fp-vector space Vp, the quotient map Vp \to Qp, and the restriction
# fQp of f to Qp
function _cokernel_as_Fp_vector_space(HinV::TorQuadModuleMap, p::IntegerUnion)
H = domain(HinV)
V = codomain(HinV)
n = ngens(V)
F = GF(p)
Vp = vector_space(F, n)
function _VtoVp(x::TorQuadModuleElem)
v = data(x).coeff
return Vp(vec(collect(v)))
end
function _VptoV(v::ModuleElem{FqFieldElem})
x = map(z -> lift(ZZ, z), v.v)
return sum(x[i]*V[i] for i in 1:n; init=id(V))
end
VtoVp = Hecke.MapFromFunc(V, Vp, _VtoVp, _VptoV)
subgene = elem_type(Vp)[VtoVp(HinV(a)) for a in gens(H)]
Hp, _ = sub(Vp, subgene)
Qp, VptoQp = quo(Vp, Hp)
return Qp, VtoVp, VptoQp
end
# Given an embedding of an `(G, f)`-stable finite quadratic module `V` of `q`,
# where the abelian group structure on `V` is `p`-elementary, compute
# representatives of `G`-orbit of `f`-stable subgroups of `V` of order `ord`,
# which contains `p^l*q_p` where `q_p` is the `p`-primary part of `q`.
#
# Note that `G` must lie in the centralizer of `f` in `O(q)` and `G` is seen
# as a set of outer automorphisms (so two subgroups are in the
# same orbit if they are `G`-isomorphic).
#
# The stabilizers in `G` are also computed.
#
# Note that any torsion quadratic module `H` in output is given by an embedding
# of `H` in `q`.
function _subgroups_orbit_representatives_and_stabilizers_elementary(Vinq::TorQuadModuleMap,
G::AutomorphismGroup{TorQuadModule},
ord::IntegerUnion,
_p::IntegerUnion,
f::Union{TorQuadModuleMap, AutomorphismGroupElem{TorQuadModule}} = id_hom(codomain(Vinq)),
l::IntegerUnion = -1)
res = Tuple{TorQuadModuleMap, AutomorphismGroup{TorQuadModule}}[]
p = ZZ(_p)
V = domain(Vinq)
if ord > order(V)
return res
end
q = codomain(Vinq)
pq, pqtoq = primary_part(q, p)
l = l < 0 ? valuation(order(pq), p) : l
g = valuation(ord, p)
# In theory, V should contain H0 := p^l*pq where pq is the p-primary part of q
all(a -> has_preimage_with_preimage(Vinq, (p^l)*pqtoq(a))[1], gens(pq)) || return res
H0, H0inq = sub(q, elem_type(q)[q(lift((p^l)*a)) for a in gens(pq)])
@hassert :ZZLatWithIsom 1 is_invariant(f, H0inq)
# H0 should be contained in the groups we want. So either H0 is the only one
# and we return it, or if order(H0) > ord, there are no subgroups as wanted
if order(H0) >= ord
order(H0) > ord && return res
push!(res, (H0inq, G))
return res
end
# Now the groups we look for should strictly contain H0.
# If ord == order(V), then there is only V satisfying the given
# conditions, and V is stabilized by the all G
if ord == order(V)
push!(res, (Vinq, G))
return res
end
# Now the groups we look for are strictly contained between H0 and V
H0inV = hom(H0, V, elem_type(V)[V(lift(a)) for a in gens(H0)])
@hassert :ZZLatWithIsom 1 is_injective(H0inV)
# Since V and H0 are elementary p-groups, they can be seen as finite
# dimensional vector spaces over a finite field, and so is their quotient.
# Moreover, subgroups of V of order ord and containing H0 are in bijections
# with sub vector spaces of V/H0 of rank val_p(ord - order(H))
Qp, VtoVp, VptoQp = _cokernel_as_Fp_vector_space(H0inV, p)
Vp = codomain(VtoVp)
# Should never happen, but who knows...
dim(Qp) == 0 && return res
# We descend G to V for computing stabilizers later on
GV, GtoGV = restrict_automorphism_group(G, Vinq; check=false)
# Automorphisms in G preserved V and H0, since the construction of H0 is
# natural. Therefore, the action of G descends to the quotient and we look for
# invariants sub-vector spaces of given rank in the quotient (then lifting
# generators and putting them with H0 will give us invariant subgroups as
# wanted)
act_GV = dense_matrix_type(elem_type(base_ring(Qp)))[change_base_ring(base_ring(Qp), matrix(gg)) for gg in gens(GV)]
act_GV = dense_matrix_type(elem_type(base_ring(Qp)))[solve(VptoQp.matrix, g*VptoQp.matrix; side=:right) for g in act_GV]
MGp = matrix_group(base_ring(Qp), dim(Qp), act_GV)
GVtoMGp = hom(GV, MGp, MGp.(act_GV); check=false)
GtoMGp = compose(GtoGV, GVtoMGp)
satV, _ = kernel(GtoMGp)
g-ngens(snf(abelian_group(H0))[1]) >= dim(Qp) && return res
F = base_ring(Qp)
# K is H0 but seen a subvector space of Vp (which is V)
K = kernel(VptoQp.matrix; side=:left)
k = nrows(K)
gene_H0 = elem_type(q)[q(lift(a)) for a in gens(H0)]
orb_and_stab = orbit_representatives_and_stabilizers(MGp, g-k)
for (orb, stab) in orb_and_stab
i = orb.map
gene_orbQp = elem_type(Qp)[Qp(vec(collect(i(v).v))) for v in gens(domain(i))]
gene_orbVp = elem_type(Vp)[preimage(VptoQp, v) for v in gene_orbQp]
gene_orbV = elem_type(V)[preimage(VtoVp, Vp(v)) for v in gene_orbVp]
gene_orbq = elem_type(q)[image(Vinq, v) for v in gene_orbV]
append!(gene_orbq, gene_H0)
orbq, orbqinq = sub(q, gene_orbq)
@hassert :ZZLatWithIsom 1 order(orbq) == ord
# We keep only f-stable subspaces
is_invariant(f, orbqinq) || continue
stabq_gen = elem_type(G)[GtoMGp\(s) for s in gens(stab)]
stabq, _ = sub(G, union!(stabq_gen, gens(satV)))
# Stabilizers should preserve the actual subspaces, by definition. so if we
# have lifted everything properly, this should hold..
@hassert :ZZLatWithIsom 1 is_invariant(stabq, orbqinq)
push!(res, (orbqinq, stabq))
end
return res
end
# Compute `O`-orbits of `f`-stable submodules of `ker(mu(f))` which are isometric,
# as torsion quadratic modules, to `H`. It also computes the stabilizers in `O`
# of such subgroups. If `H` is not given, then return orbits of stable
# submodules of order `ordH`.
#
# The outputs are given by embeddings of such submodules in `q`.
#
# The code splits the computations into primary part since they are orthogonal
# to each others.
function _classes_isomorphic_subgroups(q::TorQuadModule,
O::AutomorphismGroup{TorQuadModule},
f::Union{TorQuadModuleMap, AutomorphismGroupElem{TorQuadModule}} = id_hom(domain(O));
H::Union{Nothing, TorQuadModule}=nothing,
ordH::Union{Nothing, IntegerUnion}=nothing,
mu::PolyRingElem=zero(Hecke.Globals.Qx))
res = Tuple{TorQuadModuleMap, AutomorphismGroup{TorQuadModule}}[]
if isnothing(H)
@assert !isnothing(ordH)
@assert ordH > 0
else
ordH = order(H)
end
!is_divisible_by(order(q), ordH) && return res
if ordH == 1
_, j = sub(q, elem_type(q)[])
push!(res, (j, O))
return res
end
# Trivial case: we look for subgroups in a given primary part of q
ok, e, p = is_prime_power_with_data(ordH)
if ok
if (e == 1) || (!isnothing(H) && is_elementary(H, p))
_, Vinq = _get_V(f, mu, p)