Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/quadratic_forms/quadratic_form__equivalence_testing.py
4084 views
1
"""
2
Equivalence Testing
3
"""
4
5
from sage.matrix.constructor import Matrix
6
from sage.misc.mrange import mrange
7
from sage.rings.arith import hilbert_symbol, prime_divisors, is_prime, valuation, GCD, legendre_symbol
8
from sage.rings.integer_ring import ZZ
9
10
from quadratic_form import is_QuadraticForm
11
12
13
from sage.misc.misc import SAGE_ROOT
14
15
import tempfile, os
16
17
from random import random
18
19
20
################################################################################
21
## Routines to test if two quadratic forms over ZZ are globally equivalent. ##
22
## (For now, we require both forms to be positive definite.) ##
23
################################################################################
24
25
def is_globally_equivalent__souvigner(self, other, return_transformation=False):
26
"""
27
Uses the Souvigner code to compute the number of automorphisms.
28
29
INPUT:
30
a QuadraticForm
31
32
OUTPUT:
33
boolean, and optionally a matrix
34
35
EXAMPLES::
36
37
sage: Q = QuadraticForm(ZZ, 3, [1, 0, -1, 2, -1, 5])
38
sage: Q1 = QuadraticForm(ZZ, 3, [8, 6, 5, 3, 4, 2])
39
sage: M = Q.is_globally_equivalent__souvigner(Q1, True) ; M # optional -- souvigner
40
[ 0 0 -1]
41
[ 1 0 0]
42
[-1 1 1]
43
sage: Q1(M) == Q # optional -- souvigner
44
True
45
46
"""
47
## Write an input text file
48
F_filename = '/tmp/tmp_isom_input' + str(random()) + ".txt"
49
F = open(F_filename, 'w')
50
#F = tempfile.NamedTemporaryFile(prefix='tmp_isom_input', suffix=".txt") ## This failed because it may have hyphens, which are interpreted badly by the Souvigner code.
51
F.write("\n #1 \n")
52
53
## Write the first form
54
n = self.dim()
55
F.write(str(n) + "x0 \n") ## Use the lower-triangular form
56
for i in range(n):
57
for j in range(i+1):
58
if i == j:
59
F.write(str(2 * self[i,j]) + " ")
60
else:
61
F.write(str(self[i,j]) + " ")
62
F.write("\n")
63
64
## Write the second form
65
F.write("\n")
66
n = self.dim()
67
F.write(str(n) + "x0 \n") ## Use the lower-triangular form
68
for i in range(n):
69
for j in range(i+1):
70
if i == j:
71
F.write(str(2 * other[i,j]) + " ")
72
else:
73
F.write(str(other[i,j]) + " ")
74
F.write("\n")
75
F.flush()
76
#print "Input filename = ", F.name
77
#os.system("less " + F.name)
78
79
## Call the Souvigner automorphism code
80
souvigner_isom_path = SAGE_ROOT + "/local/bin/Souvigner_ISOM"
81
G1 = tempfile.NamedTemporaryFile(prefix='tmp_isom_ouput', suffix=".txt")
82
#print "Output filename = ", G1.name
83
#print "Executing the shell command: " + souvigner_isom_path + " '" + F.name + "' > '" + G1.name + "'"
84
os.system(souvigner_isom_path + " '" + F.name + "' > '" + G1.name +"'")
85
86
87
## Read the output
88
G2 = open(G1.name, 'r')
89
line = G2.readline()
90
if line.startswith("Error:"):
91
raise RuntimeError, "There is a problem using the souvigner code... " + line
92
elif line.find("not isomorphic") != -1: ## Checking if this text appears, if so then they're not isomorphic!
93
return False
94
else:
95
## Decide whether to read the transformation matrix, and return true
96
if not return_transformation:
97
F.close()
98
G1.close()
99
G2.close()
100
os.system("rm -f " + F_filename)
101
return True
102
else:
103
## Try to read the isomorphism matrix
104
M = Matrix(ZZ, n, n)
105
for i in range(n):
106
new_row_text = G2.readline().split()
107
#print new_row_text
108
for j in range(n):
109
M[i,j] = new_row_text[j]
110
111
## Remove temporary files and return the value
112
F.close()
113
G1.close()
114
G2.close()
115
os.system("rm -f " + F_filename)
116
if return_transformation:
117
return M.transpose()
118
else:
119
return True
120
#return True, M
121
122
## Raise and error if we're here:
123
raise RuntimeError, "Oops! There is a problem..."
124
125
126
127
def is_globally_equivalent_to(self, other, return_matrix=False, check_theta_to_precision='sturm', check_local_equivalence=True):
128
"""
129
Determines if the current quadratic form is equivalent to the
130
given form over ZZ. If return_matrix is True, then we also return
131
the transformation matrix M so that self(M) == other.
132
133
INPUT:
134
a QuadraticForm
135
136
OUTPUT:
137
boolean, and optionally a matrix
138
139
EXAMPLES::
140
141
sage: Q = DiagonalQuadraticForm(ZZ, [1,1,1,1])
142
sage: M = Matrix(ZZ, 4, 4, [1,2,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
143
sage: Q1 = Q(M)
144
sage: Q.(Q1) # optional -- souvigner
145
True
146
sage: MM = Q.is_globally_equivalent_to(Q1, return_matrix=True) # optional -- souvigner
147
sage: Q(MM) == Q1 # optional -- souvigner
148
True
149
150
::
151
152
sage: Q1 = QuadraticForm(ZZ, 3, [1, 0, -1, 2, -1, 5])
153
sage: Q2 = QuadraticForm(ZZ, 3, [2, 1, 2, 2, 1, 3])
154
sage: Q3 = QuadraticForm(ZZ, 3, [8, 6, 5, 3, 4, 2])
155
sage: Q1.is_globally_equivalent_to(Q2) # optional -- souvigner
156
False
157
sage: Q1.is_globally_equivalent_to(Q3) # optional -- souvigner
158
True
159
sage: M = Q1.is_globally_equivalent_to(Q3, True) ; M # optional -- souvigner
160
[-1 -1 0]
161
[ 1 1 1]
162
[-1 0 0]
163
sage: Q1(M) == Q3 # optional -- souvigner
164
True
165
166
::
167
168
sage: Q = DiagonalQuadraticForm(ZZ, [1, -1])
169
sage: Q.is_globally_equivalent_to(Q)
170
Traceback (most recent call last):
171
...
172
ValueError: not a definite form in QuadraticForm.is_globally_equivalent_to()
173
174
"""
175
## only for definite forms
176
if not self.is_definite():
177
raise ValueError, "not a definite form in QuadraticForm.is_globally_equivalent_to()"
178
179
## Check that other is a QuadraticForm
180
#if not isinstance(other, QuadraticForm):
181
if not is_QuadraticForm(other):
182
raise TypeError, "Oops! You must compare two quadratic forms, but the argument is not a quadratic form. =("
183
184
185
## Now use the Souvigner code by default! =)
186
return other.is_globally_equivalent__souvigner(self, return_matrix) ## Note: We switch this because the Souvigner code has the opposite mapping convention to us. (It takes the second argument to the first!)
187
188
189
190
## ---------------------------------- Unused Code below ---------------------------------------------------------
191
192
## Check if the forms are locally equivalent
193
if (check_local_equivalence == True):
194
if not self.is_locally_equivalent_to(other):
195
return False
196
197
## Check that the forms have the same theta function up to the desired precision (this can be set so that it determines the cusp form)
198
if check_theta_to_precision != None:
199
if self.theta_series(check_theta_to_precision, var_str='', safe_flag=False) != other.theta_series(check_theta_to_precision, var_str='', safe_flag=False):
200
return False
201
202
203
## Make all possible matrices which give an isomorphism -- can we do this more intelligently?
204
## ------------------------------------------------------------------------------------------
205
206
## Find a basis of short vectors for one form, and try to match them with vectors of that length in the other one.
207
basis_for_self, self_lengths = self.basis_of_short_vectors(show_lengths=True)
208
max_len = max(self_lengths)
209
short_vectors_of_other = other.short_vector_list_up_to_length(max_len + 1)
210
211
## Make the matrix A:e_i |--> v_i to our new basis.
212
A = Matrix(basis_for_self).transpose()
213
Q2 = A.transpose() * self.matrix() * A ## This is the matrix of 'self' in the new basis
214
Q3 = other.matrix()
215
216
## Determine all automorphisms
217
n = self.dim()
218
Auto_list = []
219
220
## DIAGNOSTIC
221
#print "n = " + str(n)
222
#print "pivot_lengths = " + str(pivot_lengths)
223
#print "vector_list_by_length = " + str(vector_list_by_length)
224
#print "length of vector_list_by_length = " + str(len(vector_list_by_length))
225
226
for index_vec in mrange([len(short_vectors_of_other[self_lengths[i]]) for i in range(n)]):
227
M = Matrix([short_vectors_of_other[self_lengths[i]][index_vec[i]] for i in range(n)]).transpose()
228
if M.transpose() * Q3 * M == Q2:
229
if return_matrix:
230
return A * M.inverse()
231
else:
232
return True
233
234
## If we got here, then there is no isomorphism
235
return False
236
237
238
def is_locally_equivalent_to(self, other, check_primes_only=False, force_jordan_equivalence_test=False):
239
"""
240
Determines if the current quadratic form (defined over ZZ) is
241
locally equivalent to the given form over the real numbers and the
242
`p`-adic integers for every prime p.
243
244
This works by comparing the local Jordan decompositions at every
245
prime, and the dimension and signature at the real place.
246
247
INPUT:
248
a QuadraticForm
249
250
OUTPUT:
251
boolean
252
253
EXAMPLES::
254
255
sage: Q1 = QuadraticForm(ZZ, 3, [1, 0, -1, 2, -1, 5])
256
sage: Q2 = QuadraticForm(ZZ, 3, [2, 1, 2, 2, 1, 3])
257
sage: Q1.is_globally_equivalent_to(Q2) # optional -- souvigner
258
False
259
sage: Q1.is_locally_equivalent_to(Q2)
260
True
261
262
"""
263
## TO IMPLEMENT:
264
if self.det() == 0:
265
raise NotImplementedError, "OOps! We need to think about whether this still works for degenerate forms... especially check the signature."
266
267
## Check that both forms have the same dimension and base ring
268
if (self.dim() != other.dim()) or (self.base_ring() != other.base_ring()):
269
return False
270
271
## Check that the determinant and level agree
272
if (self.det() != other.det()) or (self.level() != other.level()):
273
return False
274
275
## -----------------------------------------------------
276
277
## Test equivalence over the real numbers
278
if self.signature() != other.signature():
279
return False
280
281
## Test equivalence over Z_p for all primes
282
if (self.base_ring() == ZZ) and (force_jordan_equivalence_test == False):
283
284
## Test equivalence with Conway-Sloane genus symbols (default over ZZ)
285
if self.CS_genus_symbol_list() != other.CS_genus_symbol_list():
286
return False
287
else:
288
## Test equivalence via the O'Meara criterion.
289
for p in prime_divisors(ZZ(2) * self.det()):
290
#print "checking the prime p = ", p
291
if not self.has_equivalent_Jordan_decomposition_at_prime(other, p):
292
return False
293
294
## All tests have passed!
295
return True
296
297
298
299
300
def has_equivalent_Jordan_decomposition_at_prime(self, other, p):
301
"""
302
Determines if the given quadratic form has a Jordan decomposition
303
equivalent to that of self.
304
305
INPUT:
306
a QuadraticForm
307
308
OUTPUT:
309
boolean
310
311
EXAMPLES::
312
313
sage: Q1 = QuadraticForm(ZZ, 3, [1, 0, -1, 1, 0, 3])
314
sage: Q2 = QuadraticForm(ZZ, 3, [1, 0, 0, 2, -2, 6])
315
sage: Q3 = QuadraticForm(ZZ, 3, [1, 0, 0, 1, 0, 11])
316
sage: [Q1.level(), Q2.level(), Q3.level()]
317
[44, 44, 44]
318
sage: Q1.has_equivalent_Jordan_decomposition_at_prime(Q2,2)
319
False
320
sage: Q1.has_equivalent_Jordan_decomposition_at_prime(Q2,11)
321
False
322
sage: Q1.has_equivalent_Jordan_decomposition_at_prime(Q3,2)
323
False
324
sage: Q1.has_equivalent_Jordan_decomposition_at_prime(Q3,11)
325
True
326
sage: Q2.has_equivalent_Jordan_decomposition_at_prime(Q3,2)
327
True
328
sage: Q2.has_equivalent_Jordan_decomposition_at_prime(Q3,11)
329
False
330
331
"""
332
## Sanity Checks
333
#if not isinstance(other, QuadraticForm):
334
if type(other) != type(self):
335
raise TypeError, "Oops! The first argument must be of type QuadraticForm."
336
if not is_prime(p):
337
raise TypeError, "Oops! The second argument must be a prime number."
338
339
## Get the relevant local normal forms quickly
340
self_jordan = self.jordan_blocks_by_scale_and_unimodular(p, safe_flag= False)
341
other_jordan = other.jordan_blocks_by_scale_and_unimodular(p, safe_flag=False)
342
343
## DIAGNOSTIC
344
#print "self_jordan = ", self_jordan
345
#print "other_jordan = ", other_jordan
346
347
348
## Check for the same number of Jordan components
349
if len(self_jordan) != len(other_jordan):
350
return False
351
352
353
## Deal with odd primes: Check that the Jordan component scales, dimensions, and discriminants are the same
354
if p != 2:
355
for i in range(len(self_jordan)):
356
if (self_jordan[i][0] != other_jordan[i][0]) \
357
or (self_jordan[i][1].dim() != other_jordan[i][1].dim()) \
358
or (legendre_symbol(self_jordan[i][1].det() * other_jordan[i][1].det(), p) != 1):
359
return False
360
361
## All tests passed for an odd prime.
362
return True
363
364
365
## For p = 2: Check that all Jordan Invariants are the same.
366
elif p == 2:
367
368
## Useful definition
369
t = len(self_jordan) ## Define t = Number of Jordan components
370
371
372
## Check that all Jordan Invariants are the same (scale, dim, and norm)
373
for i in range(t):
374
if (self_jordan[i][0] != other_jordan[i][0]) \
375
or (self_jordan[i][1].dim() != other_jordan[i][1].dim()) \
376
or (valuation(GCD(self_jordan[i][1].coefficients()), p) != valuation(GCD(other_jordan[i][1].coefficients()), p)):
377
return False
378
379
## DIAGNOSTIC
380
#print "Passed the Jordan invariant test."
381
382
383
## Use O'Meara's isometry test 93:29 on p277.
384
## ------------------------------------------
385
386
## List of norms, scales, and dimensions for each i
387
scale_list = [ZZ(2)**self_jordan[i][0] for i in range(t)]
388
norm_list = [ZZ(2)**(self_jordan[i][0] + valuation(GCD(self_jordan[i][1].coefficients()), 2)) for i in range(t)]
389
dim_list = [(self_jordan[i][1].dim()) for i in range(t)]
390
391
## List of Hessian determinants and Hasse invariants for each Jordan (sub)chain
392
## (Note: This is not the same as O'Meara's Gram determinants, but ratios are the same!) -- NOT SO GOOD...
393
## But it matters in condition (ii), so we multiply all by 2 (instead of dividing by 2 since only square-factors matter, and it's easier.)
394
j = 0
395
self_chain_det_list = [ self_jordan[j][1].Gram_det() * (scale_list[j]**dim_list[j])]
396
other_chain_det_list = [ other_jordan[j][1].Gram_det() * (scale_list[j]**dim_list[j])]
397
self_hasse_chain_list = [ self_jordan[j][1].scale_by_factor(ZZ(2)**self_jordan[j][0]).hasse_invariant__OMeara(2) ]
398
other_hasse_chain_list = [ other_jordan[j][1].scale_by_factor(ZZ(2)**other_jordan[j][0]).hasse_invariant__OMeara(2) ]
399
400
for j in range(1, t):
401
self_chain_det_list.append(self_chain_det_list[j-1] * self_jordan[j][1].Gram_det() * (scale_list[j]**dim_list[j]))
402
other_chain_det_list.append(other_chain_det_list[j-1] * other_jordan[j][1].Gram_det() * (scale_list[j]**dim_list[j]))
403
self_hasse_chain_list.append(self_hasse_chain_list[j-1] \
404
* hilbert_symbol(self_chain_det_list[j-1], self_jordan[j][1].Gram_det(), 2) \
405
* self_jordan[j][1].hasse_invariant__OMeara(2))
406
other_hasse_chain_list.append(other_hasse_chain_list[j-1] \
407
* hilbert_symbol(other_chain_det_list[j-1], other_jordan[j][1].Gram_det(), 2) \
408
* other_jordan[j][1].hasse_invariant__OMeara(2))
409
410
411
## SANITY CHECK -- check that the scale powers are strictly increasing
412
for i in range(1, len(scale_list)):
413
if scale_list[i-1] >= scale_list[i]:
414
raise RuntimeError, "Oops! There is something wrong with the Jordan Decomposition -- the given scales are not strictly increasing!"
415
416
417
## DIAGNOSTIC
418
#print "scale_list = ", scale_list
419
#print "norm_list = ", norm_list
420
#print "dim_list = ", dim_list
421
#print
422
#print "self_chain_det_list = ", self_chain_det_list
423
#print "other_chain_det_list = ", other_chain_det_list
424
#print "self_hasse_chain_list = ", self_hasse_chain_list
425
#print "other_hasse_chain_det_list = ", other_hasse_chain_list
426
427
428
## Test O'Meara's two conditions
429
for i in range(t-1):
430
431
## Condition (i): Check that their (unit) ratio is a square (but it suffices to check at most mod 8).
432
modulus = norm_list[i] * norm_list[i+1] / (scale_list[i] ** 2)
433
if modulus > 8:
434
modulus = 8
435
if (modulus > 1) and (((self_chain_det_list[i] / other_chain_det_list[i]) % modulus) != 1):
436
#print "Failed when i =", i, " in condition 1."
437
return False
438
439
## Check O'Meara's condition (ii) when appropriate
440
if norm_list[i+1] % (4 * norm_list[i]) == 0:
441
if self_hasse_chain_list[i] * hilbert_symbol(norm_list[i] * other_chain_det_list[i], -self_chain_det_list[i], 2) \
442
!= other_hasse_chain_list[i] * hilbert_symbol(norm_list[i], -other_chain_det_list[i], 2): ## Nipp conditions
443
#print "Failed when i =", i, " in condition 2."
444
return False
445
446
447
## All tests passed for the prime 2.
448
return True
449
450
else:
451
raise TypeError, "Oops! This should not have happened."
452
453
454
455
456
457
458
459
460