Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
181 views
unlisted
ubuntu2004
1
# -*- coding: utf-8 -*-
2
3
from copy import deepcopy
4
import itertools
5
import os
6
import pickle
7
try:
8
from collections.abc import Iterable
9
except ImportError:
10
from collections import Iterable
11
12
from sage.env import DOT_SAGE
13
14
from sage.structure.sage_object import SageObject
15
from sage.arith.all import factorial, bernoulli, gcd, lcm, multinomial
16
from sage.arith.misc import binomial
17
from sage.groups.perm_gps.permgroup import PermutationGroup
18
from sage.matrix.constructor import matrix
19
from sage.matrix.special import block_matrix
20
from sage.combinat.all import Permutations, Subsets, Partitions
21
from sage.misc.cachefunc import cached_function
22
from sage.misc.misc_c import prod
23
from sage.misc.flatten import flatten
24
from sage.modules.free_module_element import vector
25
from sage.rings.all import Integer, Rational, QQ, ZZ
26
from sage.modules.free_module import span
27
28
from .moduli import MODULI_ST, get_moduli, socle_degree
29
from .stable_graph import StableGraph, GraphIsom
30
from . import DR
31
from . import file_cache
32
33
initialized = True
34
g = Integer(0)
35
n = Integer(3)
36
37
38
def reset_g_n(gloc, nloc):
39
from .superseded import deprecation
40
deprecation(109, 'reset_g_n is deprecated. Please use TautologicalRing instead.')
41
gloc = Integer(gloc)
42
nloc = Integer(nloc)
43
if gloc < 0 or nloc < 0:
44
raise ValueError
45
global g, n
46
g = gloc
47
n = nloc
48
49
50
Hdatabase = {}
51
52
# stgraph is a class modelling stable graphs G
53
# all vertices and all legs are uniquely globally numbered by integers, vertices numbered by 1,2...,m
54
# G.genera = list of genera of m vertices
55
# G.legs = list of length m, where ith entry is set of legs attached to vertex i
56
# CONVENTION: usually legs are labelled 1,2,3,4,....
57
# G.edges = edges of G, given as list of ordered 2-tuples, sorted in ascending order by smallest element
58
# TODO: do I really need sorted in ascending order ...
59
#
60
# G.g() = total genus of G
61
62
# Examples
63
# --------
64
# Creating a stgraph with two vertices of genera 3,5 joined
65
# by an edge with a self-loop at the genus 3 vertex.
66
# sage: stgraph([3,5],[[1,3,5],[2]],[(1,2),(3,5)])
67
# [3, 5] [[1, 3, 5], [2]] [(1, 2), (3, 5)]
68
69
stgraph = StableGraph
70
71
72
def trivgraph(g, n):
73
"""
74
Return the trivial graph in genus `g` with `n` markings.
75
76
EXAMPLES::
77
78
sage: from admcycles.admcycles import trivgraph
79
sage: trivgraph(1,1)
80
[1] [[1]] []
81
"""
82
return stgraph([g], [list(range(1, n + 1))], [])
83
84
85
# gives a list of dual graphs for \bar M_{g,n} of codimension r, i.e. with r edges
86
# TODO: Huge potential for optimization, at the moment consecutively degenerate and check for isomorphisms
87
def list_strata(g, n, r):
88
if r == 0:
89
return [stgraph([g], [list(range(1, n + 1))], [])]
90
Lold = list_strata(g, n, r - 1)
91
Lnew = []
92
for G in Lold:
93
for v in range(G.num_verts()):
94
Lnew += G.degenerations(v)
95
# now Lnew contains a list of all possible degenerations of graphs G with r-1 edges
96
# need to identify duplicates
97
98
if not Lnew:
99
return []
100
101
Ldupfree = [Lnew[0]]
102
count = 1
103
# duplicate-free list of strata
104
105
for i in range(1, len(Lnew)):
106
newgraph = True
107
for j in range(count):
108
if Lnew[i].is_isomorphic(Ldupfree[j]):
109
newgraph = False
110
break
111
if newgraph:
112
Ldupfree += [Lnew[i]]
113
count += 1
114
115
return Ldupfree
116
117
# degeneration_graph computes a data structure containing all isomorphism classes of stable graphs of genus g with n markings 1,...,n and the information about one-edge degenerations between them as well as a lookup table for graph-invariants, to quickly identify the index of a given graph
118
# optional argument: rmax = maximal number of edges that should be computed (so rmax < 3*g-3+n means only a partial degeneration graph is computed - if function had been called with higher rmax before, a larger degeneration_graph will be returned
119
# the function will look for previous calls in the dictionary of cached_function and restart from there
120
# output = (deglist,invlist)
121
# deglist is a list [L0,L1,L2,...] where Lk is itself a list [G0,G1,G2,...] with all isomorphism classes of stable graphs having k edges
122
# the entries Gj have the form [stgraph Gr, (tuple of the half-edges of Gr), set of morphisms to L(j-1), set of morphisms from L(j+1)]
123
# a morphism A->B is of the form (DT,halfedgeimages), where
124
# * DT is the number of B in L(j-1) or of A in L(j+1)
125
# * halfedgeimages is a tuple (of length #halfedges(B)) with entries 0,1,...,#halfedges(A)-1, where halfedgeimages[i]=j means halfedge number i of B corresponds to halfedge number j in A
126
# invlist is a list [I0,I1,I2,...] where Ik is a list [invariants,boundaries], where
127
# * invariants is a list of all Gr.invariant() for Gr in Lk
128
# * boundaries is a list [i0,i1,i2,...] of indices such that the Graphs in Lk with invariant number N (in invariants) are G(i(N)+1), ..., G(i(N+1))
129
130
131
@cached_function
132
def degeneration_graph(g, n, rmax=None):
133
if rmax is None:
134
rmax = 3 * g - 3 + n
135
if rmax > 3 * g - 3 + n:
136
rmax = 3 * g - 3 + n
137
138
# initialize deglist and invlist
139
try:
140
deglist, invlist = degeneration_graph.cached(g, n)
141
r0 = 3 * g - 3 + n
142
except KeyError:
143
for r0 in range(3 * g - 3 + n, -2, -1):
144
try:
145
deglist, invlist = degeneration_graph.cached(g, n, r0)
146
break
147
except KeyError:
148
pass
149
150
if r0 == -1: # function has not been called before
151
deglist = [[[stgraph([g], [list(range(1, n + 1))], []), (), set(), set()]]]
152
invlist = [[[deglist[0][0][0].invariant()], [-1, 0]]]
153
154
for r in range(r0 + 1, rmax + 1):
155
deglist += [[]] # add empty list L(r)
156
templist = []
157
158
# first we must degenerate all stgraphs in degree r-1 and record how the degeneration went
159
for i in range(len(deglist[r - 1])):
160
Gr = deglist[r - 1][i][0]
161
deg_Gr = Gr.degenerations()
162
for dGr in deg_Gr:
163
# since all degenerations deg_Gr have the same list of edges as Gr with the new edge appended and since stgraph.halfedges() returns the half-edges in this order, half-edge alpha of Gr corresponds to half-edge alpha in deg_Gr
164
lis = tuple(range(len(deglist[r - 1][i][1])))
165
templist += [[dGr, dGr.halfedges(), set([(i, lis)]), set([])]]
166
167
# now templist contains a temporary list of all possible degenerations of deglist[r-1]
168
# we now proceed to consolidate this list to deglist[r], combining isomorphic entries
169
def inva(o):
170
return o[0].invariant()
171
templist.sort(key=inva)
172
173
# first we reconstruct the blocks of entries having the same graph invariants
174
current_invariant = None
175
tempbdry = []
176
tempinv = []
177
count = 0
178
for Gr in templist:
179
inv = Gr[0].invariant()
180
if inv != current_invariant:
181
tempinv += [inv]
182
tempbdry += [count - 1]
183
current_invariant = inv
184
count += 1
185
tempbdry += [len(templist) - 1]
186
187
# now go through blocks and check for actual isomorphisms
188
count = 0 # counts the number of isomorphism classes already added to deglist[r]
189
190
invlist += [[[], []]]
191
for i in range(len(tempinv)):
192
# go through templist[tempbdry[i]+1], ..., templist[tempbdry[i+1]]
193
isomcl = []
194
for j in range(tempbdry[i] + 1, tempbdry[i + 1] + 1):
195
Gr = templist[j][0] # must check if stgraph Gr is isomorphic to any of the graphs in isomcl
196
new_graph = True
197
for k in range(len(isomcl)):
198
Gr2 = isomcl[k]
199
ans, Iso = Gr.is_isomorphic(Gr2[0], certificate=True)
200
if ans:
201
new_graph = False
202
dicl = Iso[1]
203
# modify isomcl[k] to add new graph upstairs, and record this isomorphism in the data of this upstairs graph
204
upstairs = list(templist[j][2])[0][0] # number of graph upstairs
205
# h runs through the half-edges of the graph upstairs, which are included via the same name in Gr, so dicl translates them to legs of Gr2
206
# and we record the index of this image leg in the list of half-edges of Gr2
207
lisnew = tuple((Gr2[1].index(dicl[h]) for h in deglist[r - 1][upstairs][1]))
208
Gr2[2].add((upstairs, lisnew))
209
deglist[r - 1][upstairs][3].add((count + k, lisnew))
210
break
211
if new_graph:
212
isomcl += [templist[j]]
213
# now also add the info upstairs
214
upstairs = list(templist[j][2])[0][0]
215
lisnew = list(templist[j][2])[0][1]
216
deglist[r - 1][upstairs][3].add((count + len(isomcl) - 1, lisnew))
217
218
deglist[r] += isomcl
219
invlist[r][0] += [isomcl[0][0].invariant()]
220
invlist[r][1] += [count - 1]
221
222
count += len(isomcl)
223
224
invlist[r][1] += [count - 1]
225
226
# remove old entry from cache_function dict if more has been computed now
227
if rmax > r0 and r0 != -1:
228
degeneration_graph.cache.pop(((g, n, r0), ()))
229
230
return (deglist, invlist)
231
232
# finds Gr in the degeneration_graph of the corresponding (g,n), where markdic is a dictionary sending markings of Gr to 1,2,..,n
233
# returns a tuple (r,i,dicv,dicl), where
234
# * the graph isomorphic to Gr is in degeneration_graph(g,n)[0][r][i][0]
235
# * dicv, dicl are dictionaries giving the morphism of stgraphs from Gr to degeneration_graph(g,n)[0][r][i][0]
236
237
238
def deggrfind(Gr, markdic=None):
239
# global Graph1
240
# global Graph2
241
# global Grdeggrfind
242
# Grdeggrfind = deepcopy(Gr)
243
g = Gr.g()
244
n = len(Gr.list_markings())
245
r = Gr.num_edges()
246
247
if markdic is not None:
248
Gr_rename = Gr.copy(mutable=True)
249
Gr_rename.rename_legs(markdic)
250
else:
251
markdic = {i: i for i in range(1, n + 1)}
252
Gr_rename = Gr
253
Gr_rename.set_immutable()
254
Inv = Gr_rename.invariant()
255
(deglist, invlist) = degeneration_graph(g, n, r)
256
257
InvIndex = invlist[r][0].index(Inv)
258
for i in range(invlist[r][1][InvIndex] + 1, invlist[r][1][InvIndex + 1] + 1):
259
# (Graph1,Graph2)= (Gr_rename,deglist[r][i][0])
260
ans, Isom = Gr_rename.is_isomorphic(deglist[r][i][0], certificate=True)
261
if ans:
262
dicl = {l: Isom[1][markdic[l]] for l in markdic}
263
dicl.update({l: Isom[1][l] for l in Gr.halfedges()})
264
return (r, i, Isom[0], dicl)
265
print('Help, cannot find ' + repr(Gr))
266
print((g, n, r))
267
print((Inv, list_strata(2, 2, 0)[0].invariant()))
268
269
270
# returns the list of all A-structures on Gamma, for two stgraphs A,Gamma
271
# the output is a list [(dicv1,dicl1),(dicv2,dicl2),...], where
272
# * dicv are (surjective) dictionaries from the vertices of Gamma to the vertices of A
273
# * dicl are (injective) dictionaries from the legs of A to the legs of Gamma
274
# optional input: identGamma, identA of the form (rA,iA,dicvA,diclA) are result of deggrfind(A,markdic)
275
# TODO: for now assume that markings are 1,2,...,n
276
def Astructures(Gamma, A, identGamma=None, identA=None):
277
if A.num_edges() > Gamma.num_edges():
278
return []
279
g = Gamma.g()
280
mark = Gamma.list_markings()
281
n = len(mark)
282
283
if g != A.g():
284
print('Error, Astructuring graphs of different genera')
285
print(Gamma)
286
print(A)
287
raise ValueError('A very specific bad thing happened')
288
return []
289
if set(mark) != set(A.list_markings()):
290
print('Error, Astructuring graphs with different markings')
291
return []
292
markdic = {mark[i - 1]: i for i in range(1, n + 1)}
293
294
# first identify A and Gamma inside the degeneration graph
295
(deglist, invlist) = degeneration_graph(g, n, Gamma.num_edges())
296
297
if identA is None:
298
(rA, iA, dicvA, diclA) = deggrfind(A, markdic)
299
else:
300
(rA, iA, dicvA, diclA) = identA
301
if identGamma is None:
302
(rG, iG, dicvG, diclG) = deggrfind(Gamma, markdic)
303
else:
304
(rG, iG, dicvG, diclG) = identGamma
305
306
# go through the graph in deglist, starting from Gamma going upwards the necessary amount of steps and collect all morphisms in a set
307
# a morphism is encoded by a tuple (j,(i0,i1,...,im)), where j is the number of the current target of the morphism (in the list deglist[r])
308
# and i0,i1,..,im give the numbers of the half-edges in Gamma, to which the m+1 half-edges of the target correspond
309
310
morphisms = set([(iG, tuple(range(len(deglist[rG][iG][1]))))]) # start with the identity on Gamma
311
312
for r in range(rG, rA, -1):
313
new_morphisms = set()
314
for mor in morphisms:
315
# add to new_morphisms all compositions of mor with a morphism starting at the target of mor
316
for (tar, psi) in deglist[r][mor[0]][2]:
317
composition = tuple((mor[1][j] for j in psi))
318
new_morphisms.add((tar, composition))
319
morphisms = new_morphisms
320
321
# now pick out the morphisms actually landing in position (rA,iA)
322
new_morphisms = set(mor for mor in morphisms if mor[0] == iA)
323
morphisms = new_morphisms
324
325
# at the end we have to precompose with the automorphisms of Gamma
326
Auto = GraphIsom(deglist[rG][iG][0], deglist[rG][iG][0])
327
new_morphisms = set()
328
for (Autov, Autol) in Auto:
329
for mor in morphisms:
330
# now composition is no longer of the format above
331
# instead composition[i] is the label of the half-edge in deglist[rG][iG][0] to which half-edge number i in the target corresponds
332
composition = tuple((Autol[deglist[rG][iG][1][j]] for j in mor[1]))
333
new_morphisms.add((mor[0], composition))
334
morphisms = new_morphisms
335
336
# now we must translate everything back to the original graphs Gamma,A and their labels,
337
# also reconstructing the vertex-action from the half-edge action
338
finalmorphisms = []
339
dicmarkings = {h: h for h in mark}
340
Ahalfedges = A.halfedges()
341
dicAtild = {deglist[rA][iA][1][i]: i for i in range(len(deglist[rA][iA][1]))}
342
diclGinverse = {diclG[h]: h for h in Gamma.halfedges()}
343
344
for (targ, mor) in morphisms:
345
dicl = {h: diclGinverse[mor[dicAtild[diclA[h]]]] for h in Ahalfedges} # final dictionary for the half-edges
346
dicl.update(dicmarkings)
347
348
# now reconstruct dicv from this
349
if A.num_verts() == 1:
350
dicv = {ver: 0 for ver in range(Gamma.num_verts())}
351
else:
352
# Now we pay the price for not recording the morphisms on the vertices before
353
# we need to reconstruct it by the known morphisms of half-edges by determining the connected components of the complement of beta(H_A)
354
# TODO: do we want to avoid this reconstruction by more bookkeeping?
355
356
dicv = {Gamma.vertex(dicl[h]): A.vertex(h) for h in dicl}
357
remaining_vertices = set(range(Gamma.num_verts())) - set(dicv)
358
remaining_edges = set(e for e in Gamma.edges(copy=False)
359
if e[0] not in dicl.values())
360
current_vertices = set(dicv)
361
362
while remaining_vertices:
363
newcurrent_vertices = set()
364
for e in list(remaining_edges):
365
if Gamma.vertex(e[0]) in current_vertices:
366
vnew = Gamma.vertex(e[1])
367
remaining_edges.remove(e)
368
# if vnew is in current vertices, we don't have to do anything (already know it)
369
# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)
370
if vnew not in current_vertices:
371
dicv[vnew] = dicv[Gamma.vertex(e[0])]
372
remaining_vertices.discard(vnew)
373
newcurrent_vertices.add(vnew)
374
continue
375
if Gamma.vertex(e[1]) in current_vertices:
376
vnew = Gamma.vertex(e[0])
377
remaining_edges.remove(e)
378
# if vnew is in current vertices, we don't have to do anything (already know it)
379
# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)
380
if vnew not in current_vertices:
381
dicv[vnew] = dicv[Gamma.vertex(e[1])]
382
remaining_vertices.discard(vnew)
383
newcurrent_vertices.add(vnew)
384
current_vertices = newcurrent_vertices
385
# now dicv and dicl are done, so they are added to the list of known A-structures
386
finalmorphisms += [(dicv, dicl)]
387
return finalmorphisms
388
389
390
# find a list commdeg of generic (G1,G2)-stgraphs G3
391
# Elements of this list will have the form (G3,vdict1,ldict1,vdict2,ldict2), where
392
# * G3 is a stgraph
393
# * vdict1, vdict2 are the vertex-maps from vertices of G3 to G1 and G2
394
# * ldict1, ldict2 are the leg-maps from legs of G1 and G2 to G3
395
# modiso = True means we give a list of generic (G1,G2)-stgraphs G3 up to isomorphisms of G3
396
# if rename=True, the operation will also work if the markings of G1,G2 are not labelled 1,2,...,n; since this involves more bookkeeping, rename=False should run slightly faster
397
def common_degenerations(G1, G2, modiso=False, rename=False):
398
# TODO: fancy graph-theoretic implementation in case that G1 has a single edge?
399
400
# almost brute force implementation below
401
g = G1.g()
402
mkings = G1.list_markings()
403
n = len(mkings)
404
405
# for convenience, we want r1 <= r2
406
switched = False
407
if G1.num_edges() > G2.num_edges():
408
temp = G1
409
G1 = G2
410
G2 = temp
411
switched = True
412
413
if G1.num_edges() == 0 and modiso:
414
# G1 is trivial graph, we want to avoid having to compute entire degeneration tree here
415
if switched:
416
return [(G2, {v: v for v in range(G2.num_verts())}, {l: l for l in G2.leglist()}, {v: 0 for v in range(G2.num_verts())}, {l: l for l in mkings})]
417
else:
418
return [(G2, {v: 0 for v in range(G2.num_verts())}, {l: l for l in mkings}, {v: v for v in range(G2.num_verts())}, {l: l for l in G2.leglist()})]
419
420
if rename:
421
markdic = {mkings[i]: i + 1 for i in range(n)}
422
renamedicl1 = {l: l + n + 1 for l in G1.leglist()}
423
renamedicl2 = {l: l + n + 1 for l in G2.leglist()}
424
renamedicl1.update(markdic)
425
renamedicl2.update(markdic)
426
427
G1 = G1.relabel({}, renamedicl1, mutable=False)
428
G2 = G2.relabel({}, renamedicl2, mutable=False)
429
430
(r1, i1, dicv1, dicl1) = deggrfind(G1)
431
(r2, i2, dicv2, dicl2) = deggrfind(G2)
432
433
(deglist, invlist) = degeneration_graph(g, n, r1 + r2)
434
435
commdeg = []
436
437
descendants1 = set([i1])
438
for r in range(r1 + 1, r2 + 1):
439
descendants1 = set(d[0] for i in descendants1
440
for d in deglist[r - 1][i][3])
441
442
descendants2 = set([i2])
443
444
for r in range(r2, r1 + r2 + 1):
445
# look for intersection of descendants1, descendants2 and find generic (G1,G2)-struct
446
for i in descendants1.intersection(descendants2):
447
Gamma = deglist[r][i][0]
448
Gammafind = (r, i, {j: j for j in range(Gamma.num_verts())}, {l: l for l in Gamma.leglist()})
449
G1structures = Astructures(Gamma, G1, identGamma=Gammafind, identA=(r1, i1, dicv1, dicl1))
450
G2structures = Astructures(Gamma, G2, identGamma=Gammafind, identA=(r2, i2, dicv2, dicl2))
451
452
if modiso is True:
453
AutGamma = GraphIsom(Gamma, Gamma)
454
AutGammatild = [(dicv, {dicl[l]:l for l in dicl}) for (dicv, dicl) in AutGamma]
455
tempdeg = []
456
457
# now need to identify the generic G1,G2-structures
458
numlegs = len(Gamma.leglist())
459
for (dv1, dl1) in G1structures:
460
for (dv2, dl2) in G2structures:
461
numcoveredlegs = len(set(list(dl1.values()) + list(dl2.values())))
462
if numcoveredlegs == numlegs:
463
if switched:
464
if rename:
465
tempdeg.append((Gamma, dv2, {l: dl2[renamedicl2[l]] for l in renamedicl2}, dv1, {
466
l: dl1[renamedicl1[l]] for l in renamedicl1}))
467
else:
468
tempdeg.append((Gamma, dv2, dl2, dv1, dl1))
469
else:
470
if rename:
471
tempdeg.append((Gamma, dv1, {l: dl1[renamedicl1[l]] for l in renamedicl1}, dv2, {
472
l: dl2[renamedicl2[l]] for l in renamedicl2}))
473
else:
474
tempdeg.append((Gamma, dv1, dl1, dv2, dl2))
475
476
if modiso:
477
# eliminate superfluous isomorphic elements from tempdeg
478
while tempdeg:
479
(Gamma, dv1, dl1, dv2, dl2) = tempdeg.pop(0)
480
commdeg.append((Gamma, dv1, dl1, dv2, dl2))
481
for (dicv, dicltild) in AutGammatild:
482
try:
483
tempdeg.remove((Gamma, {v: dv1[dicv[v]] for v in dicv}, {l: dicltild[dl1[l]] for l in dl1}, {
484
v: dv2[dicv[v]] for v in dicv}, {l: dicltild[dl2[l]] for l in dl2}))
485
except ValueError:
486
pass
487
else:
488
commdeg += tempdeg
489
490
# (except in last step) compute new descendants
491
if r < r1 + r2:
492
descendants1 = set(d[0] for i in descendants1 for d in deglist[r][i][3])
493
descendants2 = set(d[0] for i in descendants2 for d in deglist[r][i][3])
494
495
return commdeg
496
497
498
# Gstgraph is a class modelling stable graphs Gamma with action of a group G and character data at legs
499
#
500
# Gamma.G = finite group acting on Gamma
501
# Gamma.gamma = underlying stable graph gamma of type stgraph
502
# Gamma.vertact and G.legact = dictionary, assigning to tuples (g,x) of g in G and x a vertex/leg a new vertex/leg
503
# Gamma.character = dictionary, assigning to leg l a tuple (h,e,k) of
504
# * a generator h in G of the stabilizer of l
505
# * the order e of the stabilizer
506
# * an integer 0<=k<e such that h acts on the tangent space of the curve at the leg l by exp(2 pi i/e * k)
507
508
class Gstgraph:
509
def __init__(self, G, gamma, vertact, legact, character, hdata=None):
510
self.G = G
511
self.gamma = gamma
512
self.vertact = vertact
513
self.legact = legact
514
self.character = character
515
if hdata is None:
516
self.hdata = self.hurwitz_data()
517
else:
518
self.hdata = hdata
519
520
def copy(self, mutable=True):
521
G = Gstgraph.__new__(Gstgraph)
522
G.G = self.G
523
G.gamma = self.gamma.copy(mutable=mutable)
524
G.vertact = self.vertact.copy()
525
G.legact = self.legact.copy()
526
G.character = self.character.copy()
527
return G
528
529
def __repr__(self):
530
return repr(self.gamma)
531
532
def __deepcopy__(self, memo):
533
raise RuntimeError("do not deepcopy")
534
return Gstgraph(self.G, deepcopy(self.gamma), deepcopy(self.vertact), deepcopy(self.legact), deepcopy(self.character), hdata=deepcopy(self.hdata))
535
536
# returns dimension of moduli space at vertex v; if v is None, return dimension of entire stratum for self
537
def dim(self, v=None):
538
if v is None:
539
return self.quotient_graph().dim()
540
# TODO: optimization potential
541
return self.extract_vertex(v).dim()
542
543
# renames legs according to dictionary di
544
# TODO: no check that this renaming is legal, i.e. produces well-defined graph
545
def rename_legs(self, di):
546
if not self.gamma.is_mutable():
547
self.gamma = self.gamma.copy()
548
self.gamma.relabel({}, di, inplace=True)
549
temp_legact = {}
550
temp_character = {}
551
for k in self.legact:
552
# the operation temp_legact[(k[0],k[1])]=self.legact[k] would produce copy of self.legact, di.get replaces legs by new name, if applicable, and leaves leg invariant otherwise
553
temp_legact[(k[0], di.get(k[1], k[1]))] = di.get(self.legact[k], self.legact[k])
554
for k in self.character:
555
temp_character[di.get(k, k)] = self.character[k]
556
557
# returns the stabilizer group of vertex i
558
def vstabilizer(self, i):
559
gen = []
560
for g in self.G:
561
if self.vertact[(g, i)] == i:
562
gen += [g]
563
try:
564
return self.G.ambient_group().subgroup(gen)
565
except AttributeError:
566
return self.G.subgroup(gen)
567
568
# returns the stabilizer group of leg j
569
def lstabilizer(self, j):
570
try:
571
return self.G.ambient_group().subgroup([self.character[j][0]])
572
except AttributeError:
573
return self.G.subgroup([self.character[j][0]])
574
575
# converts self into a prodHclass (with gamma0 being the corresponding trivial graph)
576
577
def to_prodHclass(self):
578
return prodHclass(stgraph([self.gamma.g()], [list(self.gamma.list_markings())], []), [self.to_decHstratum()])
579
580
# converts self into a decHstratum (with gamma0 being the corresponding trivial graph)
581
def to_decHstratum(self):
582
dicv = {}
583
dicvinv = {}
584
dicl = {}
585
quotgr = self.quotient_graph(dicv, dicvinv, dicl)
586
587
spaces = {v: (self.gamma.genera(dicvinv[v]), HurwitzData(self.vstabilizer(dicvinv[v]), [
588
self.character[l] for l in quotgr.legs(v, copy=False)])) for v in range(quotgr.num_verts())}
589
590
masterdicl = {}
591
592
for v in range(quotgr.num_verts()):
593
n = 1
594
gord = spaces[v][1].G.order()
595
tGraph = trivGgraph(*spaces[v])
596
vinv = dicvinv[v]
597
598
# for l in quotgr.legs(v) find one leg l' in the orbit of l which belongs to vinv
599
legreps = []
600
for l in quotgr.legs(v, copy=False):
601
for g in self.G:
602
if self.legact[(g, l)] in self.gamma.legs(vinv, copy=False):
603
legreps.append(self.legact[(g, l)])
604
break
605
606
# make a list quotrep of representatives of self.G / spaces[v][1].G (quotient by Stab(vinv))
607
(quotrep, di) = leftcosetaction(self.G, spaces[v][1].G)
608
609
for l in legreps:
610
for g in spaces[v][1].G:
611
for gprime in quotrep:
612
masterdicl[self.legact[(gprime * g, l)]] = tGraph.legact[(g, n)]
613
n += ZZ(gord) // self.character[l][1]
614
615
vertdata = []
616
for w in range(self.gamma.num_verts()):
617
a = dicv[w]
618
619
wdicl = {l: masterdicl[l] for l in self.gamma.legs(w, copy=False)}
620
621
vertdata.append([a, wdicl])
622
623
return decHstratum(self.gamma.copy(mutable=False), spaces, vertdata)
624
625
# takes vertex v_i of the stable graph and returns a one-vertex Gstgraph with group G_{v_i} and all the legs and self-loops attached to v_i
626
def extract_vertex(self, i):
627
G_new = self.vstabilizer(i)
628
lgs = self.gamma.legs(i, copy=True) # takes list of legs attached to i
629
egs = self.gamma.edges_between(i, i)
630
gamma_new = stgraph([self.gamma.genera(i)], [lgs], egs)
631
632
vertact_new = {}
633
for g in G_new:
634
vertact_new[(g, 0)] = 0
635
636
legact_new = {}
637
for g in G_new:
638
for j in lgs:
639
legact_new[(g, j)] = self.legact[(g, j)]
640
641
character_new = {}
642
for j in lgs:
643
character_new[j] = self.character[j] # self.character[j] is a tuple, so no deepcopy is required
644
645
return Gstgraph(G_new, gamma_new, vertact_new, legact_new, character_new)
646
647
# glues the Gstgraph Gr (with group H) at the vertex i of a Gstgraph self
648
# and performs this operation equivariantly under the G-action on self
649
# all legs at the elements of the orbit of i are not renamed and the G-action and character on them is not changed
650
# optional arguments: if divGr/dil are given they are supposed to be a dictionary, which will be cleared and updated with the renaming-convention to pass from leg/vertex-names in Gr to leg/vertex-names in the glued graph (at former vertex i)
651
# similarly, divs will be a dictionary assigning vertex numbers in the old self (not in the orbit of i) the corresponding number in the new self
652
# necessary conditions:
653
# * the stabilizer of vertex i in self = H
654
# * every leg of i is also a leg in Gr
655
# note that legs of Gr which belong to an edge in Gr are not identified with legs of self, even if they have the same name
656
def equivariant_glue_vertex(self, i, Gr, divGr={}, divs={}, dil={}):
657
divGr.clear()
658
divs.clear()
659
dil.clear()
660
661
for j in range(self.gamma.num_verts()):
662
divs[j] = j
663
664
G = self.G
665
H = Gr.G
666
667
# the orbit of vertex i is given by gH*i for g in L
668
(L, di) = leftcosetaction(G, H)
669
oldvertno = self.gamma.num_verts() - len(L) # number of old vertices that will remain in end
670
671
# first we rename every leg in Gr that is not a leg of i such that the new leg-names do not appear in self
672
# TODO: to be changed (access to private _maxleg attribute)
673
Gr_mod = Gr.copy()
674
m = max(self.gamma._maxleg, Gr.gamma._maxleg)
675
676
a = set().union(*Gr.gamma.legs(copy=False)) # legs of Gr
677
b = set(self.gamma.legs(i, copy=False)) # legs of self at vertex i
678
e = a - b # contains the set of legs of Gr that are not legs of vertex i
679
augment_dic = {}
680
for l in e:
681
m += 1
682
augment_dic[l] = m
683
684
Gr_mod.rename_legs(augment_dic)
685
686
# collects the dictionaries translating legs in Gr_mod not belonging to vertex i to legs in the glued graph at elements g in L
687
legdiclist = {}
688
689
# records orbit of i under elements of L
690
orbi = {}
691
oldlegsi = self.gamma.legs(i, copy=False)
692
693
# go through the orbit of i
694
for g in L:
695
orbi[g] = self.vertact[(g, i)]
696
# create a translated copy of Gr using the information of the G-action on self
697
Gr_transl = Gr_mod.copy()
698
transl_dic = {l: self.legact[(g, l)] for l in oldlegsi}
699
Gr_transl.rename_legs(transl_dic)
700
701
# glue G_transl to vertex g*i of self (use divs to account for deletions in meantime) and remember how internal edges are relabelled
702
divGr_temp = {}
703
divs_temp = {}
704
dil_temp = {}
705
self.gamma.glue_vertex(divs[self.vertact[(g, i)]], Gr_transl.gamma,
706
divGr=divGr_temp, divs=divs_temp, dil=dil_temp)
707
708
# update various dictionaries now
709
divs.pop(self.vertact[(g, i)]) # no longer needed
710
for j in divs:
711
divs[j] = divs_temp[divs[j]]
712
legdiclist.update({(g, j): dil_temp[j] for j in dil_temp})
713
714
# adjust vertact
715
# ERROR here, from renaming vetices -> rather use vertact_reconstruct()
716
# for g in L:
717
# for h in G:
718
# self.vertact.pop((h,orbi[g])) # remove action of h on g*i
719
#
720
#
721
# for g1 in range(len(L)):
722
# for g2 in range(len(L)):
723
# for h in H:
724
# for j in range(len(Gr.gamma.genera)):
725
# # record where vertex number j in Gr(translated by g1) goes under g2*h
726
# self.vertact[(L[g2]*h,oldvertno+g1*len(Gr.gamma.genera)+j)]=oldvertno+di[(L[g2]*h,g1)]*len(Gr.gamma.genera)+j
727
728
# adjust legact and character
729
for j in e:
730
for g1 in range(len(L)):
731
# we are adjusting here the data for the leg corresponding to j in Gr that was glued to vertex g1*i
732
for g2 in range(len(L)):
733
for h in H:
734
# the element g2*h*(g1)**{-1} acts on g1*i resulting in g2*h*i
735
# for g1 fixed, the expression g2*h*(g1)^{-1} runs through the elements of G
736
self.legact[(L[g2] * h * L[g1].inverse(), legdiclist[(L[g1], augment_dic[j])])
737
] = legdiclist[(L[g2], augment_dic[Gr.legact[(h, j)]])]
738
# character given by conjugation
739
self.character[legdiclist[(L[g1], augment_dic[j])]] = (
740
L[g1] * Gr.character[j][0] * L[g1].inverse(), Gr.character[j][1], Gr.character[j][2])
741
742
self.vertact_reconstruct()
743
744
dil.update({j: legdiclist[(L[0], augment_dic[j])] for j in e}) # here we assume that L[0]=id_G
745
divGr.update({j: oldvertno + j for j in range(Gr.gamma.num_verts())})
746
747
def quotient_graph(self, dicv={}, dicvinv={}, dicl={}, relabel=False):
748
r"""
749
Computes the quotient graph of self under the group action.
750
751
dicv, dicl are dictionaries that record the quotient map of vertices and legs,
752
dicvinv is a dictionary giving some section of dicv (going from vertices of
753
quotient to vertices of self).
754
If relabel=False, the legs of the quotient graph are labeled with the
755
label of their smallest preimage. Otherwise, the labels are normalized
756
to 1,...,n such that the ordering of the labels is preserved.
757
758
EXAMPLES::
759
760
sage: from admcycles.admcycles import trivGgraph, HurData
761
sage: G = PermutationGroup([(1, 2)])
762
sage: H = HurData(G, [G[1], G[1], G[1], G[1]])
763
sage: tG = trivGgraph(1, H); tG.quotient_graph()
764
[0] [[1, 2, 3, 4]] []
765
sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[0]])
766
sage: tG = trivGgraph(1, H); tG.quotient_graph()
767
[0] [[1, 2, 3, 4, 5]] []
768
sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[1]])
769
sage: tG = trivGgraph(1, H); tG.quotient_graph() # no such cover exists (RH formula)
770
Traceback (most recent call last):
771
...
772
ValueError: Riemann-Hurwitz formula not satisfied in this Gstgraph.
773
774
TESTS::
775
776
sage: from admcycles.admcycles import trivGgraph, HurData
777
sage: G = PermutationGroup([(1, 2, 3)])
778
sage: H = HurData(G, [G[1]])
779
sage: tG = trivGgraph(2, H); tG.quotient_graph()
780
[1] [[1]] []
781
sage: H = HurData(G, [G[1] for i in range(6)])
782
sage: tG = trivGgraph(1, H); tG.quotient_graph() # quotient vertex would have genus -1
783
Traceback (most recent call last):
784
...
785
ValueError: Riemann-Hurwitz formula not satisfied in this Gstgraph.
786
787
Check issues related to previous bug in quotient_graph:
788
789
sage: from admcycles.admcycles import StableGraph, HurData, trivGgraph, Htautclass, Hdecstratum
790
sage: gamma = StableGraph([0, 0, 1], [[1, 2, 5], [3, 6, 14], [13]], [(5, 6), (13, 14)])
791
sage: G = SymmetricGroup(2)
792
sage: H = HurData(G,[G[1],G[1],G[0]])
793
sage: trivGg = trivGgraph(2, H)
794
sage: trivGclass = Htautclass([Hdecstratum(trivGg)])
795
sage: A = trivGclass.quotient_pullback(gamma.to_tautological_class())
796
sage: all(u.Gr.quotient_graph().is_isomorphic(gamma) for u in A.terms)
797
True
798
799
Check that the markings of the quotient graphs are the same for all graphs in a given stratum.
800
801
sage: from admcycles.admcycles import HurData, list_Hstrata
802
sage: G = CyclicPermutationGroup(4)
803
sage: g = G.gen()
804
sage: H = HurData(G, [g, g^2, g^3, g^2])
805
sage: all(sorted(G.quotient_graph().list_markings()) == [1, 2, 4, 5] for G in list_Hstrata(2, H, 1))
806
True
807
"""
808
dicv.clear()
809
dicvinv.clear()
810
dicl.clear()
811
812
vertlist = list(range(self.gamma.num_verts()))
813
leglist = []
814
for v in vertlist:
815
leglist += self.gamma.legs(v, copy=False)
816
817
countv = 0
818
# compute orbits of G-action
819
for v in vertlist[:]:
820
if v in vertlist: # has not yet been part of orbit of previous point
821
dicvinv[countv] = v
822
for g in self.G:
823
if self.vertact[(g, v)] in vertlist:
824
dicv[self.vertact[(g, v)]] = countv
825
vertlist.remove(self.vertact[(g, v)])
826
countv += 1
827
828
for l in leglist[:]:
829
if l in leglist: # has not yet been part of orbit of previous point
830
orbit = set(self.legact[(g, l)] for g in self.G)
831
min_l = min(orbit)
832
for ll in orbit:
833
# note that leg-names in quotient graph equal the name of one preimage in self
834
# and as a "canonical" choice we choose the smallest one
835
dicl[ll] = min_l
836
leglist.remove(ll)
837
838
# create new stgraph with vertices/legs given by values of dicv, dicl
839
quot_leg = [[] for j in range(countv)] # list of legs of quotient
840
legset = set(dicl.values())
841
for l in legset:
842
quot_leg[dicv[self.gamma.vertex(l)]] += [l]
843
844
quot_genera = []
845
for v in range(countv):
846
Gv = self.vstabilizer(dicvinv[v]).order()
847
# self.character[l][1] = e = order of Stab_l
848
b = sum([1 - QQ(1) / (self.character[l][1]) for l in quot_leg[v]])
849
# genus of quotient vertex by Riemann-Hurwitz formula
850
qgenus = ((2 * self.gamma.genera(dicvinv[v]) - 2) / QQ(Gv) + 2 - b) / QQ(2)
851
if not (qgenus.is_integer() and qgenus >= 0):
852
raise ValueError('Riemann-Hurwitz formula not satisfied in this Gstgraph.')
853
quot_genera += [ZZ(qgenus)]
854
855
unused_legs = legset.copy()
856
quot_edges = []
857
for e in self.gamma.edges(copy=False):
858
if e[0] in unused_legs:
859
quot_edges += [(e[0], dicl[e[1]])]
860
unused_legs.remove(e[0])
861
unused_legs.remove(dicl[e[1]])
862
863
quot_graph = stgraph(quot_genera, quot_leg, quot_edges, mutable=True)
864
if relabel:
865
markings = sorted(quot_graph.list_markings())
866
legdict = {old: new + 1 for new, old in enumerate(markings)}
867
max_marking = max(legdict.values())
868
halfedges = quot_graph.halfedges()
869
for h in halfedges:
870
assert h > max_marking
871
legdict[h] = h
872
quot_graph.relabel({}, legdict, inplace=True)
873
dicl_new = {l1: legdict[l2] for l1, l2 in dicl.items()}
874
dicl.clear()
875
dicl.update(dicl_new)
876
quot_graph.tidy_up()
877
quot_graph.set_immutable()
878
879
return quot_graph
880
881
# returns Hurwitz data corresponding to the Hurwitz space in which self is a boundary stratum
882
# HOWEVER: since names of legs are not necessarily canonical, there is no guarantee that they will be compatible
883
# i.e. self is not necessarily a degeneration of trivGgraph(g,hurwitz_data(self))
884
def hurwitz_data(self):
885
quotgr = self.quotient_graph()
886
marks = sorted(quotgr.list_markings())
887
return HurwitzData(self.G, [self.character[l] for l in marks])
888
889
# TODO: currently ONLY works for cyclic groups G
890
def delta_degree(self, v):
891
r"""
892
Gives the degree of the delta-map from the Hurwitz space parametrizing the curve
893
placed on vertex v to the corresponding stable-maps space.
894
895
Note: currently only works for cyclic groups G.
896
897
EXAMPLES::
898
899
sage: from admcycles.admcycles import trivGgraph, HurData
900
sage: G = PermutationGroup([(1, 2)])
901
sage: H = HurData(G, [G[1], G[1], G[1], G[1]])
902
sage: tG = trivGgraph(1, H); tG.delta_degree(0) # unique cover with 2 automorphisms
903
1/2
904
sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[0]])
905
sage: tG = trivGgraph(1, H); tG.delta_degree(0) # unique cover with 1 automorphism
906
1
907
sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[1]])
908
sage: tG = trivGgraph(1, H); tG.delta_degree(0) # no such cover exists (RH formula)
909
0
910
sage: G = PermutationGroup([(1, 2, 3)])
911
sage: H = HurData(G, [G[1]])
912
sage: tG = trivGgraph(2, H); tG.delta_degree(0) # no such cover exists (repr. theory)
913
0
914
"""
915
if self.gamma.num_verts() > 1:
916
return self.extract_vertex(v).delta_degree(0)
917
try:
918
quotgr = self.quotient_graph()
919
except ValueError: # Riemann-Hurwitz formula not satisfied, so space empty and degree 0
920
return 0
921
gprime = quotgr.genera(0)
922
n = self.G.order()
923
924
# verify that Hurwitz space is nonempty #TODO: this assumes G cyclic (like the rest of the function)
925
if (n == 2 and len([1 for l in quotgr.legs(0, copy=False) if self.character[l][1] == 2]) % 2) or (n > 2 and prod([self.character[l][0]**(ZZ(self.character[l][2]).inverse_mod(n)) for l in quotgr.legs(0, copy=False) if self.character[l][1] != 1]) != self.G.one()):
926
return 0
927
928
# compute number of homomorphisms of pi_1(punctured base curve) to G giving correct Hurwitz datum by inclusion-exclusion type formula
929
sigmagcd = QQ(n) / lcm([self.character[l][1] for l in quotgr.legs(0, copy=False)])
930
primfac = set(sigmagcd.ceil().prime_factors())
931
numhom = 0
932
for S in Subsets(primfac):
933
numhom += (-1)**len(S) * (QQ(n) / prod(S))**(2 * gprime)
934
935
# TODO: why not divide by gcd([self.character[l][1] for l in quotgr.legs(0)]) instead???
936
return numhom * prod([QQ(n) / self.character[l][1] for l in quotgr.legs(0, copy=False)]) / QQ(n)
937
938
# reconstructs action of G on vertices from action on legs. Always possible for connected stable graphs
939
def vertact_reconstruct(self):
940
if self.gamma.num_verts() == 1:
941
# only one vertex means trivial action
942
self.vertact = {(g, 0): 0 for g in self.G}
943
else:
944
self.vertact = {(g, v): self.gamma.vertex(self.legact[(g, self.gamma.legs(
945
v, copy=False)[0])]) for g in self.G for v in range(self.gamma.num_verts())}
946
947
def degenerations(self, dege, contracted_edge):
948
r"""
949
For a codimension 1 degeneration dege of self.quotient_graph(relabel=True) list all
950
degenerations [G0, ...] of self such that Gi.quotient_graph(relabel=True) is isomorphic to dege.
951
contracted_edge is the edge of dege that needs to be contracted to obtain
952
a graph that is isomorphic to self.quotient_graph(rename=True).
953
If dege has more than two vertices, we do not guarantee that the graphs Gi are
954
unique up to isomorphism.
955
"""
956
957
dicv = {}
958
dicvinv = {}
959
dicl = {}
960
quot_Graph = self.quotient_graph(dicv=dicv, dicvinv=dicvinv, dicl=dicl, relabel=True)
961
962
# We need for each leg of quot_Graph the preimages of this leg in self
963
dicl_pre = {l: [] for l in dicl.values()}
964
for l1, l2 in dicl.items():
965
dicl_pre[l2].append(l1)
966
967
# A priory dege_contr is only isomorphic to self.quotient_graph
968
# We rename the edges of dege such that dege_contr IS self.quotient_graph
969
dege_contr = dege.copy(mutable=True)
970
dege_contr.contract_edge(contracted_edge)
971
is_iso, dicts = dege_contr.is_isomorphic(quot_Graph, certificate=True)
972
if not is_iso:
973
raise ValueError("The supplied graph and edge contraction do not give rise to a graph isomorphic to self.quotient_graph(relabel=True)")
974
(_, legdict) = dicts
975
max_leg = max(legdict.values())
976
legdict[contracted_edge[0]] = max_leg + 1
977
legdict[contracted_edge[1]] = max_leg + 2
978
dege = dege.relabel({}, legdict)
979
contracted_edge = (max_leg + 1, max_leg + 2)
980
981
# if self has more than one vertex, extract v and see it as a Gstgraph with
982
# its own stabilizer group as symmetries and then split up according
983
# to the classification of boundary divisors in Hurwitz stacks.
984
# Then all possible degenerations of v are glued back in equivariantly
985
# to the original graph
986
if self.gamma.num_verts() > 1:
987
# determine a vertex v of self that lies in the fiber above the
988
# vertex of self.quotient_graph() the edge is contracted to.
989
v = None
990
v1_dege = dege.vertex(contracted_edge[0])
991
v2_dege = dege.vertex(contracted_edge[1])
992
if quot_Graph.num_verts() == 1:
993
# Any vertex of self is ok
994
v = 0
995
else:
996
# At least on of the vertices of dege adjacent to the contracted
997
# edge has an additional adjacent leg. The vertex of
998
# self.quotient_graph adjacent to the image of this leg
999
# is the vertex we are looking for.
1000
l_dege = None
1001
for legs in (dege.legs(v1_dege, copy=False), dege.legs(v2_dege, copy=False)):
1002
if len(legs) <= 1:
1003
continue
1004
l_dege = [l for l in legs if l not in contracted_edge][0]
1005
break
1006
assert l_dege is not None
1007
v_quot = None
1008
for v_tmp, legs in enumerate(quot_Graph.legs(copy=False)):
1009
if l_dege in legs:
1010
v_quot = v_tmp
1011
break
1012
assert v_quot is not None
1013
v = None
1014
for v1, v2 in dicv.items():
1015
if v2 == v_quot:
1016
v = v1
1017
break
1018
assert v is not None
1019
1020
v0 = self.extract_vertex(v)
1021
1022
# In theory we need to extract the corresponding subgraph from dege,
1023
# but we need to adjust the markings: leg i of self is mapped to
1024
# some leg j1 in self.quotien_graph(), but to some other leg
1025
# j2 in v0.quotient_graph(). Hence we need to relabel leg j1 of
1026
# the extracted subgraph to j2. While doing so, we need to make
1027
# sure that the new edge is relabled to some valid value.
1028
v_tmp = [v1_dege, v2_dege] if v1_dege != v2_dege else [v1_dege]
1029
dege_sub = dege.extract_subgraph(v_tmp, rename=False, mutable=True)[0]
1030
dicl_v0 = {}
1031
v0_quot = v0.quotient_graph(dicl=dicl_v0, relabel=True)
1032
1033
# For the relevant legs j1 in self.quotient_graph() we need a pre-
1034
# image i that is actually contained in v0.
1035
dicl_pre_v0_section = {}
1036
legs_v0 = flatten(v0.gamma.legs(copy=False))
1037
for l1, ls in dicl_pre.items():
1038
for l2 in ls:
1039
if l2 in legs_v0:
1040
dicl_pre_v0_section[l1] = l2
1041
break
1042
lm = {l: dicl_v0[dicl_pre_v0_section[l]]
1043
for l in flatten(dege_sub.legs(copy=True))
1044
if l in dicl_pre_v0_section}
1045
max_leg = max(lm.values())
1046
lm[contracted_edge[0]] = max_leg + 1
1047
lm[contracted_edge[1]] = max_leg + 2
1048
dege_sub.relabel({}, lm, inplace=True)
1049
contracted_edge_sub = (max_leg + 1, max_leg + 2)
1050
# We need to remove all the edges that are not the edges of the
1051
# quotient of v0 or the contracted edge (those edges are the images
1052
# of edges between different G-images of v0).
1053
dege_sub._edges = v0_quot.edges() + [contracted_edge_sub]
1054
dege_sub.set_immutable()
1055
1056
L = v0.degenerations(dege_sub, contracted_edge_sub)
1057
M = [self.copy() for j in range(len(L))]
1058
for j in range(len(L)):
1059
M[j].equivariant_glue_vertex(v, L[j])
1060
return M
1061
1062
if self.gamma.num_verts() == 1:
1063
quot_degen = [dege]
1064
# If self has a self-loop that is connecting both vertices
1065
# of dege, we need to consider both possible "orientations" as
1066
# the G-action on each end differs by sign.
1067
# TODO: There is some optimization potential: If there are multiple
1068
# such edges, we may not need to consider all the possible combinations.
1069
if dege.num_verts() == 2:
1070
edges_between = [tuple(sorted(e)) for e in dege.edges_between(0, 1)]
1071
edges_between.remove(contracted_edge)
1072
if edges_between:
1073
for edges in itertools.chain.from_iterable(itertools.combinations(edges_between, r) for r in range(1, len(edges_between) + 1)):
1074
1075
lm = {}
1076
for l1, l2 in edges:
1077
lm[l1] = l2
1078
lm[l2] = l1
1079
quot_degen.append(dege.relabel({}, lm))
1080
degen = [] # list of degenerations
1081
1082
if self.G.is_cyclic():
1083
# first create dictionaries GtoNum, NumtoG translating elements of G to numbers 0,1, ..., G.order()-1
1084
G = self.G
1085
n = G.order()
1086
1087
# cumbersome method to obtain some generator. since G.gens() is not guaranteed to be minimal, go through and check order
1088
Ggenerators = G.gens()
1089
for ge in Ggenerators:
1090
if ge.order() == n:
1091
sigma = ge
1092
break
1093
1094
# now sigma is fixed generator of G, want to have dictionaries such that sigma <-> 1, sigma**2 <-> 2, ..., sigma**n <-> 0
1095
GtoNum = {}
1096
NumtoG = {}
1097
mu = sigma
1098
1099
for j in range(1, n + 1):
1100
GtoNum[mu] = j % n
1101
NumtoG[j % n] = mu
1102
mu = mu * sigma
1103
1104
# extract data of e_alpha, k_alpha, nu_alpha, where alpha runs through legs of dege
1105
e = {}
1106
k = {}
1107
nu = {}
1108
1109
for dege in quot_degen:
1110
# all legs in dege come from legs of self, except the one added by the degeneration
1111
legs = set(dege.leglist())
1112
legs.difference_update(contracted_edge)
1113
for alpha in legs:
1114
alpha_pre = dicl_pre[alpha][0]
1115
e[alpha] = self.character[alpha_pre][1]
1116
r = (GtoNum[self.character[alpha_pre][0]] * e[alpha] / QQ(n)).floor()
1117
k[alpha] = (self.character[alpha_pre][2] * r.inverse_mod(e[alpha])) % e[alpha]
1118
nu[alpha] = k[alpha].inverse_mod(e[alpha]) # case e[alpha]=1 works, produces nu[alpha]=0
1119
1120
if dege.num_verts() == 2:
1121
I1 = dege.legs(0, copy=True)
1122
I2 = dege.legs(1, copy=True)
1123
if contracted_edge[0] in I1:
1124
I1.remove(contracted_edge[0])
1125
I2.remove(contracted_edge[1])
1126
else:
1127
I1.remove(contracted_edge[1])
1128
I2.remove(contracted_edge[0])
1129
I = I1 + I2
1130
1131
n1_min = lcm([e[alpha] for alpha in I1])
1132
n2_min = lcm([e[alpha] for alpha in I2])
1133
1134
# TODO: following for-loop has great optimization potential, but current form should be sufficient for now
1135
for n1_add in range(1, ZZ(n) // n1_min + 1):
1136
for n2_add in range(1, ZZ(n) // n2_min + 1):
1137
n1 = n1_add * n1_min # nj is the order of the stabilizer of the preimage of vertex j-1 in dege
1138
n2 = n2_add * n2_min
1139
# lcm condition from connectedness of final graph
1140
if n1.divides(n) and n2.divides(n) and lcm(n1, n2) == n:
1141
# we must make sure that the order e of the stabilizer of the preimage of the separating edge and the
1142
# corresponding characters (described by k1,k2) at the legs produce nonempty Hurwitz spaces
1143
# this amounts to a congruence relation mod n1, n2, which can be explicitly solved
1144
A1 = -QQ(sum([QQ(n1) / e[alpha] * nu[alpha] for alpha in I1])).floor()
1145
e_new = ZZ(n1) // n1.gcd(A1)
1146
nu1 = (QQ(A1 % n1) / (QQ(n1) / e_new)).floor()
1147
nu2 = e_new - nu1
1148
k1 = nu1.inverse_mod(e_new)
1149
k2 = nu2.inverse_mod(e_new)
1150
1151
# now we list all ways to distribute the markings, avoiding duplicates by symmetries
1152
I1_temp = I1[:]
1153
I2_temp = I2[:]
1154
# dictionary associating to a leg l in I the possible positions of l (from 0 to n/n1-1 if l in I1, ...)
1155
possi = {}
1156
1157
if I1:
1158
possi[I1[0]] = [0] # use G-action to move marking I1[0] to zeroth vertex on the left
1159
I1_temp.pop(0)
1160
if I2: # if there is one more leg on the other vertex, use stabilizer of zeroth left vertex to move
1161
possi[I2[0]] = list(range(gcd(n1, n // n2)))
1162
I2_temp.pop(0)
1163
else:
1164
if I2:
1165
possi[I2[0]] = [0]
1166
I2_temp.pop(0)
1167
for i in I1_temp:
1168
possi[i] = list(range(n // n1))
1169
for i in I2_temp:
1170
possi[i] = list(range(n // n2))
1171
1172
# mark_dist is a list of the form [(v1,v2,...), ...] where leg I[0] goes to vertex number v1 in its orbit, ....
1173
mark_dist = itertools.product(*[possi[j] for j in I])
1174
1175
# now all choices are made, and we construct the degenerated graph, to add it to degen
1176
# first: use Riemann-Hurwitz to compute the genera of the vertices above 0,1
1177
g1 = ((n1 * (2 * dege.genera(0) - 2) + n1 *
1178
(sum([1 - QQ(1) / e[alpha] for alpha in I1]) + 1 - QQ(1) / e_new)) / QQ(2) + 1).floor()
1179
g2 = ((n2 * (2 * dege.genera(1) - 2) + n2 *
1180
(sum([1 - QQ(1) / e[alpha] for alpha in I2]) + 1 - QQ(1) / e_new)) / QQ(2) + 1).floor()
1181
1182
# nonemptyness condition #TODO: is this the only thing you need to require?
1183
if g1 < 0 or g2 < 0:
1184
continue
1185
1186
new_genera = [g1 for j in range(n // n1)] + [g2 for j in range(QQ(n) / n2)]
1187
new_legs = [[] for j in range(n // n1 + n // n2)]
1188
new_legact = self.legact.copy()
1189
new_edges = self.gamma.edges()
1190
new_character = self.character.copy()
1191
1192
# now go through the orbit of the edge and adjust all necessary data
1193
# TODO: to be changed (access to private maxleg attribute)
1194
m0 = self.gamma._maxleg + 1
1195
m = self.gamma._maxleg + 1
1196
1197
for j in range(ZZ(n) // e_new):
1198
new_legs[j % (n // n1)] += [m]
1199
new_legs[(j % (n // n2)) + (n // n1)] += [m + 1]
1200
new_edges += [(m, m + 1)]
1201
new_legact[(sigma, m)] = m0 + ((m + 2 - m0) % (2 * n // e_new))
1202
new_legact[(sigma, m + 1)] = new_legact[(sigma, m)] + 1
1203
new_character[m] = (NumtoG[(n // e_new) % n], e_new, k1)
1204
new_character[m + 1] = (NumtoG[(n // e_new) % n], e_new, k2)
1205
1206
m += 2
1207
1208
# take care of remaining actions of g in G-{sigma} on legs belonging to orbit of new edge
1209
for jplus in range(2, n + 1):
1210
for m in range(m0, m0 + 2 * n // e_new):
1211
new_legact[(NumtoG[jplus % n], m)] = new_legact[(
1212
sigma, new_legact[(NumtoG[(jplus - 1) % n], m)])]
1213
1214
for md in mark_dist:
1215
new_legs_plus = [l[:] for l in new_legs]
1216
for alpha in range(len(I1)):
1217
for j in range(n // e[I[alpha]]):
1218
new_legs_plus[(md[alpha] + j) % (n // n1)
1219
] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]
1220
for alpha in range(len(I1), len(I)):
1221
for j in range(n // e[I[alpha]]):
1222
new_legs_plus[(md[alpha] + j) % (n // n2) + (n // n1)
1223
] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]
1224
1225
# all the data is now ready to generate the new graph
1226
new_Ggraph = Gstgraph(G, stgraph(new_genera, new_legs_plus, new_edges), {
1227
}, new_legact, new_character, hdata=self.hdata)
1228
new_Ggraph.vertact_reconstruct()
1229
if not any(equiGraphIsom(G, new_Ggraph)
1230
for G in degen):
1231
degen += [new_Ggraph]
1232
if dege.num_verts() == 1: # case of self-loop
1233
I = dege.legs(0, copy=True)
1234
I.remove(contracted_edge[0]) # remove the legs corresponding to the degenerated edge
1235
I.remove(contracted_edge[1])
1236
1237
n0_min = lcm([e[alpha] for alpha in I])
1238
1239
# TODO: following for-loop has great optimization potential, but current form should be sufficient for now
1240
for n0_add in range(1, ZZ(n) // n0_min + 1):
1241
n0 = n0_add * n0_min # n0 is the order of the stabilizer of any of the vertices
1242
if n0.divides(n):
1243
for e_new in n0.divisors():
1244
for g0 in [x for x in range(ZZ(n) // (2 * n0) + 1) if gcd(x, n // n0) == 1]:
1245
for nu1 in [j for j in range(e_new) if gcd(j, e_new) == 1]:
1246
# g0==0 means only one vertex and then there is a symmetry inverting edges
1247
if g0 == 0 and nu1 > QQ(e_new) / 2:
1248
continue
1249
nu2 = e_new - nu1
1250
k1 = Integer(nu1).inverse_mod(e_new)
1251
k2 = Integer(nu2).inverse_mod(e_new)
1252
1253
# now we list all ways to distribute the markings, avoiding duplicates by symmetries
1254
I_temp = I[:]
1255
1256
# dictionary associating to a leg l in I the possible positions of l (from 0 to n/n1-1 if l in I1, ...)
1257
possi = {}
1258
1259
if I:
1260
possi[I[0]] = [0] # use G-action to move marking I1[0] to zeroth vertex
1261
I_temp.pop(0)
1262
for i in I_temp:
1263
possi[i] = list(range(n // n0))
1264
1265
# mark_dist is a list of the form [(v1,v2,...), ...] where leg I[0] goes to vertex number v1 in its orbit, ....
1266
mark_dist = itertools.product(*[possi[j] for j in I])
1267
1268
# now all choices are made, and we construct the degenerated graph, to add it to degen
1269
# first: use Riemann-Hurwitz to compute the genera of the vertices above 0,1
1270
gen0 = (
1271
(n0 * (2 * dege.genera(0) - 2) + n0 * (sum([1 - QQ(1) / e[alpha] for alpha in I]) + 2 - QQ(2) / e_new)) / QQ(2) + 1).floor()
1272
1273
# nonemptyness condition #TODO: is this the only thing you need to require?
1274
if gen0 < 0:
1275
continue
1276
1277
new_genera = [gen0 for j in range(n // n0)]
1278
new_legs = [[] for j in range(n // n0)]
1279
new_legact = self.legact.copy()
1280
new_edges = self.gamma.edges()
1281
new_character = self.character.copy()
1282
1283
# now go through the orbit of the edge and adjust all necessary data
1284
# TODO: to be changed (access to private _maxleg attribute)
1285
m0 = self.gamma._maxleg + 1
1286
m = self.gamma._maxleg + 1
1287
1288
for j in range(ZZ(n) // e_new):
1289
new_legs[j % (n // n0)] += [m]
1290
new_legs[(j + g0) % (n // n0)] += [m + 1]
1291
new_edges += [(m, m + 1)]
1292
new_legact[(sigma, m)] = m0 + ((m + 2 - m0) % (2 * n // e_new))
1293
new_legact[(sigma, m + 1)] = new_legact[(sigma, m)] + 1
1294
new_character[m] = (NumtoG[(n // e_new) % n], e_new, k1)
1295
new_character[m + 1] = (NumtoG[(n // e_new) % n], e_new, k2)
1296
1297
m += 2
1298
1299
# take care of remaining actions of g in G-{sigma} on legs belonging to orbit of new edge
1300
for jplus in range(2, n + 1):
1301
for m in range(m0, m0 + 2 * n // e_new):
1302
new_legact[(NumtoG[jplus % n], m)] = new_legact[(
1303
sigma, new_legact[(NumtoG[(jplus - 1) % n], m)])]
1304
1305
for md in mark_dist:
1306
new_legs_plus = [l[:] for l in new_legs]
1307
for alpha in range(len(I)):
1308
for j in range(n // e[I[alpha]]):
1309
new_legs_plus[(md[alpha] + j) % (n // n0)
1310
] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]
1311
1312
# all the data is now ready to generate the new graph
1313
new_Ggraph = Gstgraph(G, stgraph(new_genera, new_legs_plus, new_edges), {
1314
}, new_legact, new_character, hdata=self.hdata)
1315
new_Ggraph.vertact_reconstruct()
1316
if not any(equiGraphIsom(G, new_Ggraph)
1317
for G in degen):
1318
degen += [new_Ggraph]
1319
return degen
1320
else:
1321
print('Degenerations for non-cyclic groups not implemented!')
1322
return []
1323
1324
# returns the list of all G-isomorphisms of the Gstgraphs Gr1 and Gr2
1325
# isomorphisms must respect markings (=legs that don't appear in edges)
1326
# they are given as [dictionary of images of vertices, dictionary of images of legs]
1327
# TODO:IDEA: first take quotients, compute isomorphisms between them, then lift??
1328
1329
1330
def equiGraphIsom(Gr1, Gr2):
1331
if Gr1.G != Gr2.G:
1332
return []
1333
Iso = GraphIsom(Gr1.gamma, Gr2.gamma) # list of all isomorphisms of underlying dual graphs
1334
GIso = []
1335
1336
# TODO: once Gequiv=False, can abort other loops
1337
# TODO: check character data!!
1338
for I in Iso:
1339
Gequiv = True
1340
for g in Gr1.G.gens():
1341
# check if isomorphism I is equivariant with respect to g: I o g = g o I
1342
# action on vertices:
1343
for v in I[0]:
1344
if I[0][Gr1.vertact[(g, v)]] != Gr2.vertact[(g, I[0][v])]:
1345
Gequiv = False
1346
break
1347
# action on legs:
1348
for l in I[1]:
1349
if I[1][Gr1.legact[(g, l)]] != Gr2.legact[(g, I[1][l])]:
1350
Gequiv = False
1351
break
1352
# character:
1353
for l in I[1]:
1354
if not Gequiv:
1355
break
1356
for j in [t for t in range(Gr1.character[l][1]) if gcd(t, Gr1.character[l][1]) == 1]:
1357
if Gr1.character[l][0]**j == Gr2.character[I[1][l]][0] and not ((j * Gr1.character[l][2] - Gr2.character[I[1][l]][2]) % Gr1.character[l][1]) == 0:
1358
Gequiv = False
1359
break
1360
if Gequiv:
1361
GIso += [I]
1362
1363
return GIso
1364
1365
# HurwitzData models the ramification data D of a Galois cover of curves in the following way:
1366
#
1367
# D.G = finite group
1368
# D.l = list of elements of the form (h,e,k) corresponding to orbits of markings with
1369
# * a generator h in G of the stabilizer of the element p of an orbit
1370
# * the order e of the stabilizer
1371
# * an integer 0<=k<e such that h acts on the tangent space of the curve at p by exp(2 pi i/e * k)
1372
1373
1374
class HurwitzData:
1375
def __init__(self, G, l):
1376
self.G = G
1377
self.l = l
1378
1379
def __eq__(self, other):
1380
if not isinstance(other, HurwitzData):
1381
return False
1382
return (self.G == other.G) and (self.l == other.l)
1383
1384
def __hash__(self):
1385
return hash((self.G, tuple(self.l)))
1386
1387
def __repr__(self):
1388
return repr((self.G, self.l))
1389
1390
def __deepcopy__(self, memo):
1391
return HurwitzData(self.G, self.l.copy())
1392
1393
def nummarks(self):
1394
g = self.G.order()
1395
N = ZZ.sum(g // r[1] for r in self.l)
1396
return N
1397
1398
# HurData takes a group G and a list l of group elements (giving monodromy around the corresponding markings) and gives back the corresponding Hurwitz data
1399
# This is just a more convenient way to enter HurwitzData
1400
1401
1402
def HurData(G, l):
1403
return HurwitzData(G, [(h, h.order(), min(h.order(), 1)) for h in l])
1404
1405
# returns the trivial stable graph of genus gen with group action corresponding to the HurwitzData D
1406
1407
1408
def trivGgraph(gen, D):
1409
G = D.G
1410
1411
# graph has only one vertex with trivial G-action
1412
vertact = {}
1413
for g in D.G:
1414
vertact[(g, 0)] = 0
1415
1416
n = 0 # counts number of legs as we go through Hurwitz data
1417
legact = {}
1418
character = {}
1419
1420
for e in D.l:
1421
# h=e[0] generates the stabilizer of (one element of) the orbit corresponding to e
1422
H = G.subgroup([e[0]])
1423
# markings corresponding to entry e are labelled by cosets gH and G-action is given by left-action of G
1424
(L, dic) = leftcosetaction(G, H)
1425
for g in G:
1426
for j in range(len(L)):
1427
# shift comes from fact that the n legs 1,2,...,n have already been assigned
1428
legact[(g, j + 1 + n)] = dic[(g, j)] + 1 + n
1429
1430
for j in range(len(L)):
1431
# Stabilizer at point gH generated by ghg**{-1}, of same order e, acts still by exp(2 pi i/e * k)
1432
character[j + 1 + n] = (L[j] * e[0] * L[j].inverse(), e[1], e[2])
1433
n += len(L)
1434
1435
gamma = stgraph([gen], [list(range(1, n + 1))], [])
1436
1437
return Gstgraph(G, gamma, vertact, legact, character, hdata=D)
1438
1439
1440
@cached_function
1441
def Hcovers_degeneration_graph(g, H, rmax=None):
1442
r"""
1443
Returns a nested list L where L[r][i] is a (duplicate-free) list of all Gstgraphs G for which
1444
G.quotient_graph(relabel=True) is isomorphic to the graph given by degeneration_graph(...)[r][i].
1445
1446
EXAMPLES::
1447
1448
sage: from admcycles.admcycles import degeneration_graph, Hcovers_degeneration_graph, HurData
1449
sage: G = CyclicPermutationGroup(2)
1450
sage: g = G.gen()
1451
sage: H = HurData(G, [g, g, g, g])
1452
sage: degGr = degeneration_graph(0, 4)[0]
1453
sage: Hcovs = Hcovers_degeneration_graph(1, H)
1454
sage: for r in range(2):
1455
....: for l_Ggr, (stGr, _, _, _) in zip(Hcovs[r], degGr[r]):
1456
....: assert all(Ggr.quotient_graph(relabel=True).is_isomorphic(stGr) for Ggr in l_Ggr)
1457
"""
1458
1459
g_base = trivGgraph(g, H).quotient_graph().g()
1460
n_base = len(H.l)
1461
1462
# initialize covermap
1463
if rmax is None or rmax > 3 * g_base - 3 + n_base:
1464
rmax = 3 * g_base - 3 + n_base
1465
1466
r0 = -1
1467
for r0 in range(3 * g_base - 3 + n, -2, -1):
1468
try:
1469
covermap = Hcovers_degeneration_graph.cached(g, H, r0)
1470
break
1471
except KeyError:
1472
pass
1473
1474
if r0 == -1: # function has not been called before
1475
covermap = [[[trivGgraph(g, H)]]]
1476
1477
# compute the possible covers for each degeneration
1478
degGr = degeneration_graph(g_base, n_base, rmax)[0]
1479
1480
for r in range(max(r0 + 1, 1), rmax + 1):
1481
covermap.append([]) # add empty list L(r)
1482
1483
for (Gr, halfedges, edge_contractions, _) in degGr[r]:
1484
templist = []
1485
(gr_index, halfedgeimages) = next(iter(edge_contractions)) # get an arbitrary edge contraction
1486
# determine which edge of Gr needs to be contracted to obtain
1487
# the graph degGr[r-1][gr_index].
1488
halfedges_in_image = [halfedges[h] for h in halfedgeimages]
1489
1490
halfedges = set(Gr.halfedges())
1491
halfedges.difference_update(halfedges_in_image)
1492
assert len(halfedges) == 2
1493
contracted_edge = tuple(sorted(halfedges))
1494
for cover_contracted in covermap[r - 1][gr_index]:
1495
for G in cover_contracted.degenerations(Gr, contracted_edge):
1496
if not any(equiGraphIsom(G, H) for H in templist):
1497
templist.append(G)
1498
covermap[-1].append(templist)
1499
1500
# remove old entry from cached_function dict if more has been computed now
1501
if rmax > r0 and r0 != -1:
1502
Hcovers_degeneration_graph.cache.pop(((g, H, r0), ()))
1503
1504
return covermap
1505
1506
1507
def list_Hstrata(g, H, r):
1508
r"""
1509
Gives a (duplicate-free) list of Gstgraphs for the Hurwitz space of genus g with HurwitzData H in codimension r.
1510
"""
1511
1512
if r == 0:
1513
return [trivGgraph(g, H)]
1514
1515
g_base = trivGgraph(g, H).quotient_graph().g()
1516
n_base = len(H.l)
1517
if r > 3 * g_base - 3 + n_base:
1518
return []
1519
1520
covermap = Hcovers_degeneration_graph(g, H, r)
1521
return flatten(covermap[r])
1522
1523
1524
def list_Hcovers(Gr, H, g=None):
1525
r"""
1526
For a given stabel graph list all possible H-covers.
1527
The markings of Gr must be [1, ..., len(H.l)].
1528
If g is not given, it is computed from Gr and H.
1529
1530
EXAMPLES::
1531
1532
sage: from admcycles.admcycles import list_Hcovers, HurData, stgraph, trivGgraph, equiGraphIsom
1533
sage: G = CyclicPermutationGroup(2)
1534
sage: g = G.gen()
1535
sage: H = HurData(G, [g, g, g, g])
1536
1537
sage: Gr = stgraph([0], [[1,2,3,4]], [])
1538
sage: list_Hcovers(Gr, H)
1539
[[1] [[1, 2, 3, 4]] []]
1540
1541
sage: Gr = stgraph([0, 0], [[1,2,5], [3,4,6]], [(5,6)])
1542
sage: list_Hcovers(Gr, H)
1543
[[0, 0] [[5, 7, 1, 2], [6, 8, 3, 4]] [(5, 6), (7, 8)]]
1544
1545
# Test that a previous bug in Gstgraph.degenerations is fixed
1546
sage: H = HurData(G, [g*g, g, g])
1547
sage: gr = stgraph([1, 0], [[4], [2, 3, 1, 5]], [(4, 5)])
1548
sage: list_Hcovers(gr, H)
1549
[[1, 1, 0] [[5], [7], [6, 8, 3, 4, 1, 2]] [(5, 6), (7, 8)],
1550
[1, 0] [[5, 7], [6, 8, 3, 4, 1, 2]] [(5, 6), (7, 8)]]
1551
"""
1552
1553
if g is None:
1554
d = H.G.order()
1555
g = (d * (2 * Gr.g() - 2) + sum(d / l[1] * (l[1] - 1) for l in H.l) + 2) / 2
1556
assert g.is_integer()
1557
g = ZZ(g)
1558
1559
if Gr.num_edges() == 0:
1560
return [trivGgraph(g, H)]
1561
1562
# We contract an arbitrary edge, list all covers for the contracted
1563
# graph and degenerate those.
1564
contracted_edge = Gr.edges(copy=False)[0]
1565
Gr_contr = Gr.copy(mutable=True)
1566
Gr_contr.contract_edge(contracted_edge)
1567
return flatten([G.degenerations(Gr, contracted_edge)
1568
for G in list_Hcovers(Gr_contr, H, g)])
1569
1570
1571
# gives a list of the quotgraphs of the entries of list_Hstrata(g,H,r) and the corresponding dictionaries
1572
# list elements are of the form [quotient graph,deggrfind(quotient graph),dicv,dicvinv,dicl]
1573
# if localize=True, finds the quotient graphs in the corresponding degeneration_graph and records their index as well as an isomorphism to the standard-graph
1574
# then the result has the form: [quotient graph,index in degeneration_graph,dicv,dicvinv,dicl,diclinv], where
1575
# * dicv is the quotient map sending vertices of Gr in list_Hstrata(g,H,r) to vertices IN THE STANDARD GRAPH inside degeneration_graph
1576
# * dicvinv gives a section of dicv
1577
# * dicl sends leg names in Gr to leg names IN THE STANDARD GRAPH
1578
# * diclinv gives a section of dicl
1579
1580
1581
@cached_function
1582
def list_quotgraphs(g, H, r, localize=True):
1583
Hgr = list_Hstrata(g, H, r)
1584
result = []
1585
for gr in Hgr:
1586
dicv = {}
1587
dicvinv = {}
1588
dicl = {}
1589
qgr = gr.quotient_graph(dicv, dicvinv, dicl)
1590
if localize:
1591
dgfind = deggrfind(qgr)
1592
defaultdicv = dgfind[2]
1593
defaultdicl = dgfind[3]
1594
defaultdicvinv = {defaultdicv[v]: v for v in defaultdicv}
1595
defaultdiclinv = {defaultdicl[l]: l for l in defaultdicl}
1596
1597
result.append([qgr, dgfind[1], {v: defaultdicv[dicv[v]] for v in dicv}, {
1598
w: dicvinv[defaultdicvinv[w]] for w in defaultdicvinv}, {l: defaultdicl[dicl[l]] for l in dicl}, defaultdiclinv])
1599
else:
1600
result.append([qgr, dicv, dicvinv, dicl])
1601
return result
1602
1603
# cyclicGstgraph is a convenient method to create a Gstgraph for a cyclic group action. It takes as input
1604
# * a stgraph Gr
1605
# * a number n giving the order of the cyclic group G
1606
# * a tuple perm of the form ((1,2),(3,),(4,5,6)) describing the action of a generator sigma of G on the legs; all legs need to appear to fix their order for interpreting cha
1607
# * a tuple cha=(2,3,1) giving for each of the cycles of legs l in perm the number k such that sigma**{n/|G_l|} acts on the tangent space at l by exp(2 pi i/|G_l| *k)
1608
# and returns the corresponding Gstgraph
1609
# optionally, the generator sigma can be given as input directly and G is computed as its parent
1610
# Here we use that the G-action on a connected graph Gr is uniquely determined by the G-action on the legs
1611
# TODO: user-friendliness: allow (3) instead of (3,)
1612
1613
1614
def cyclicGstgraph(Gr, n, perm, cha, sigma=None):
1615
if sigma is None:
1616
G = PermutationGroup(tuple(range(1, n + 1)))
1617
sigma = G.gens()[0]
1618
else:
1619
G = sigma.parent()
1620
1621
# first legaction and character
1622
perm_dic = {} # converts action of sigma into dictionary
1623
character = {}
1624
1625
for cycleno in range(len(perm)):
1626
e = ZZ(n) // len(perm[cycleno]) # order of stabilizer
1627
for j in range(len(perm[cycleno])):
1628
perm_dic[perm[cycleno][j]] = perm[cycleno][(j + 1) % len(perm[cycleno])]
1629
character[perm[cycleno][j]] = (sigma**(len(perm[cycleno])), e, cha[cycleno])
1630
1631
legact = {(sigma, k): perm_dic[k] for k in perm_dic}
1632
1633
mu = sigma
1634
for j in range(2, n + 1):
1635
mu2 = mu * sigma
1636
legact.update({(mu2, k): perm_dic[legact[(mu, k)]] for k in perm_dic})
1637
mu = mu2
1638
1639
# TODO: insert appropriate hdata
1640
# we don't bother to fill in vertact, since it is reconstructable anyway
1641
Gr_new = Gstgraph(G, Gr, {}, legact, character)
1642
Gr_new.vertact_reconstruct()
1643
1644
return Gr_new
1645
1646
# returns a tuple (L,d) of
1647
# * a list L =[g_0, g_1, ...] of representatives g_i of the cosets g_i H of H in G
1648
# * a dictionary d associating to tuples (g,n) of g in G and integers n the integer m, such that g*(g_n H) = g_m H
1649
# TODO: assumes first element of L is id_G
1650
1651
1652
def leftcosetaction(G, H):
1653
C = G.cosets(H, side='left')
1654
L = [C[i][0] for i in range(len(C))]
1655
d = {}
1656
1657
for g in G:
1658
for i in range(len(C)):
1659
gtild = g * L[i]
1660
for j in range(len(C)):
1661
if gtild in C[j]:
1662
d[(g, i)] = j
1663
return (L, d)
1664
1665
1666
def tautclass(arg, g=None, n=None):
1667
r"""
1668
Construct a tautological class.
1669
1670
The return object has type :class:`admcycles.tautological_ring.TautlogicalClass`.
1671
1672
INPUT:
1673
1674
arg : a tuple or list of :class:`decstratum`
1675
1676
g : integer
1677
the genus
1678
1679
n : integer
1680
the number of marked points
1681
1682
EXAMPLES::
1683
1684
sage: from admcycles.admcycles import *
1685
1686
sage: gamma = StableGraph([1,2],[[1,2],[3]],[(2,3)])
1687
sage: ds1 = decstratum(gamma, kappa=[[1],[]]); ds1
1688
Graph : [1, 2] [[1, 2], [3]] [(2, 3)]
1689
Polynomial : (kappa_1)_0
1690
sage: ds2 = decstratum(gamma, kappa=[[],[1]]); ds2
1691
Graph : [1, 2] [[1, 2], [3]] [(2, 3)]
1692
Polynomial : (kappa_1)_1
1693
sage: t = tautclass([ds1, ds2])
1694
sage: (t - gamma.to_tautological_class() * kappaclass(1,3,1)).is_zero()
1695
True
1696
1697
sage: t = 7*fundclass(0,4) + psiclass(1,0,4) + 3 * psiclass(2,0,4) - psiclass(3,0,4)
1698
sage: s = t.FZsimplify(); s
1699
Graph : [0] [[1, 2, 3, 4]] []
1700
Polynomial : 7 + 3*(kappa_1)_0
1701
sage: u = t.FZsimplify(r=0); u
1702
Graph : [0] [[1, 2, 3, 4]] []
1703
Polynomial : 7
1704
1705
sage: t = kappaclass(1,1,1)
1706
sage: t.evaluate()
1707
1/24
1708
1709
sage: t = psiclass(1,2,1)
1710
sage: s = t.forgetful_pushforward([1]); s
1711
Graph : [2] [[]] []
1712
Polynomial : 2
1713
sage: s.fund_evaluate()
1714
2
1715
1716
sage: a = psiclass(1,2,3)
1717
sage: t = a + a*a
1718
sage: t.degree_list()
1719
[1, 2]
1720
1721
sage: diff = kappaclass(1,3,0) - 12*lambdaclass(1,3,0)
1722
sage: diff.is_zero()
1723
False
1724
sage: diff.is_zero(moduli='sm')
1725
True
1726
1727
Non-standard markings::
1728
1729
sage: gamma = StableGraph([1],[[3,7]],[])
1730
sage: ds = decstratum(gamma, kappa=[[1]])
1731
sage: tautclass([ds])
1732
Graph : [1] [[3, 7]] []
1733
Polynomial : (kappa_1)_0
1734
sage: tautclass([ds]).parent()
1735
TautologicalRing(g=1, n=(3, 7), moduli='st') over Rational Field
1736
1737
Test the ``g`` and ``n`` parameters::
1738
1739
sage: tautclass([], g=2, n=2)
1740
0
1741
sage: tautclass([], g=2, n=2).parent()
1742
TautologicalRing(g=2, n=2, moduli='st') over Rational Field
1743
"""
1744
from .tautological_ring import TautologicalRing, TautologicalClass
1745
1746
if not arg:
1747
if g is None or n is None:
1748
# NOTE: in this situation we do not return a TautologicalClass since we do not know
1749
# a priori the parameters g and n. Since we have proper coercions it should not be
1750
# a problem in most situations.
1751
from .superseded import deprecation
1752
deprecation(109, 'can not guess g, n from the input, return an integer. Please provde g and n to tautclass')
1753
return ZZ(0)
1754
return TautologicalRing(g, n).zero()
1755
1756
if isinstance(arg, TautologicalClass):
1757
return arg
1758
elif isinstance(arg, (tuple, list)):
1759
if g is None:
1760
g = arg[0].gamma.g()
1761
if n is None:
1762
n = tuple(sorted(arg[0].gamma.list_markings()))
1763
return TautologicalRing(g, n)(arg)
1764
else:
1765
raise TypeError('invalid input for tautclass')
1766
1767
1768
# TODO: storing the data as (unordered) lists is very inefficient!
1769
# Simpler if we had a dictionary: (monomial) -> coefficient
1770
# that would require a hashable version of monomial
1771
class KappaPsiPolynomial(SageObject):
1772
r"""
1773
Polynomial in kappa and psi-classes on a common stable graph.
1774
1775
The data is stored as a list monomials of entries (kappa,psi) and a list
1776
coeff of their coefficients. Here (kappa,psi) is a monomial in kappa, psi
1777
classes, represented as
1778
1779
- ``kappa``: list of length self.gamma.num_verts() of lists of the form [3,0,2]
1780
meaning that this vertex carries kappa_1**3*kappa_3**2
1781
1782
- ``psi``: dictionary, associating nonnegative integers to some legs, where
1783
psi[l]=3 means that there is a psi**3 at this leg for a kppoly p. The values
1784
can not be zero.
1785
1786
If ``p`` is such a polynomial, ``p[i]`` is of the form ``(kappa,psi,coeff)``.
1787
1788
EXAMPLES::
1789
1790
sage: from admcycles.admcycles import kppoly
1791
sage: p1 = kppoly([([[0, 1], [0, 0, 2]], {1:2, 2:3}), ([[], [0, 1]], {})], [-3, 5])
1792
sage: p1
1793
-3*(kappa_2)_0*(kappa_3^2)_1*psi_1^2*psi_2^3 + 5*(kappa_2)_1
1794
"""
1795
1796
def __init__(self, monom, coeff):
1797
self.monom = monom
1798
self.coeff = coeff
1799
assert len(self.monom) == len(self.coeff)
1800
1801
def __bool__(self):
1802
return bool(self.monom)
1803
1804
def copy(self):
1805
r"""
1806
TESTS::
1807
1808
sage: from admcycles.admcycles import kppoly
1809
sage: p = kppoly([([[], [0, 1]], {0:3, 1:2})], [5])
1810
sage: p.copy()
1811
5*(kappa_2)_1*psi_0^3*psi_1^2
1812
"""
1813
res = KappaPsiPolynomial.__new__(KappaPsiPolynomial)
1814
res.monom = [([x[:] for x in k], p.copy()) for k, p in self.monom]
1815
res.coeff = self.coeff[:]
1816
return res
1817
1818
def __iter__(self): # TODO fix
1819
return iter([self[i] for i in range(len(self))])
1820
1821
def __getitem__(self, i):
1822
return self.monom[i] + (self.coeff[i],)
1823
1824
def __len__(self):
1825
return len(self.monom)
1826
1827
def __neg__(self):
1828
return (-1) * self
1829
1830
def __add__(self, other): # TODO: Add __radd__
1831
r"""
1832
TESTS:
1833
1834
Check that mutable data are not shared between the result and the terms::
1835
1836
sage: from admcycles.admcycles import kappacl, psicl
1837
sage: A = kappacl(0,2,2)
1838
sage: B = psicl(3,2)
1839
sage: C = A + B
1840
sage: C.monom[1][1].update({2:3})
1841
sage: A
1842
(kappa_2)_0
1843
sage: B
1844
psi_3
1845
"""
1846
if other == 0:
1847
return self.copy()
1848
new = self.copy()
1849
for (k, p, c) in other:
1850
try:
1851
ind = new.monom.index((k, p))
1852
new.coeff[ind] += c
1853
if new.coeff[ind] == 0:
1854
new.monom.pop(ind)
1855
new.coeff.pop(ind)
1856
except ValueError:
1857
if c != 0:
1858
new.monom.append((k[:], p.copy()))
1859
new.coeff.append(c)
1860
return new
1861
1862
# returns the degree of the ith term of self (default: i=0). If self is empty, give back None
1863
def deg(self, i=0):
1864
if not self.monom:
1865
return None
1866
else:
1867
(kappa, psi) = self.monom[i]
1868
return sum([sum((j + 1) * kvec[j] for j in range(len(kvec))) for kvec in kappa]) + sum(psi.values())
1869
1870
# computes the pullback of self under a graph morphism described by dicv, dicl
1871
# returns a kppoly on this graph
1872
def graphpullback(self, dicv, dicl):
1873
if len(self.monom) == 0:
1874
return kppoly([], [])
1875
1876
numvert_self = len(self.monom[0][0])
1877
numvert = len(dicv)
1878
1879
preim = [[] for i in range(numvert_self)]
1880
for v in dicv:
1881
preim[dicv[v]].append(v)
1882
1883
resultpoly = kppoly([], [])
1884
for (kappa, psi, coeff) in self:
1885
# pullback of psi-classes
1886
psipolydict = {dicl[l]: psi[l] for l in psi}
1887
psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])
1888
1889
# pullback of kappa-classes
1890
kappapoly = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim[v]])**kappa[v][k]
1891
for k in range(len(kappa[v]))]) for v in range(numvert_self)])
1892
1893
resultpoly += coeff * psipoly * kappapoly
1894
return resultpoly
1895
1896
def rename_legs(self, dic):
1897
r"""
1898
Rename the legs according to ``dic``.
1899
"""
1900
for count in range(len(self.monom)):
1901
self.monom[count] = (self.monom[count][0], {dic.get(
1902
l, l): self.monom[count][1][l] for l in self.monom[count][1]})
1903
return self
1904
1905
# takes old kappa-data and embeds self in larger graph with numvert vertices on vertices start, start+1, ...
1906
def expand_vertices(self, start, numvert):
1907
for count in range(len(self.monom)):
1908
self.monom[count] = ([[] for i in range(start)] + self.monom[count][0] + [[]
1909
for i in range(numvert - start - len(self.monom[count][0]))], self.monom[count][1])
1910
return self
1911
1912
def __radd__(self, other):
1913
if other == 0:
1914
return self.copy()
1915
1916
def __mul__(self, other):
1917
if isinstance(other, kppoly):
1918
return kppoly([([kappaadd(kappa1[i], kappa2[i]) for i in range(len(kappa1))], {l: psi1.get(l, 0) + psi2.get(l, 0) for l in set(list(psi1) + list(psi2))}) for (kappa1, psi1) in self.monom for (kappa2, psi2) in other.monom], [a * b for a in self.coeff for b in other.coeff]).consolidate()
1919
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational):
1920
else:
1921
return self.__rmul__(other)
1922
1923
def __rmul__(self, other):
1924
if isinstance(other, kppoly):
1925
return self.__mul__(other)
1926
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
1927
else:
1928
new = self.copy()
1929
for i in range(len(self)):
1930
new.coeff[i] *= other
1931
return new
1932
1933
def __pow__(self, exponent):
1934
if isinstance(exponent, (Integer, Rational, int)):
1935
return prod(exponent * [self])
1936
1937
def _monomial_str(self, kappa, psi, unicode=False):
1938
r"""
1939
Return the string representation of the monomial ``kappa``, ``psi``.
1940
"""
1941
if unicode:
1942
kappa_name = "κ"
1943
psi_name = "ψ"
1944
index_prefix = ""
1945
exponent_prefix = ""
1946
indices = "₀₁₂₃₄₅₆₇₈₉"
1947
exponents = "⁰¹²³⁴⁵⁶⁷⁸⁹"
1948
else:
1949
kappa_name = "kappa"
1950
psi_name = "psi"
1951
index_prefix = "_"
1952
exponent_prefix = "^"
1953
indices = exponents = "0123456789"
1954
1955
def num_str(n, digits_str, minus='-'):
1956
if n < 0:
1957
raise ValueError
1958
elif n == 0:
1959
return digits_str[0]
1960
else:
1961
return ''.join(digits_str[d] for d in reversed(ZZ(n).digits(10)))
1962
1963
result = []
1964
1965
# kappa part
1966
for v, kappa_v in enumerate(kappa):
1967
kappa_str = []
1968
for deg, power in enumerate(kappa_v):
1969
if power:
1970
deg += 1
1971
kappa_str.append(kappa_name + index_prefix + num_str(deg, indices) + exponent_prefix + num_str(power, exponents) if power != 1 else kappa_name + index_prefix + num_str(deg, indices))
1972
if kappa_str:
1973
result.append(('({})' + index_prefix + '{}').format('*'.join(kappa_str), num_str(v, indices)))
1974
1975
# psi part
1976
for i, j in psi.items():
1977
result.append(psi_name + index_prefix + num_str(i, indices) + exponent_prefix + num_str(j, exponents) if j != 1 else psi_name + index_prefix + num_str(i, indices))
1978
1979
return '*'.join(result)
1980
1981
def str(self, unicode=False):
1982
result = ''
1983
for i, (c, m) in enumerate(zip(self.coeff, self.monom)):
1984
cs = repr(c)
1985
ms = self._monomial_str(kappa=m[0], psi=m[1], unicode=unicode)
1986
has_add_or_sub = '+' in cs[1:] or '-' in cs[1:]
1987
if i:
1988
if cs[0] == '-':
1989
if not has_add_or_sub:
1990
result += ' - '
1991
cs = repr(-c)
1992
elif not ms:
1993
result += ' - '
1994
cs = cs[1:]
1995
else:
1996
result += ' + '
1997
else:
1998
result += ' + '
1999
2000
if not ms:
2001
result += cs
2002
elif has_add_or_sub:
2003
result += '(' + cs + ')*' + ms
2004
elif cs == '1':
2005
result += ms
2006
elif cs == '-1':
2007
result += '-' + ms
2008
else:
2009
result += cs + '*' + ms
2010
2011
return result if result else '0'
2012
2013
def _repr_(self):
2014
r"""
2015
TESTS::
2016
2017
sage: from admcycles.admcycles import KappaPsiPolynomial
2018
sage: x = polygen(ZZ)
2019
sage: m1 = ([[0,1,1]], {1: 2})
2020
sage: m2 = ([[1,0,1]], {})
2021
sage: m3 =([[]], {1:1})
2022
sage: m4 = ([], {1:1,2:1})
2023
sage: m5 = ([[]], {})
2024
sage: KappaPsiPolynomial([m1, m2, m3, m4, m5], [x-1, -1, x+2, -3, - x + 3])
2025
(x - 1)*(kappa_2*kappa_3)_0*psi_1^2 - (kappa_1*kappa_3)_0 + (x + 2)*psi_1 - 3*psi_1*psi_2 - x + 3
2026
2027
sage: m1 = ([[]], {1:1})
2028
sage: m2 = ([[]], {})
2029
sage: KappaPsiPolynomial([m1, m2], [3, 4])
2030
3*psi_1 + 4
2031
"""
2032
return self.str(unicode=False)
2033
2034
def _unicode_art_(self):
2035
r"""
2036
TESTS::
2037
2038
sage: from admcycles.admcycles import KappaPsiPolynomial
2039
sage: x = polygen(ZZ)
2040
sage: m1 = ([[0,1,1]], {13: 25})
2041
sage: m2 = ([[1,0,1] + [0] * 10 + [1]], {})
2042
sage: m3 =([[]], {1:123})
2043
sage: m4 = ([], {1:1,2:1})
2044
sage: m5 = ([[]], {})
2045
sage: p = KappaPsiPolynomial([m1, m2, m3, m4, m5], [x-1, -1, x+2, -3, - x + 3])
2046
sage: unicode_art(p)
2047
(x - 1)*(κ₂*κ₃)₀*ψ₁₃²⁵ - (κ₁*κ₃*κ₁₄)₀ + (x + 2)*ψ₁¹²³ - 3*ψ₁*ψ₂ - x + 3
2048
sage: m1 = ([[]], {1:1})
2049
sage: m2 = ([[]], {})
2050
sage: p = KappaPsiPolynomial([m1, m2], [3, 4])
2051
sage: unicode_art(p)
2052
3*ψ₁ + 4
2053
"""
2054
from sage.typeset.unicode_art import UnicodeArt
2055
return UnicodeArt([self.str(unicode=True)])
2056
2057
# TODO: this should rather be called "normalize"
2058
def consolidate(self, force=True):
2059
r"""
2060
Remove trailing zeroes in kappa and l with psi[l]=0 and things with coeff=0
2061
and sum up again.
2062
2063
TESTS::
2064
2065
sage: from admcycles.admcycles import kppoly
2066
sage: kppoly([([[], [0, 1]], {})], [5]).consolidate()
2067
5*(kappa_2)_1
2068
sage: kppoly([([[0, 0], [0,1,0]], {})], [3]).consolidate()
2069
3*(kappa_2)_1
2070
sage: kppoly([([[], [0,1]], {})], [0]).consolidate() # known bug
2071
0
2072
"""
2073
modified = force
2074
for i in range(len(self.monom)):
2075
kappa, psi = self.monom[i]
2076
for kap in kappa:
2077
if kap and kap[-1] == 0:
2078
modified = True
2079
while kap and not kap[-1]:
2080
kap.pop()
2081
for l in list(psi):
2082
if not psi[l]:
2083
modified = True
2084
psi.pop(l)
2085
2086
# now we check for possibly newly identified duplicates
2087
if modified:
2088
i = 0
2089
while i < len(self.monom):
2090
if not self.coeff[i]:
2091
self.coeff.pop(i)
2092
self.monom.pop(i)
2093
continue
2094
m = self.monom[i]
2095
j = i + 1
2096
while True:
2097
try:
2098
j = self.monom.index(m, j)
2099
except ValueError:
2100
break
2101
else:
2102
self.coeff[i] += self.coeff[j]
2103
self.monom.pop(j)
2104
self.coeff.pop(j)
2105
if not self.coeff[i]:
2106
self.monom.pop(i)
2107
self.coeff.pop(i)
2108
else:
2109
i += 1
2110
2111
return self
2112
2113
2114
kppoly = KappaPsiPolynomial
2115
2116
# some functions for entering kappa and psi classes
2117
2118
2119
def kappacl(vertex, index, numvert, g=None, n=None):
2120
r"""
2121
Returns the polynomial (kappa_index)_vertex
2122
2123
INPUT:
2124
2125
vertex : integer
2126
the vertex on which the kappa class is supported
2127
index : integer
2128
the index of the kappa class
2129
numvert : integer
2130
the total number of vertices
2131
2132
EXAMPLES::
2133
2134
sage: from admcycles.admcycles import kappacl
2135
sage: kappacl(0, 2, 4)
2136
(kappa_2)_0
2137
sage: kappacl(2, 1, 3)
2138
(kappa_1)_2
2139
"""
2140
# if g is not None or n is not None:
2141
# raise ValueError('g or n should not be provided')
2142
if index == 0:
2143
return (2 * g - 2 + n) * onekppoly(numvert)
2144
if index < 0:
2145
return KappaPsiPolynomial([], [])
2146
li = [[] for i in range(numvert)]
2147
li[vertex] = (index - 1) * [0] + [1]
2148
return KappaPsiPolynomial([(li, {})], [1])
2149
2150
2151
def psicl(leg, numvert):
2152
r"""
2153
Return the polynomial (psi)_leg - must give total number of vertices
2154
2155
EXAMPLES::
2156
2157
sage: from admcycles.admcycles import psicl
2158
sage: psicl(1, 4)
2159
psi_1
2160
"""
2161
li = [[] for i in range(numvert)]
2162
return KappaPsiPolynomial([(li, {leg: 1})], [1])
2163
2164
2165
def onekppoly(numvert):
2166
r"""
2167
Return one as a kappa-psi polynomial
2168
2169
INPUT:
2170
2171
numvert : integer
2172
the number of vertices
2173
2174
EXAMPLES::
2175
2176
sage: from admcycles.admcycles import onekppoly
2177
sage: onekppoly(4)
2178
1
2179
"""
2180
return KappaPsiPolynomial([([[] for cnt in range(numvert)], {})], [1])
2181
2182
# def clean(self):
2183
# for k in self.coeff.keys():
2184
# if self.coeff[k]==0:
2185
# self.coeff.pop(k)
2186
# return self
2187
# TODO: Multiplication with other kppoly
2188
2189
2190
class decstratum(SageObject):
2191
r"""
2192
A tautological class given by a boundary stratum, decorated by a polynomial in
2193
kappa-classes on the vertices and psi-classes on the legs (i.e. half-edges
2194
and markings)
2195
2196
The internal structure is as follows
2197
2198
- ``gamma``: underlying stgraph for the boundary stratum
2199
- ``kappa``: list of length gamma.num_verts() of lists of the form [3,0,2]
2200
meaning that this vertex carries kappa_1^3*kappa_3^2
2201
- ``psi``: dictionary, associating nonnegative integers to some legs, where
2202
psi[l]=3 means that there is a psi^3 at this leg
2203
2204
We adopt the convention that: kappa_a = pi_*(psi_{n+1}^{a+1}), where
2205
pi is the universal curve over the moduli space, psi_{n+1} is the psi-class
2206
at the marking n+1 that is forgotten by pi.
2207
"""
2208
2209
def __init__(self, gamma, kappa=None, psi=None, poly=None):
2210
self.gamma = gamma.copy(mutable=False) # pick an immutable copy (possibly avoids copy)
2211
if kappa is None:
2212
kappa = [[] for i in range(gamma.num_verts())]
2213
if psi is None:
2214
psi = {}
2215
if poly is None:
2216
# NOTE: the following might be wrong as the Python integer 1 is unlikely to
2217
# be an element of the base ring
2218
# See https://gitlab.com/modulispaces/admcycles/-/issues/83
2219
self.poly = kppoly([(kappa, psi)], [1])
2220
else:
2221
if isinstance(poly, kppoly):
2222
self.poly = poly.copy()
2223
# if isinstance(poly,sage.rings.integer.Integer) or isinstance(poly,sage.rings.rational.Rational):
2224
else:
2225
self.poly = poly * onekppoly(gamma.num_verts())
2226
2227
# def deg(self):
2228
# return len(self.gamma.edges)+sum([sum([k[i]*(i+1) for i in range(len(k))]) for k in kappa])+sum(psi.values())
2229
2230
def __bool__(self):
2231
return bool(self.poly)
2232
2233
def copy(self, mutable=False):
2234
S = decstratum.__new__(decstratum)
2235
S.gamma = self.gamma.copy(mutable)
2236
S.poly = self.poly.copy()
2237
return S
2238
2239
def automorphism_number(self):
2240
r"""Returns number of automorphisms of underlying graph fixing decorations by kappa and psi-classes.
2241
Currently assumes that self has exaclty one nonzero term."""
2242
kappa, psi = self.poly.monom[0]
2243
g, n, r = self.gnr_list()[0]
2244
markings = range(1, n + 1)
2245
num = DR.num_of_stratum(Pixtongraph(self.gamma, kappa, psi), g, r, markings)
2246
return DR.autom_count(num, g, r, markings, MODULI_ST)
2247
2248
def rename_legs(self, dic, rename=None, inplace=True, return_dicts=False):
2249
r"""
2250
Rename the markings according to ``dic``.
2251
2252
EXAMPLES::
2253
2254
sage: from admcycles.stable_graph import StableGraph
2255
sage: from admcycles.admcycles import decstratum
2256
sage: g = StableGraph([0, 0], [[1, 3], [2, 4, 5, 6]], [(3, 4), (5, 6)])
2257
sage: D = decstratum(g, [[0,1], []], {1: 1})
2258
sage: D
2259
Graph : [0, 0] [[1, 3], [2, 4, 5, 6]] [(3, 4), (5, 6)]
2260
Polynomial : (kappa_2)_0*psi_1
2261
sage: D.rename_legs({1: 2, 2: 1})
2262
Graph : [0, 0] [[2, 3], [1, 4, 5, 6]] [(3, 4), (5, 6)]
2263
Polynomial : (kappa_2)_0*psi_2
2264
"""
2265
if rename is not None:
2266
from .superseded import deprecation
2267
deprecation(109, 'the rename argument in decstratum.rename_legs is deprecated (and actually ignored)')
2268
if inplace:
2269
result = self
2270
new_gamma = self.gamma.copy(mutable=True)
2271
else:
2272
result = self.copy(mutable=True)
2273
new_gamma = result.gamma
2274
_, markings_relabelling, legs_relabelling = new_gamma.rename_legs(dic, inplace=True, return_dicts=True)
2275
new_gamma.set_immutable()
2276
result.gamma = new_gamma
2277
result.gamma.set_immutable() # always better
2278
result.poly.rename_legs(legs_relabelling)
2279
return (result, markings_relabelling, legs_relabelling) if return_dicts else result
2280
2281
def __repr__(self):
2282
return 'Graph : ' + repr(self.gamma) + '\n' + 'Polynomial : ' + repr(self.poly)
2283
2284
def _unicode_art_(self):
2285
r"""
2286
TESTS::
2287
2288
sage: from admcycles.stable_graph import StableGraph
2289
sage: from admcycles.admcycles import decstratum
2290
sage: g = StableGraph([0, 0], [[1, 3], [2, 4, 5, 6]], [(3, 4), (5, 6)])
2291
sage: D = decstratum(g, [[0,1], []], {1: 1})
2292
sage: unicode_art(D)
2293
Graph :
2294
╭───╮
2295
│ │╭╮
2296
3 456
2297
╭┴╮ ╭┴┴┴╮
2298
│0│ │0 │
2299
╰┬╯ ╰┬──╯
2300
1 2
2301
<BLANKLINE>
2302
Polynomial : (κ₂)₀*ψ₁
2303
"""
2304
from sage.typeset.unicode_art import unicode_art
2305
return unicode_art('Graph :') * unicode_art(self.gamma) * (unicode_art('\nPolynomial : ') + unicode_art(self.poly))
2306
2307
# returns a list of lists of the form [kp1,kp2,...,kpr] where r is the number of vertices in self.gamma and kpj is a kppoly being the part of self.poly living on the jth vertex (we put coeff in the first vertex)
2308
def split(self):
2309
result = []
2310
numvert = self.gamma.num_verts()
2311
lvdict = {l: v for v in range(numvert) for l in self.gamma.legs(v, copy=False)}
2312
2313
for (kappa, psi, coeff) in self.poly:
2314
psidiclist = [{} for v in range(numvert)]
2315
for l in psi:
2316
psidiclist[lvdict[l]][l] = psi[l]
2317
newentry = [kppoly([([kappa[v]], psidiclist[v])], [1]) for v in range(numvert)]
2318
newentry[0] *= coeff
2319
result.append(newentry)
2320
return result
2321
2322
def __neg__(self):
2323
return (-1) * self
2324
2325
def __mul__(self, other):
2326
return self.__rmul__(other)
2327
2328
def __rmul__(self, other):
2329
r"""
2330
TESTS::
2331
sage: from admcycles import TautologicalRing
2332
sage: P = TautologicalRing(1,2).psi(1)
2333
sage: P * next(iter(P._terms.values()))
2334
Graph : [1] [[1, 2]] []
2335
Polynomial : psi_1^2
2336
sage: next(iter(P._terms.values())) * P
2337
Graph : [1] [[1, 2]] []
2338
Polynomial : psi_1^2
2339
"""
2340
from .tautological_ring import TautologicalClass
2341
if isinstance(other, Hdecstratum):
2342
return other.__mul__(self)
2343
2344
if isinstance(other, decstratum):
2345
# TODO: to be changed (direct access to _edges attribute)
2346
if other.gamma.num_edges() > self.gamma.num_edges():
2347
return other.__rmul__(self)
2348
if self.gamma.num_edges() == other.gamma.num_edges() == 0:
2349
# both are just vertices with some markings and some kappa-psi-classes
2350
# multiply those kappa-psi-classes and return the result
2351
return tautclass([decstratum(self.gamma, poly=self.poly * other.poly)])
2352
p1 = self.convert_to_prodtautclass()
2353
p2 = self.gamma.boundary_pullback(other)
2354
p = p1 * p2
2355
return p.pushforward()
2356
elif isinstance(other, TautologicalClass):
2357
return other.parent()([self]) * other
2358
else:
2359
# assume scalar multiplication
2360
new = self.copy(mutable=False)
2361
new.poly *= other
2362
return new
2363
2364
def multiply(self, other, R):
2365
r"""
2366
Return the result of the multiplication of the decstratum ``self``
2367
and the decstratum ``other`` in the tautological ring ``R``
2368
"""
2369
if type(self) is not type(other):
2370
raise TypeError
2371
2372
# TODO: be more clever in the multiplication. Many terms could be
2373
# zero because of the moduli encoded in R.
2374
if other.gamma.num_edges() > self.gamma.num_edges():
2375
return other.multiply(self, R)
2376
if self.gamma.num_edges() == other.gamma.num_edges() == 0:
2377
# both are just vertices with some markings and some kappa-psi-classes
2378
# multiply those kappa-psi-classes and return the result
2379
return R(self.gamma, poly=self.poly * other.poly)
2380
p1 = self.convert_to_prodtautclass()
2381
p2 = self.gamma.boundary_pullback(other)
2382
return (p1 * p2).pushforward(R)
2383
2384
def to_tautological_class(self):
2385
r"""
2386
Converts decstratum to TautologicalClass.
2387
2388
EXAMPLES::
2389
2390
sage: from admcycles.admcycles import decstratum, StableGraph
2391
sage: d = decstratum(StableGraph([1],[[1,2,3]],[(2,3)]),psi={1:1})
2392
sage: dt = d.to_tautological_class()
2393
sage: dt.parent()
2394
TautologicalRing(g=2, n=1, moduli='st') over Rational Field
2395
"""
2396
from .tautological_ring import TautologicalRing
2397
g = self.gamma.g()
2398
n = self.gamma.n() # TODO: adapt to nonstandard markings
2399
return TautologicalRing(g, n)([self])
2400
2401
def toTautvect(self, g=None, n=None, r=None):
2402
return converttoTautvect(self, g, n, r)
2403
2404
def toTautbasis(self, g=None, n=None, r=None):
2405
return Tautvecttobasis(converttoTautvect(self, g, n, r), g, n, r)
2406
2407
# remove all terms that must vanish for dimension reasons
2408
def dimension_filter(self, moduli='st'):
2409
for i in range(len(self.poly.monom) - 1, -1, -1):
2410
(kappa, psi) = self.poly.monom[i]
2411
for v in range(self.gamma.num_verts()):
2412
# local check: we are not exceeding the socle degree at any of the vertices
2413
kappa_deg = sum([(k + 1) * kappa[v][k] for k in range(len(kappa[v]))])
2414
psi_deg = sum([psi[l] for l in self.gamma.legs(v, copy=False) if l in psi])
2415
socle = socle_degree(self.gamma.genera(v), self.gamma.num_legs(v), get_moduli(moduli))
2416
if kappa_deg + psi_deg > socle:
2417
self.poly.monom.pop(i)
2418
self.poly.coeff.pop(i)
2419
break
2420
return self
2421
2422
# remove all terms of degree higher than dmax
2423
def degree_cap(self, dmax):
2424
for i in range(len(self.poly.monom) - 1, -1, -1):
2425
if self.poly.deg(i) + self.gamma.num_edges() > dmax:
2426
self.poly.monom.pop(i)
2427
self.poly.coeff.pop(i)
2428
return self
2429
2430
# returns degree d part of self
2431
def degree_part(self, d):
2432
result = self.copy(mutable=False)
2433
for i in range(len(result.poly.monom)):
2434
if result.poly.deg(i) + result.gamma.num_edges() != d:
2435
result.poly.coeff[i] = 0
2436
return result.consolidate()
2437
2438
def consolidate(self):
2439
self.poly.consolidate()
2440
return self
2441
2442
# computes integral against the fundamental class of the corresponding moduli space
2443
# will not complain if terms are mixed degree or if some of them do not have the right codimension
2444
def evaluate(self, moduli='st'):
2445
DRmoduli = get_moduli(moduli, DRpy=True)
2446
answer = 0
2447
for (kappa, psi, coeff) in self.poly:
2448
temp = 1
2449
for v in range(self.gamma.num_verts()):
2450
psilist = [psi.get(l, 0) for l in self.gamma.legs(v, copy=False)]
2451
kappalist = []
2452
for j in range(len(kappa[v])):
2453
kappalist += [j + 1 for k in range(kappa[v][j])]
2454
if sum(psilist + kappalist) != socle_degree(self.gamma.genera(v), len(psilist), DRmoduli):
2455
temp = 0
2456
break
2457
temp *= DR.socle_formula(self.gamma.genera(v), psilist, kappalist, moduli_type=DRmoduli)
2458
answer += coeff * temp
2459
return answer
2460
2461
def forgetful_pushforward(self, markings, dicv=False):
2462
r"""
2463
Return the decstratum corresponding to the pushforward under
2464
the map forgetting the ``markings``.
2465
2466
The obtained ``decstratum`` has underlying stable graph the one obtained by
2467
forgetting and stabilizing. The algorithm currently works by forgetting one
2468
marking at a time.
2469
"""
2470
result = self.copy(mutable=True)
2471
result.dimension_filter()
2472
vertim = list(range(self.gamma.num_verts()))
2473
for m in markings:
2474
# take current result and forget m
2475
v = result.gamma.vertex(m)
2476
if result.gamma.genera(v) == 0 and len(result.gamma.legs(v, copy=False)) == 3:
2477
# this vertex will become unstable, but the previous dimension filter took care that no term in result.poly has kappa or psi on vertex v
2478
# thus we can simply forget this vertex and go on, but we must adjust the kappa entries in result.poly
2479
2480
# If the vertex v carries another marking m' and a leg l, then while m' and l cannot have psi-data, the psi-data from the complementary leg of l must be transferred to m'
2481
# TODO: use more elegant (dicv,dicl)-return of .stabilize()
2482
vlegs = result.gamma.legs(v, copy=True)
2483
vlegs.remove(m)
2484
vmarks = set(vlegs).intersection(set(result.gamma.list_markings()))
2485
2486
if vmarks:
2487
mprime = list(vmarks)[0]
2488
vlegs.remove(mprime)
2489
leg = vlegs[0]
2490
legprime = result.gamma.leginversion(leg)
2491
for (kappa, psi, coeff) in result.poly:
2492
if legprime in psi:
2493
temp = psi.pop(legprime)
2494
psi[mprime] = temp
2495
2496
result.gamma.forget_markings([m])
2497
result.gamma.stabilize()
2498
vertim.pop(v)
2499
for (kappa, psi, coeff) in result.poly:
2500
kappa.pop(v)
2501
else:
2502
# no vertex becomes unstable, hence result.gamma is unchanged
2503
# we proceed to push forward on the space corresponding to marking v
2504
g = result.gamma.genera(v)
2505
n = result.gamma.num_legs(v) - 1 # n of the target space
2506
numvert = result.gamma.num_verts()
2507
2508
newpoly = kppoly([], [])
2509
for (kappa, psi, coeff) in result.poly:
2510
trunckappa = [kap[:] for kap in kappa]
2511
trunckappa[v] = []
2512
truncpsi = psi.copy()
2513
for l in result.gamma.legs(v, copy=False):
2514
truncpsi.pop(l, 0)
2515
trunckppoly = kppoly([(trunckappa, truncpsi)], [coeff]) # kppoly-term supported away from vertex v
2516
2517
kappav = kappa[v]
2518
psiv = {l: psi.get(l, 0) for l in result.gamma.legs(v, copy=False) if l != m}
2519
psivpoly = kppoly([([[] for i in range(numvert)], psiv)], [1])
2520
a_m = psi.get(m, 0)
2521
2522
cposs = []
2523
for b in kappav:
2524
cposs.append(list(range(b + 1)))
2525
# if kappav=[3,0,1] then cposs = [[0,1,2,3],[0],[0,1]]
2526
2527
currpoly = kppoly([], [])
2528
2529
for cvect in itertools.product(*cposs):
2530
currpoly += prod([binomial(kappav[i], cvect[i]) for i in range(len(cvect))]) * prod([kappacl(v, i + 1, numvert)**cvect[i] for i in range(
2531
len(cvect))]) * psivpoly * kappacl(v, a_m + sum([(kappav[i] - cvect[i]) * (i + 1) for i in range(len(cvect))]) - 1, numvert, g=g, n=n)
2532
if a_m == 0:
2533
kappapoly = prod([kappacl(v, i + 1, numvert)**kappav[i] for i in range(len(kappav))])
2534
2535
def psiminuspoly(l):
2536
psivminus = psiv.copy()
2537
psivminus[l] -= 1
2538
return kppoly([([[] for i in range(numvert)], psivminus)], [1])
2539
2540
currpoly += sum([psiminuspoly(l) for l in psiv if psiv[l] > 0]) * kappapoly
2541
2542
newpoly += currpoly * trunckppoly
2543
2544
result.poly = newpoly
2545
result.gamma.forget_markings([m])
2546
2547
result.dimension_filter()
2548
2549
result.gamma.set_immutable()
2550
if dicv:
2551
return (result, {v: vertim[v] for v in range(len(vertim))})
2552
else:
2553
return result
2554
2555
# TODO: why the rename argument is here? shouldn't it always be True?
2556
2557
def forgetful_pullback_list(self, newmark, rename=True):
2558
r"""
2559
Return a list of :class:`decstratum` that corresponds to the pullback under
2560
the forgetful map which forget the markings in ``newmark``.
2561
"""
2562
if not newmark:
2563
return [self]
2564
if rename:
2565
# Make sure that none of the newmark appears as a leg of the graph
2566
mleg = max(self.gamma._maxleg + 1, max(newmark) + 1)
2567
rnself = self.copy(mutable=True)
2568
2569
dic = {i: i for l in self.gamma._legs for i in l}
2570
for e in self.gamma.edges(copy=False):
2571
for i in e:
2572
if i in newmark:
2573
dic[i] = mleg
2574
mleg += 1
2575
rnself.gamma.relabel({}, dic, inplace=True)
2576
2577
# Now fix psi-data
2578
rnself.poly.rename_legs(dic)
2579
else:
2580
rnself = self
2581
2582
# TODO: change recursive definition to explicit, direct definition?
2583
# mdist = itertools.product(*[list(range(len(rnself.gamma.genera))) for j in range(len(newmark))])
2584
2585
# Strategy: first compute pullback under forgetting newmark[0], then by recursion pull this back under forgetting other elements of newmark
2586
a = newmark[0]
2587
nextnewmark = newmark[1:]
2588
partpullback = [] # list of decstratums to construct tautological class obtained by forgetful-pulling-back a
2589
2590
for v in range(self.gamma.num_verts()):
2591
# v gives the vertex where the marking a goes
2592
2593
# first the purely kappa-psi-term
2594
# grv is the underlying stgraph
2595
# TODO: to be changed (direct access to private attributes _legs)
2596
grv = rnself.gamma.copy()
2597
grv._legs[v] += [a]
2598
grv.tidy_up()
2599
grv.set_immutable()
2600
2601
# polyv is the underlying kppoly
2602
polyv = kppoly([], [])
2603
for (kappa, psi, coeff) in rnself.poly:
2604
# for kappa[v]=(3,0,2) gives [[0,1,2,3],[0],[0,1,2]]
2605
binomdist = [list(range(0, m + 1)) for m in kappa[v]]
2606
2607
for bds in itertools.product(*binomdist):
2608
kappa_new = [kap[:] for kap in kappa]
2609
kappa_new[v] = list(bds)
2610
2611
psi_new = psi.copy()
2612
psi_new[a] = sum([(i + 1) * (kappa[v][i] - bds[i]) for i in range(len(bds))])
2613
polyv = polyv + kppoly([(kappa_new, psi_new)], [((-1)**(sum([(kappa[v][i] - bds[i]) for i in range(len(bds))])))
2614
* coeff * prod([binomial(kappa[v][i], bds[i]) for i in range(len(bds))])])
2615
2616
# now the terms with boundary divisors
2617
# TODO: to be changed (access to private attribute _maxleg)
2618
rnself.gamma.tidy_up()
2619
# this is the leg landing on vertex v, connecting it to the rational component below
2620
newleg = max(rnself.gamma._maxleg + 1, a + 1)
2621
grl = {} # dictionary: leg l -> graph with rational component attached where l was
2622
for l in rnself.gamma.legs(v, copy=False):
2623
# create a copy of rnself.gamma with leg l replaced by a rational component attached to v, containing a and l
2624
# TODO: use functions!!
2625
new_gr = rnself.gamma.copy()
2626
new_gr._legs[v].remove(l)
2627
new_gr._legs[v] += [newleg]
2628
new_gr._genera += [0]
2629
new_gr._legs += [[newleg + 1, a, l]]
2630
new_gr._edges += [(newleg, newleg + 1)]
2631
new_gr.tidy_up()
2632
new_gr.set_immutable()
2633
2634
grl[l] = new_gr
2635
2636
# contains kappa, psi polynomials attached to the graphs above
2637
polyl = {l: kppoly([], []) for l in rnself.gamma.legs(v, copy=False)}
2638
2639
for (kappa, psi, coeff) in rnself.poly:
2640
for j in set(psi).intersection(set(rnself.gamma.legs(v, copy=False))):
2641
if psi[j] == 0: # no real psi class here
2642
continue
2643
# rational component, which is placed last in the graphs above, doesn't get kappa classes
2644
kappa_new = [kap[:] for kap in kappa] + [[]]
2645
psi_new = psi.copy()
2646
2647
# psi**b at leg l is replaced by -psi^(b-1) at the leg connecting v to the new rational component
2648
psi_new[newleg] = psi_new.pop(j) - 1
2649
2650
polyl[j] = polyl[j] + kppoly([(kappa_new, psi_new)], [-coeff])
2651
partpullback += [decstratum(grv, poly=polyv)] + [decstratum(grl[l], poly=polyl[l])
2652
for l in rnself.gamma.legs(v, copy=False) if len(polyl[l]) > 0]
2653
2654
# now partpullback is list of decstratums; call recursively on the next newmark
2655
result = []
2656
for ds in partpullback:
2657
result.extend(ds.forgetful_pullback_list(nextnewmark))
2658
return result
2659
2660
def forgetful_pullback(self, newmark, rename=True):
2661
return tautclass(self.forgetful_pullback_list(newmark, rename))
2662
2663
# converts the decstratum self to a prodtautclass on self.gamma
2664
def convert_to_prodtautclass(self):
2665
# TODO: to be changed (direct access to private attributes _genera, _legs)
2666
terms = []
2667
for (kappa, psi, coeff) in self.poly:
2668
currterm = []
2669
for v in range(self.gamma.num_verts()):
2670
psiv = {(lnum + 1): psi[self.gamma._legs[v][lnum]]
2671
for lnum in range(len(self.gamma._legs[v])) if self.gamma._legs[v][lnum] in psi}
2672
currterm.append(decstratum(stgraph([self.gamma.genera(v)], [list(
2673
range(1, len(self.gamma._legs[v]) + 1))], []), kappa=[kappa[v]], psi=psiv))
2674
currterm[0].poly *= coeff
2675
terms.append(currterm)
2676
return prodtautclass(self.gamma, terms)
2677
2678
# returns a list [(g,n,r), ...] of all genera g, number n of markings and degrees r for the terms appearing in self
2679
def gnr_list(self):
2680
g = self.gamma.g()
2681
n = self.gamma.n()
2682
e = self.gamma.num_edges()
2683
L = ((g, n, e + self.poly.deg(i)) for i in range(len(self.poly.monom)))
2684
return list(set(L))
2685
2686
# stores list of Hdecstratums, together with methods for scalar multiplication from left and sum
2687
2688
2689
class Htautclass:
2690
def __init__(self, terms):
2691
self.terms = terms
2692
2693
def copy(self):
2694
ans = Htautclass.__new__(Htautclass)
2695
ans.terms = [Hds.copy(mutable=False) for Hds in self.terms]
2696
return ans
2697
2698
def __neg__(self):
2699
return (-1) * self
2700
2701
def __add__(self, other): # TODO: add += operation
2702
if other == 0:
2703
return self.copy()
2704
new = self.copy()
2705
new.terms += [Hds.copy(mutable=False) for Hds in other.terms]
2706
return new.consolidate()
2707
2708
def __radd__(self, other):
2709
if other == 0:
2710
return self
2711
else:
2712
return self + other
2713
2714
def __rmul__(self, other):
2715
new = self.copy()
2716
for i in range(len(new.terms)):
2717
new.terms[i] = other * new.terms[i]
2718
return new
2719
2720
def __mul__(self, other):
2721
from .tautological_ring import TautologicalClass
2722
if isinstance(other, TautologicalClass):
2723
return sum([a * b for a in self.terms for b in other._terms.values()])
2724
2725
def __repr__(self):
2726
return '\n\n'.join(repr(self.terms[i])
2727
for i in range(len(self.terms)))
2728
2729
def consolidate(self): # TODO: Check for isomorphisms of Gstgraphs for terms, add corresponding kppolys
2730
for i in range(len(self.terms)):
2731
self.terms[i].consolidate()
2732
return self
2733
2734
# returns self as a prodHclass on the correct trivial graph
2735
# TODO: if self.terms==[], this does not produce a meaningful result
2736
def to_prodHclass(self):
2737
if not self.terms:
2738
raise ValueError('Htautclass does not know its graph, failed to convert to prodHclass')
2739
else:
2740
gamma0 = stgraph([self.terms[0].Gr.gamma.g()], [self.terms[0].Gr.gamma.list_markings()], [])
2741
terms = [t.to_decHstratum() for t in self.terms]
2742
return prodHclass(gamma0, terms)
2743
2744
# computes the pushforward under the map \bar H -> M_{g',b}, sending C to C/G
2745
# the result is a tautclass
2746
def quotient_pushforward(self):
2747
if not self.terms:
2748
# TODO: this raises a warning
2749
return tautclass([])
2750
return sum(c.quotient_pushforward() for c in self.terms)
2751
2752
# pull back tautclass other on \bar M_{g',b} under the quotient map delta and return multiplication with self
2753
# effectively: pushforward self (divide by deg(delta)), multiply with other, then pull back entire thing
2754
# TODO: verify that this is allowed
2755
def quotient_pullback(self, other):
2756
if not self.terms:
2757
return deepcopy(self) # self=0, so pullback still zero
2758
g = self.terms[0].Gr.gamma.g()
2759
hdata = self.terms[0].Gr.hdata
2760
trivGg = trivGgraph(g, hdata)
2761
deltadeg = trivGg.delta_degree(0)
2762
2763
push = (QQ(1) / deltadeg) * self.quotient_pushforward()
2764
pro = push * other
2765
pro.dimension_filter()
2766
2767
# result = quotient pullback of pro to trivGg
2768
result = Htautclass([])
2769
edgenums = set(t.gamma.num_edges() for t in pro._terms.values())
2770
Hstrata = {i: list_Hstrata(g, hdata, i) for i in edgenums}
2771
Hquots = {i: list_quotgraphs(g, hdata, i, True) for i in edgenums}
2772
2773
for t in pro._terms.values():
2774
# t is a decstratum on \bar M_{g',b} living on a graph t.gamma
2775
# list all Hstrata having quotient graph t.gamma, then pull back t.poly and scale by right multiplicity
2776
(r, ti, tdicv, tdicl) = deggrfind(t.gamma)
2777
# tdicv and tdicl send vertex/leg names in t.gamma to the names of the vertices/legs in the standard graph
2778
2779
preims = [j for j in range(len(Hstrata[r])) if Hquots[r][j][1] == ti]
2780
2781
for j in preims:
2782
# this corresponds to a Gstgraph, which needs to be decorated by pullbacks of t.poly and added to result
2783
newGr = Hstrata[r][j].copy()
2784
numvert = newGr.gamma.num_verts()
2785
multiplicity = prod([newGr.lstabilizer(e0).order() for (e0, e1) in Hquots[r][j][0].edges(
2786
copy=False)]) * t.gamma.automorphism_number() / QQ(len(equiGraphIsom(newGr, newGr)))
2787
(quotientgraph, inde, dicv, dicvinv, dicl, diclinv) = Hquots[r][j]
2788
2789
newpoly = kppoly([], [])
2790
for (kappa, psi, coeff) in t.poly:
2791
newcoeff = coeff
2792
newkappa = [[] for u in range(numvert)]
2793
for v in range(len(kappa)):
2794
newkappa[dicvinv[tdicv[v]]] = kappa[v]
2795
newcoeff *= (QQ(1) / newGr.vstabilizer(dicvinv[tdicv[v]]).order())**(sum(kappa[v]))
2796
newpsi = {diclinv[tdicl[l]]: psi[l] for l in psi}
2797
newcoeff *= prod([(newGr.lstabilizer(diclinv[tdicl[l]]).order())**(psi[l]) for l in psi])
2798
2799
newpoly += kppoly([(newkappa, newpsi)], [newcoeff])
2800
2801
newpoly *= multiplicity
2802
2803
result.terms.append(Hdecstratum(newGr, poly=newpoly))
2804
return result
2805
2806
2807
# H-tautological class given by boundary stratum of Hurwitz space, i.e. a Gstgraph, decorated by a polynomial in kappa-classes on the vertices and psi-classes on the legs (i.e. half-edges and markings)
2808
# self.Gr = underlying Gstgraph for the boundary stratum
2809
# self.kappa = list of length len(self.gamma.genera) of lists of the form (3,0,2) meaning that this vertex carries kappa_1^3*kappa_3^2
2810
# self.psi = dictionary, associating nonnegative integers to some legs, where psi[l]=3 means that there is a psi^3 at this leg
2811
# Convention: kappa_a = pi_*(c_1(omega_pi(sum p_i))^{a+1}), where pi is the universal curve over the moduli space, omega_pi the relative dualizing sheaf and p_i are the (images of the) sections of pi given by the marked points
2812
class Hdecstratum:
2813
def __init__(self, Gr, kappa=None, psi=None, poly=None):
2814
self.Gr = Gr
2815
if kappa is None:
2816
kappa = [[] for i in range(Gr.gamma.num_verts())]
2817
if psi is None:
2818
psi = {}
2819
if poly is None:
2820
self.poly = kppoly([(kappa, psi)], [1])
2821
else:
2822
self.poly = poly
2823
# def deg(self):
2824
# return len(self.gamma.edges)+sum([sum([k[i]*(i+1) for i in range(len(k))]) for k in kappa])+sum(psi.values())
2825
2826
def copy(self, mutable=True):
2827
ans = Hdecstratum.__new__(Hdecstratum)
2828
ans.Gr = self.Gr.copy(mutable=mutable)
2829
ans.poly = self.poly.copy()
2830
return ans
2831
2832
def __neg__(self):
2833
return (-1) * self
2834
2835
def __rmul__(self, other):
2836
if isinstance(other, decstratum):
2837
return self.__mul__(other)
2838
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational):
2839
else:
2840
new = self.copy()
2841
new.poly *= other
2842
return new
2843
2844
def to_decHstratum(self):
2845
result = self.Gr.to_decHstratum()
2846
result.poly *= self.poly
2847
return result
2848
2849
def __mul__(self, other):
2850
if isinstance(other, decstratum):
2851
# Step 1: find a list commdeg of generic (self,other)-Gstgraphs Gamma3
2852
# Elements of this list will have the form (Gamma3,currentdeg) with currentdeg a list of elements (vdict1,ldict1,vdict2,ldict2), where
2853
# * Gamma3 is a Gstgraph with same group G as self.Gr
2854
# * vdict1, vdict2 are the vertex-maps from vertices of Gamma3 to self.Gr.gamma and other.gamma
2855
# * ldict1, ldict2 are the leg-maps from legs of self.Gr.gamma, other.gamma to Gamma3
2856
if self.Gr.gamma.num_edges() == 0: # TODO: implement intersection with non-trivial Gstgraph
2857
# minimal number of edge-orbits to hope for generic graph
2858
maxdeg = other.gamma.num_edges()
2859
mindeg = (QQ(maxdeg) / self.Gr.G.order()).ceil()
2860
2861
Ggrdegenerations = [list_Hstrata(self.Gr.gamma.g(), self.Gr.hdata, r) for r in range(maxdeg + 1)]
2862
2863
commdeg = []
2864
# gives for every entry in commdeg the number of degenerations isomorphic to this entry via Aut_G(Gamma3)
2865
multiplicityvector = []
2866
for deg in range(mindeg, maxdeg + 1):
2867
for Gamma3 in Ggrdegenerations[deg]:
2868
currentdeg = []
2869
currentmultiplicity = []
2870
otstructures = Astructures(Gamma3.gamma, other.gamma)
2871
if not otstructures:
2872
continue
2873
AutGr = equiGraphIsom(Gamma3, Gamma3)
2874
2875
# Now single out representatives of the AutGr-action on otstructures
2876
while otstructures:
2877
(dicvcurr, diclcurr) = otstructures[0]
2878
multiplicity = 0
2879
for (dicv, dicl) in AutGr:
2880
dicvnew = {v: dicvcurr[dicv[v]] for v in dicv}
2881
diclnew = {l: dicl[diclcurr[l]] for l in diclcurr}
2882
try:
2883
otstructures.remove((dicvnew, diclnew))
2884
multiplicity += 1
2885
except ValueError:
2886
pass
2887
# now must test if we are dealing with a generic structure
2888
2889
coveredlegs = set()
2890
for g in Gamma3.G:
2891
for l in diclcurr.values():
2892
coveredlegs.add(Gamma3.legact[(g, l)])
2893
# in general case would need to include half-edges from self.Gr.gamma
2894
if set(Gamma3.gamma.halfedges()).issubset(coveredlegs):
2895
currentdeg += [({v: 0 for v in range(Gamma3.gamma.num_verts())},
2896
{l: l for l in self.Gr.gamma.leglist()}, dicvcurr, diclcurr)]
2897
multiplicity *= QQ(1) / len(AutGr) # 1*numotheraut/len(AutGr)
2898
currentmultiplicity += [multiplicity]
2899
commdeg += [(Gamma3, currentdeg)]
2900
multiplicityvector += [currentmultiplicity]
2901
2902
# Step 1 finished
2903
2904
# Step2: go through commdeg, use vdicts to transfer kappa-classes and ldicts to transfer psi-classes
2905
# from self and other to Gamma3; also look for multiple edges in G-orbits on Gamma3 and put
2906
# the contribution (-psi-psi') coming from excess intersection there
2907
# TODO: figure out multiplicities from orders of orbits, automorphisms, etc.
2908
# TODO: eliminate early any classes that must vanish for dimension reasons
2909
# sum everything up and return it
2910
result = Htautclass([])
2911
for count in range(len(commdeg)):
2912
if not commdeg[count][1]:
2913
continue
2914
Gamma3 = commdeg[count][0]
2915
gammaresultpoly = kppoly([], [])
2916
for count2 in range(len(commdeg[count][1])):
2917
(vdict1, ldict1, vdict2, ldict2) = commdeg[count][1][count2]
2918
# compute preimages of vertices in self, other under the chosen morphisms to assign kappa-classes
2919
preim1 = [[] for i in range(self.Gr.gamma.num_verts())]
2920
for v in vdict1:
2921
preim1[vdict1[v]] += [v]
2922
preim2 = [[] for i in range(other.gamma.num_verts())]
2923
for v in vdict2:
2924
preim2[vdict2[v]] += [v]
2925
numvert = Gamma3.gamma.num_verts()
2926
2927
# excess intersection classes
2928
quotdicl = {}
2929
quotgraph = Gamma3.quotient_graph(dicl=quotdicl)
2930
numpreim = {l: 0 for l in quotgraph.halfedges()}
2931
2932
for l in self.Gr.gamma.halfedges():
2933
numpreim[quotdicl[ldict1[l]]] += 1
2934
for l in other.gamma.halfedges():
2935
numpreim[quotdicl[ldict2[l]]] += 1
2936
2937
excesspoly = onekppoly(numvert)
2938
for (e0, e1) in quotgraph.edges(copy=False):
2939
if numpreim[e0] > 1:
2940
excesspoly *= ((-1) * (psicl(e0, numvert) + psicl(e1, numvert)))**(numpreim[e0] - 1)
2941
2942
resultpoly = kppoly([], [])
2943
for (kappa1, psi1, coeff1) in self.poly:
2944
for (kappa2, psi2, coeff2) in other.poly:
2945
# pullback of psi-classes
2946
psipolydict = {ldict1[l]: psi1[l] for l in psi1}
2947
for l in psi2:
2948
psipolydict[ldict2[l]] = psipolydict.get(ldict2[l], 0) + psi2[l]
2949
psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])
2950
2951
# pullback of kappa-classes
2952
kappapoly1 = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim1[v]])**kappa1[v][k]
2953
for k in range(len(kappa1[v]))]) for v in range(self.Gr.gamma.num_verts())])
2954
kappapoly2 = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim2[v]])**kappa2[v][k]
2955
for k in range(len(kappa2[v]))]) for v in range(other.gamma.num_verts())])
2956
resultpoly += coeff1 * coeff2 * psipoly * kappapoly1 * kappapoly2
2957
2958
# TODO: divide by |Aut_G(Gamma3)| ??? if so, do in definition of multiplicityvector
2959
resultpoly *= multiplicityvector[count][count2] * excesspoly
2960
gammaresultpoly += resultpoly
2961
2962
result.terms += [Htautclass([Hdecstratum(Gamma3, poly=gammaresultpoly)])]
2963
return result
2964
2965
def __repr__(self):
2966
return 'Graph : ' + repr(self.Gr) + '\n' + 'Polynomial : ' + repr(self.poly)
2967
2968
def consolidate(self):
2969
self.poly.consolidate()
2970
return self
2971
2972
# computes the pushforward under the map \bar H -> M_{g',b}, sending C to C/G
2973
# the result is a tautclass
2974
def quotient_pushforward(self):
2975
Gord = self.Gr.G.order()
2976
2977
qdicv = {}
2978
qdicvinv = {}
2979
qdicl = {}
2980
quotgr = self.Gr.quotient_graph(dicv=qdicv, dicvinv=qdicvinv, dicl=qdicl)
2981
2982
preimlist = [[] for i in range(quotgr.num_verts())]
2983
for v in range(self.Gr.gamma.num_verts()):
2984
preimlist[qdicv[v]] += [v]
2985
2986
# first, for each vertex w in quotgr compute the degree of the delta-map \bar H(preim(v)) -> \bar M_{g(v),n(v)}
2987
deltadeg = {w: self.Gr.delta_degree(qdicvinv[w]) for w in qdicvinv}
2988
2989
# result will be decstratum on the graph quotgr
2990
# go through all terms of self.poly and add their pushforwards
2991
# for each term, go through vertices of quotient graph and collect all classes above it,
2992
# i.e. kappa-classes from vertex-preimages and psi-classes from leg-preimages
2993
2994
resultpoly = kppoly([], [])
2995
for (kappa, psi, coeff) in self.poly:
2996
kappanew = []
2997
coeffnew = coeff
2998
psinew = {}
2999
for w in range(quotgr.num_verts()):
3000
kappatemp = []
3001
for v in preimlist[w]:
3002
kappatemp = kappaadd(kappatemp, kappa[v])
3003
kappanew += [kappatemp]
3004
# Gord/len(preimlist[w]) is the order of the stabilizer of any preimage v of w
3005
coeffnew *= (QQ(Gord) / len(preimlist[w]))**sum(kappatemp)
3006
for l in psi:
3007
psinew[qdicl[l]] = psinew.get(qdicl[l], 0) + psi[l]
3008
coeffnew *= (QQ(1) / self.Gr.character[l][1])**psi[l]
3009
coeffnew *= prod(deltadeg.values())
3010
resultpoly += kppoly([(kappanew, psinew)], [coeffnew])
3011
return tautclass([decstratum(quotgr, poly=resultpoly)])
3012
3013
3014
def remove_trailing_zeros(l):
3015
r"""
3016
Remove the trailing zeroes t the end of l.
3017
3018
EXAMPLES::
3019
3020
sage: from admcycles.admcycles import remove_trailing_zeros
3021
sage: l = [0, 1, 0, 0]
3022
sage: remove_trailing_zeros(l)
3023
sage: l
3024
[0, 1]
3025
sage: remove_trailing_zeros(l)
3026
sage: l
3027
[0, 1]
3028
3029
sage: l = [0, 0]
3030
sage: remove_trailing_zeros(l)
3031
sage: l
3032
[]
3033
sage: remove_trailing_zeros(l)
3034
sage: l
3035
[]
3036
"""
3037
while l and l[-1] == 0:
3038
l.pop()
3039
3040
# returns sum of kappa-vectors for same vertex, so [0,2]+[1,2,3]=[1,4,3]
3041
3042
3043
def kappaadd(a, b):
3044
if len(a) < len(b):
3045
aprime = a + (len(b) - len(a)) * [0]
3046
else:
3047
aprime = a
3048
if len(b) < len(a):
3049
bprime = b + (len(a) - len(b)) * [0]
3050
else:
3051
bprime = b
3052
return [aprime[i] + bprime[i] for i in range(len(aprime))]
3053
3054
3055
def Graphtodecstratum(G):
3056
r"""
3057
Converts a Pixton-style Graph (matrix with univariate-Polynomial entries) into a decstratum.
3058
3059
Assume that markings on Graph are called 1, 2, 3, ..., n.
3060
3061
The function :func:`Pixtongraph` provides the conversion in the other direction.
3062
3063
EXAMPLES::
3064
3065
sage: from admcycles.DR.graph import Graph, R, X
3066
sage: from admcycles.admcycles import Graphtodecstratum
3067
sage: G = Graph(matrix(R, 3, 2, [-1, 0, 1, 1, X + 2, 1]))
3068
sage: Graphtodecstratum(G)
3069
Graph : [1, 2] [[2], [3]] [(2, 3)]
3070
Polynomial : (kappa_1)_1
3071
"""
3072
# first care about vertices, i.e. genera and kappa-classes
3073
genera = []
3074
kappa = []
3075
for i in range(1, G.M.nrows()):
3076
genera.append(G.M[i, 0][0])
3077
kappa.append([G.M[i, 0][j] for j in range(1, G.M[i, 0].degree() + 1)])
3078
# now care about markings as well as edges together with the corresponding psi-classes
3079
legs = [[] for i in genera]
3080
edges = []
3081
psi = {}
3082
legname = G.M.ncols() # legs corresponding to edges are named legname, legname+1, ...; cannot collide with marking-names
3083
for j in range(1, G.M.ncols()):
3084
for i in range(1, G.M.nrows()):
3085
if G.M[i, j] != 0:
3086
break
3087
# now i is the index of the first nonzero entry in column j
3088
if G.M[0, j] != 0: # this is a marking
3089
legs[i - 1].append(ZZ(G.M[0, j]))
3090
if G.M[i, j].degree() >= 1:
3091
psi[G.M[0, j]] = G.M[i, j][1]
3092
3093
if G.M[0, j] == 0: # this is a leg, possibly self-node
3094
if G.M[i, j][0] == 2: # self-node
3095
legs[i - 1] += [legname, legname + 1]
3096
edges.append((legname, legname + 1))
3097
if G.M[i, j].degree() >= 1:
3098
psi[legname] = G.M[i, j][1]
3099
if G.M[i, j].degree() >= 2:
3100
psi[legname + 1] = G.M[i, j][2]
3101
legname += 2
3102
if G.M[i, j][0] == 1: # edge to different vertex
3103
if G.M.nrows() <= i + 1:
3104
print('Attention')
3105
print(G.M)
3106
for k in range(i + 1, G.M.nrows()):
3107
if G.M[k, j] != 0:
3108
break
3109
# now k is the index of the second nonzero entry in column j
3110
legs[i - 1] += [legname]
3111
legs[k - 1] += [legname + 1]
3112
edges.append((legname, legname + 1))
3113
if G.M[i, j].degree() >= 1:
3114
psi[legname] = G.M[i, j][1]
3115
if G.M[k, j].degree() >= 1:
3116
psi[legname + 1] = G.M[k, j][1]
3117
legname += 2
3118
return decstratum(stgraph(genera, legs, edges), kappa=kappa, psi=psi)
3119
3120
3121
@file_cache.file_cached_function(
3122
directory=os.path.join(DOT_SAGE, "admcycles"),
3123
url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",
3124
remote_database_list="https://modulispaces.gitlab.io/relations-database/generating_indices_files",
3125
env_var="ADMCYCLES_CACHE_DIR",
3126
key=file_cache.ignore_args_key([3, 4]),
3127
filename=file_cache.ignore_args_filename())
3128
def generating_indices(g, n, r, FZ=True, FZmethod=None, moduli='st'):
3129
r"""
3130
Return the indices of a basis of `R^r(\bar M_{g,n})` in terms of the standard list of generators.
3131
3132
This function assumes that the generalized Faber-Zagier relations [PPZ15]_
3133
are a complete set of relations. For most parameters this is computed by
3134
using the FZ-relations. However, if r is greater than half the dimension of
3135
\bar M_{g,n} and all cohomology is tautological, we rather compute a
3136
generating set using Poincare duality and the intersection pairing
3137
3138
3139
INPUT:
3140
3141
FZ : bool (default: True)
3142
Whether FZ relations are used to compute the basis. If False (only for moduli='st'),
3143
instead compute basis by pairing matrix from opposite degree.
3144
3145
FZmethod : string (default: None)
3146
Which method is to be used for computing FZ relations, the options are '3spin' and 'newrels'.
3147
If no method is supplied we use the 3-spin formula when ``r`` < 3 and
3148
the new relation method otherwise.
3149
3150
moduli : string (default: 'st')
3151
If instead of 'st' one of 'ct', 'rt' or 'sm' is given, compute a basis for the tautological
3152
ring of the corresponding open subset of `\bar M_{g,n}`.
3153
3154
EXAMPLES::
3155
3156
sage: from admcycles import generating_indices
3157
sage: generating_indices(1, 2, 1)
3158
[0, 1]
3159
sage: generating_indices(1, 2, 1, FZ=False)
3160
[0, 1]
3161
sage: generating_indices(1, 2, 1, moduli='ct')
3162
[0]
3163
sage: generating_indices(1, 1, 1, moduli='ct')
3164
[]
3165
3166
TESTS::
3167
3168
sage: from admcycles import generating_indices
3169
sage: generating_indices(9, 0, 3, FZmethod='3spin', moduli='sm')
3170
[0, 1, 2]
3171
sage: generating_indices(9, 0, 3, FZmethod='newrels', moduli='sm')
3172
[0, 1, 2]
3173
3174
We do the same as above, but we avoid the use of any chached results.
3175
3176
sage: from admcycles import generating_indices
3177
sage: generating_indices.f(1, 2, 1)
3178
[0, 1]
3179
sage: generating_indices.f(1, 2, 1, FZ=False)
3180
[0, 1]
3181
sage: generating_indices.f(1, 2, 1, moduli='ct')
3182
[0]
3183
sage: generating_indices.f(1, 1, 1, moduli='ct')
3184
[]
3185
3186
Test the oneline database::
3187
3188
sage: from admcycles import generating_indices
3189
sage: from tempfile import mkdtemp
3190
sage: from shutil import rmtree
3191
sage: import os
3192
sage: import pickle
3193
sage: generating_indices.set_online_lookup(True)
3194
sage: filename = "generating_indices_0_3_0_st.pkl"
3195
sage: tmpdir = mkdtemp()
3196
sage: filename_with_path = os.path.join(tmpdir, filename)
3197
sage: generating_indices._FileCachedFunction__download(filename, filename_with_path) # optional - internet
3198
sage: f = open(filename_with_path, "rb") # optional - internet
3199
sage: pickle.load(f) # optional - internet
3200
[0]
3201
sage: rmtree(tmpdir)
3202
"""
3203
if moduli == 'tl':
3204
raise NotImplementedError('generating_indices not implemented for treelike curves')
3205
if FZ or r <= (3 * g - 3 + n) / QQ(2) or not (g <= 1 or (g == 2 and n <= 19)):
3206
if FZmethod is None:
3207
spinmethod = r < 3
3208
if FZmethod == '3spin':
3209
spinmethod = True
3210
if FZmethod == 'newrels':
3211
spinmethod = False
3212
# r < 3 is an extremely rough first approximation of when it is better to use the FZ approach
3213
rel = DR.rels_matrix(g, r, n, symm=0, moduli_type=get_moduli(moduli, DRpy=True), usespin=spinmethod)
3214
if rel.nrows() == 0: # no relation
3215
genstobasis.set_cache(list((QQ**rel.ncols()).basis()), *(g, n, r, moduli))
3216
return list(range(rel.ncols()))
3217
relconv = matrix([DR.convert_vector_to_monomial_basis(rel.row(i), g, r, tuple(range(1, n + 1)),
3218
moduli_type=get_moduli(moduli, DRpy=True)) for i in range(rel.nrows())])
3219
del rel
3220
reverseindices = list(range(relconv.ncols()))
3221
reverseindices.reverse()
3222
reordrelconv = relconv.matrix_from_columns(reverseindices)
3223
del relconv
3224
reordrelconv.echelonize() # (algorithm='classical')
3225
3226
reordnongens = reordrelconv.pivots()
3227
nongens = [reordrelconv.ncols() - i - 1 for i in reordnongens]
3228
gens = [i for i in range(reordrelconv.ncols()) if i not in nongens]
3229
3230
# compute result of genstobasis here, for efficiency reasons
3231
gtob = {gens[j]: vector(QQ, len(gens), {j: 1}) for j in range(len(gens))}
3232
for i in range(len(reordnongens)):
3233
gtob[nongens[i]] = vector([-reordrelconv[i, reordrelconv.ncols() - j - 1] for j in gens])
3234
genstobasis.set_cache([gtob[i] for i in range(reordrelconv.ncols())], g, n, r, moduli)
3235
return gens
3236
else:
3237
if moduli != 'st':
3238
raise NotImplementedError(
3239
'generating indices via pairing only implemented for whole moduli of stable curves')
3240
gencompdeg = generating_indices(g, n, 3 * g - 3 + n - r, True)
3241
maxrank = len(gencompdeg)
3242
currrank = 0
3243
M = matrix(maxrank, 0)
3244
gens = []
3245
count = 0
3246
while currrank < maxrank:
3247
Mnew = block_matrix([[M, matrix(DR.pairing_submatrix(tuple(gencompdeg), (count,), g,
3248
3 * g - 3 + n - r, tuple(range(1, n + 1))))]], subdivide=False)
3249
if Mnew.rank() > currrank:
3250
gens.append(count)
3251
M = Mnew
3252
currrank = Mnew.rank()
3253
count += 1
3254
return gens
3255
3256
3257
def Hintnumbers(g, dat, indices=None, redundancy=False):
3258
r"""
3259
TESTS::
3260
3261
We test that the function works correctly if the product Hbar * tautclass(...) is empty for some indices.
3262
3263
sage: from admcycles.admcycles import HurData, Hintnumbers
3264
sage: G = CyclicPermutationGroup(2)
3265
sage: g = G.gen()
3266
sage: e = G.one()
3267
sage: dat = HurData(G, [g, e, g, e])
3268
sage: Hintnumbers(0, dat)
3269
[4, 1, 2, 2, 1, 2, 2, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
3270
"""
3271
Hbar = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])
3272
dimens = Hbar.terms[0].Gr.dim()
3273
markins = tuple(range(1, 1 + len(Hbar.terms[0].Gr.gamma.list_markings())))
3274
3275
strata = DR.all_strata(g, dimens, markins)
3276
intnumbers = []
3277
3278
if indices is None or redundancy:
3279
effindices = list(range(len(strata)))
3280
else:
3281
effindices = indices
3282
3283
for i in effindices:
3284
# If the below product does not have any terms, quotient_pushforward
3285
# returns ZZ(0) and evaluate raises an exception.
3286
# TODO: A better way to fix this may be to add arguments g, n to
3287
# Htautclass, then quotient_pushforward can always return a
3288
# tautological class.
3289
prod = Hbar * tautclass([Graphtodecstratum(strata[i])])
3290
if prod.terms:
3291
intnumbers += [prod.quotient_pushforward().evaluate()]
3292
else:
3293
intnumbers += [ZZ(0)]
3294
3295
if not redundancy:
3296
return intnumbers
3297
3298
M = DR.FZ_matrix(g, dimens, markins)
3299
Mconv = matrix([DR.convert_vector_to_monomial_basis(M.row(i), g, dimens, markins) for i in range(M.nrows())])
3300
relcheck = Mconv * vector(intnumbers)
3301
if relcheck == 2 * relcheck: # lazy way to check relcheck==(0,0,...,0)
3302
print('Intersection numbers are consistent, number of checks: ' + repr(len(relcheck)))
3303
else:
3304
print('Intersection numbers not consistent for ' + repr((g, dat.G, dat.l)))
3305
print('relcheck = ' + repr(relcheck))
3306
3307
if indices is None:
3308
return intnumbers
3309
else:
3310
return [intnumbers[j] for j in indices]
3311
3312
3313
def Hidentify(g, dat, method='pull', vecout=False, redundancy=False, markings=None):
3314
r"""
3315
Identifies the (pushforward of the) fundamental class of \bar H_{g,dat} in
3316
terms of tautological classes.
3317
3318
INPUT:
3319
3320
- ``g`` -- integer; genus of the curve C of the cover C -> D
3321
3322
- ``dat`` -- HurwitzData; ramification data of the cover C -> D
3323
3324
- ``method`` -- string (default: `'pull'`); method of computation
3325
3326
method='pull' means we pull back to the boundary strata and recursively
3327
identify the result, then use linear algebra to restrict the class of \bar H
3328
to an affine subvectorspace (the corresponding vector space is the
3329
intersection of the kernels of the above pullback maps), then we use
3330
intersections with kappa,psi-classes to get additional information.
3331
If this is not sufficient, return instead information about this affine
3332
subvectorspace.
3333
3334
method='pair' means we compute all intersection pairings of \bar H with a
3335
generating set of the complementary degree and use this to reconstruct the
3336
class.
3337
3338
- ``vecout`` -- bool (default: `False`); return a coefficient-list with respect
3339
to the corresponding list of generators tautgens(g,n,r).
3340
NOTE: if vecout=True and markings is not the full list of markings 1,..,n, it
3341
will return a vector for the space with smaller number of markings, where the
3342
markings are renamed in an order-preserving way.
3343
3344
- ``redundancy`` -- bool (default: `False`); compute pairings with all possible
3345
generators of complementary dimension (not only a generating set) and use the
3346
redundant intersections to check consistency of the result; only valid in case
3347
method='pair'.
3348
3349
- ``markings`` -- list (default: `None`); return the class obtained by the
3350
fundamental class of the Hurwitz space by forgetting all points not in
3351
markings; for markings = None, return class without forgetting any points.
3352
3353
EXAMPLES::
3354
3355
sage: from admcycles.admcycles import trivGgraph, HurData, Hidentify
3356
sage: G = PermutationGroup([(1, 2)])
3357
sage: H = HurData(G, [G[1], G[1]])
3358
sage: t = Hidentify(2, H, markings=[]); t # Bielliptic locus in \Mbar_2
3359
Graph : [2] [[]] []
3360
Polynomial : 30*(kappa_1)_0
3361
<BLANKLINE>
3362
Graph : [1, 1] [[2], [3]] [(2, 3)]
3363
Polynomial : -9
3364
3365
Check that pair and pull methods are compatible::
3366
3367
sage: from admcycles.admcycles import Hdatabase
3368
sage: Hdatabase.clear() # remove the cached results from the previous computation
3369
sage: t2 = Hidentify(2, H, markings=[], method='pair')
3370
sage: t == t2
3371
True
3372
3373
sage: G = PermutationGroup([(1, 2, 3)])
3374
sage: H = HurData(G, [G[1]])
3375
sage: t = Hidentify(2, H, markings=[]); t.is_zero() # corresponding space is empty
3376
True
3377
3378
TESTS::
3379
3380
sage: G = PermutationGroup([(1, 2)])
3381
sage: H = HurData(G, [G[1], G[1]])
3382
sage: Hidentify(2, H, markings=[2], vecout=True)
3383
(60, -21, 66, -123, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
3384
sage: Hdatabase.clear() # remove the cached results from the previous computation
3385
sage: Hidentify(2, H, markings=[2], vecout=True, method='pair')
3386
(60, -21, 66, -123, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
3387
"""
3388
Hbar = trivGgraph(g, dat)
3389
r = Hbar.dim()
3390
n = len(Hbar.gamma.list_markings())
3391
3392
# global Hexam
3393
# Hexam=(g,dat,method,vecout,redundancy,markings)
3394
3395
if markings is None:
3396
markings = list(range(1, n + 1))
3397
3398
N = len(markings)
3399
msorted = sorted(markings)
3400
markingdictionary = {i + 1: msorted[i] for i in range(N)}
3401
invmarkingdictionary = {markingdictionary[j]: j for j in markingdictionary}
3402
3403
# Section to deal with the case that the group G is trivial quickly
3404
if dat.G.order() == 1:
3405
if len(markings) < n:
3406
return tautclass([])
3407
else:
3408
return tautclass([decstratum(stgraph([g], [list(range(1, n + 1))], []))])
3409
3410
if not cohom_is_taut(g, len(markings), 2 * (3 * g - 3 + len(markings) - r)):
3411
print('Careful, Hurwitz cycle ' + repr((g, dat)) + ' might not be tautological!\n')
3412
3413
# first: try to look up if we already computed this class (or a class with even more markings) before
3414
found = False
3415
try:
3416
Hdb = Hdatabase[(g, dat)]
3417
for marks in Hdb:
3418
if set(markings).issubset(set(marks)):
3419
forgottenmarks = set(marks) - set(markings)
3420
result = deepcopy(Hdb[marks])
3421
result = result.forgetful_pushforward(list(forgottenmarks))
3422
found = True
3423
# TODO: potentially include this result in Hdb
3424
3425
except KeyError:
3426
pass
3427
3428
# if not, proceed according to method given
3429
if method == 'pair' and not found:
3430
Hbar = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])
3431
3432
def pairf(a):
3433
prod = Hbar * a
3434
if not prod.terms:
3435
return QQ(0)
3436
return prod.quotient_pushforward().evaluate()
3437
from .tautological_ring import TautologicalRing
3438
R = TautologicalRing(g, n)
3439
result = R.identify_class(3 * g - 3 + n - r, pairfunction=pairf, check=redundancy)
3440
3441
if (g, dat) not in Hdatabase:
3442
Hdatabase[(g, dat)] = {}
3443
Hdatabase[(g, dat)][tuple(range(1, n + 1))] = result
3444
3445
if len(markings) < n:
3446
result = Hidentify(g, dat, method, False, redundancy, markings)
3447
if method == 'pull' and not found:
3448
from .tautological_ring import TautologicalRing
3449
R = TautologicalRing(g, N)
3450
bar_H = prodHclass(stgraph([g], [list(range(1, N + 1))], []), [decHstratum(stgraph([g],
3451
[list(range(1, N + 1))], []), {0: (g, dat)}, [[0, markingdictionary]])])
3452
3453
def pullfun(gamma):
3454
return bar_H.gamma0_pullback(gamma).toprodtautclass()
3455
3456
def kpfun(kappa, psi, cl):
3457
return (bar_H * cl).evaluate()
3458
3459
result = R.identify_class(3 * g - 3 + N - r, pullfunction=pullfun, kappapsifunction=kpfun, check=redundancy)
3460
result = result.rename_legs(markingdictionary, inplace=False)
3461
3462
# return section
3463
if not found:
3464
if (g, dat) not in Hdatabase:
3465
Hdatabase[(g, dat)] = {}
3466
Hdatabase[(g, dat)][tuple(markings)] = deepcopy(result)
3467
3468
if vecout:
3469
rnresult = result.rename_legs(invmarkingdictionary, inplace=False)
3470
return rnresult.vector(3 * g - 3 + len(markings) - r)
3471
else:
3472
return result
3473
3474
3475
@cached_function
3476
def pullback_matrix(g, n, d, bdry=None, irrbdry=True):
3477
r"""
3478
Compute matrix representing pullback of basis of the tautological ring
3479
of Mbar_g,n in degree d under boundary pullbacks.
3480
3481
If bdry is given (as a StableGraph with exactly one edge) it computes
3482
the matrix whose columns are the pullback of our preferred basis to the
3483
divisor bdry, identified in the (tensor product) Tautbasis.
3484
If bdry=None, concatenate all matrices for all possible boundary
3485
divisors (in their order inside list_strata(g,n,1)).
3486
3487
If additionally irrbdry=False, omit the pullback to the irreducible
3488
boundary (since there computations get more complicated).
3489
3490
EXAMPLES::
3491
3492
sage: from admcycles.admcycles import StableGraph, pullback_matrix, list_strata
3493
sage: pullback_matrix(2, 0, 1)
3494
[ 1 -2]
3495
[ 0 4]
3496
[ 1 -2]
3497
[ 1 -2]
3498
sage: L = list_strata(2,0,1); L
3499
[[1] [[1, 2]] [(1, 2)], [1, 1] [[1], [2]] [(1, 2)]]
3500
sage: pullback_matrix(2, 0, 1, L[0])
3501
[ 1 -2]
3502
[ 0 4]
3503
sage: pullback_matrix(2, 0, 1, L[1])
3504
[ 1 -2]
3505
[ 1 -2]
3506
"""
3507
genindices = generating_indices(g, n, d) # indices of generators corresponding to columns of A
3508
strata = DR.all_strata(g, d, tuple(range(1, n + 1)))
3509
gens = [Graphtodecstratum(strata[i]) for i in genindices]
3510
ngens = len(genindices)
3511
3512
if bdry is None:
3513
A = matrix(0, ngens)
3514
bdrydiv = list_strata(g, n, 1)
3515
3516
for bdry in bdrydiv:
3517
if irrbdry or bdry.num_verts() != 1:
3518
A = block_matrix([[A], [pullback_matrix(g, n, d, bdry)]], subdivide=False)
3519
A.set_immutable()
3520
return A
3521
else:
3522
pullbacks = [bdry.boundary_pullback(cl).totensorTautbasis(d, True) for cl in gens]
3523
A = matrix(pullbacks).transpose()
3524
A.set_immutable()
3525
return A
3526
3527
3528
def kpintersection_matrix(g, n, d):
3529
r"""
3530
Computes the matrix B whose columns are the intersection numbers of our preferred
3531
generating_indices(g,n,d) with the list kppolynomials of kappa-psi-polynomials of
3532
opposite degree. Returns the tuple (B,kppolynomials), where the latter are
3533
class:`TautologicalClass`.
3534
3535
TESTS::
3536
3537
sage: from admcycles.admcycles import kpintersection_matrix
3538
sage: kpintersection_matrix(0,5,1)[0]
3539
[5 3 3 3 3]
3540
[3 1 2 2 2]
3541
[3 2 1 2 2]
3542
[3 2 2 1 2]
3543
[3 2 2 2 1]
3544
[3 2 2 2 2]
3545
sage: kpintersection_matrix(0,5,2)[0]
3546
[1]
3547
"""
3548
from .tautological_ring import TautologicalRing
3549
genindices = generating_indices(g, n, d) # indices of generators corresponding to columns of A
3550
tg = TautologicalRing(g, n).generators(d)
3551
gens = [tg[i] for i in genindices]
3552
3553
kppolynomials = list(kappapsipolys(g, n, 3 * g - 3 + n - d))
3554
3555
B = [[(gen * s).evaluate() for gen in gens] for s in kppolynomials]
3556
return (matrix(B), kppolynomials)
3557
3558
3559
class prodtautclass:
3560
r"""
3561
A tautological class on the product of several spaces \bar M_{g_i, n_i},
3562
which correspond to the vertices of a stable graph.
3563
3564
One way to construct such a tautological class is to pull back an other
3565
tautological class under a boundary gluing map.
3566
3567
Internally, the product class is stored as a list ``self.terms``, where
3568
each entry is of the form ``[ds(0),ds(1), ..., ds(m)]``, where ``ds(j)``
3569
are decstratums.
3570
3571
Be careful that the marking-names on the ``ds(j)`` are 1,2,3, ...
3572
corresponding to legs 0,1,2, ... in gamma.legs(j).
3573
If argument prodtaut is given, it is a list (of length gamma.num_verts())
3574
of tautclasses with correct leg names and this should produce the
3575
prodtautclass given as pushforward of the product of these classes that is,
3576
``self.terms`` is the list of all possible choices of a decstratum from the
3577
various factors.
3578
"""
3579
3580
def __init__(self, gamma, terms=None, protaut=None):
3581
self.gamma = gamma.copy(mutable=False)
3582
if terms is not None:
3583
self.terms = terms
3584
elif protaut is not None:
3585
declists = []
3586
for t in protaut:
3587
try:
3588
declists.append(t.terms)
3589
except AttributeError:
3590
declists.append(t._terms.values())
3591
self.terms = [[ds.copy() for ds in c] for c in itertools.product(*declists)]
3592
else:
3593
self.terms = [[decstratum(StableGraph([self.gamma.genera(i)], [list(
3594
range(1, self.gamma.num_legs(i) + 1))], [])) for i in range(self.gamma.num_verts())]]
3595
3596
def copy(self):
3597
ans = prodtautclass.__new__(prodtautclass)
3598
ans.gamma = self.gamma
3599
ans.terms = [[ds.copy() for ds in t] for t in self.terms]
3600
return ans
3601
3602
def _tautological_ring(self):
3603
r"""
3604
Return the target tautological ring of the gluing map pushforward.
3605
3606
EXAMPLES::
3607
3608
sage: from admcycles.admcycles import prodtautclass, StableGraph
3609
sage: H = StableGraph([2],[[1,2,4]],[])
3610
sage: b = prodtautclass(H)
3611
sage: b._tautological_ring()
3612
TautologicalRing(g=2, n=(1, 2, 4), moduli='st') over Rational Field
3613
"""
3614
from .tautological_ring import TautologicalRing
3615
return TautologicalRing(self.gamma.g(), self.gamma.list_markings())
3616
3617
def factors(self):
3618
r"""
3619
Return the list of factors of the tensor product.
3620
"""
3621
from .tautological_ring import TautologicalRing
3622
return [TautologicalRing(self.gamma.genera(v), self.gamma.num_legs(v)) for v in range(self.gamma.num_verts())]
3623
3624
# returns the pullback of self under the forgetful map which forgot the markings in the list newmark
3625
# rename: if True, checks if newmark contains leg names already taken for edges
3626
# def forgetful_pullback(self,newmark,rename=True):
3627
# return sum([c.forgetful_pullback(newmark,rename) for c in self.terms])
3628
3629
def pushforward(self, R=None):
3630
r"""
3631
Return the tautological class obtained by pushing forward under the gluing
3632
map associated to gamma.
3633
3634
EXAMPLES::
3635
3636
sage: from admcycles.admcycles import prodtautclass, StableGraph, psiclass, fundclass
3637
sage: G = StableGraph([0, 2], [[1, 2, 4, 3], [5]], [(3, 5)])
3638
sage: b = prodtautclass(G, protaut = [psiclass(4,0,4), fundclass(2,1)])
3639
sage: b.pushforward()
3640
Graph : [0, 2] [[1, 2, 4, 3], [5]] [(3, 5)]
3641
Polynomial : psi_3
3642
"""
3643
if R is None:
3644
R = self._tautological_ring()
3645
result = R.zero()
3646
for t in self.terms:
3647
# we have to combine the graphs from all terms such that there are no collisions of half-edge names
3648
# * genera will be the concatenation of all t[i].gamma.genera
3649
# * legs will be the concatenation of renamed versions of the t[i].gamma.legs
3650
# * edges will be the union of the renamed edge-lists from the elements t[i] and the edge-list of self.gamma
3651
maxleg = max(self.gamma.leglist() + [0]) + 1
3652
genera = []
3653
legs = []
3654
# TODO: to be changed (direct access to "hidden" attributes)
3655
edges = self.gamma.edges()
3656
numvert = sum(s.gamma.num_verts() for s in t) # total number of vertices in new graph
3657
poly = onekppoly(numvert)
3658
3659
for i in range(len(t)):
3660
# s is a decstratum
3661
s = t[i]
3662
gr = s.gamma
3663
start = len(genera)
3664
genera.extend(gr.genera())
3665
3666
# create the renaming-dictionary
3667
# TODO: to be changed (direct access to _legs and _edges attribute)
3668
renamedic = {j + 1: self.gamma._legs[i][j] for j in range(len(self.gamma._legs[i]))}
3669
for (e0, e1) in gr._edges:
3670
renamedic[e0] = maxleg
3671
renamedic[e1] = maxleg + 1
3672
maxleg += 2
3673
3674
legs += [[renamedic[l] for l in ll] for ll in gr._legs]
3675
edges += [(renamedic[e0], renamedic[e1]) for (e0, e1) in gr._edges]
3676
3677
grpoly = s.poly.copy()
3678
grpoly.rename_legs(renamedic)
3679
grpoly.expand_vertices(start, numvert)
3680
poly *= grpoly
3681
result += R(StableGraph(genera, legs, edges), poly=poly)
3682
return result
3683
3684
def partial_pushforward(self, gamma0, dicv, dicl):
3685
r"""
3686
Given a stgraph gamma0 and (dicv,dicl) a morphism from self.gamma to gamma0 (i.e.
3687
self.gamma is a specialization of gamma0), it returns the prodtautclass on gamma0
3688
obtained by the partial pushforward along corresponding gluing maps.
3689
3690
Notice that in this version, one needs to 'unedge' the edges of gamma0 and also
3691
the corresponding edges in self.gamma; otherwise the method rename_legs will not
3692
function.
3693
3694
INPUT:
3695
3696
- gamma0 (StableGraph): the graph obtained by some contraction of edges
3697
of self.gamma
3698
- dicv (dict): a dictionary describing the map of vertice of self.gamma
3699
to vertices gamma0
3700
- dicl (dict): a dictionary describing the map of legs of gamma0 to
3701
self.gamma
3702
3703
EXAMPLES::
3704
3705
sage: from admcycles.admcycles import StableGraph, prodtautclass, decstratum
3706
sage: G = StableGraph([2,0],[[2,3,4],[1,5,6]],[(3,5)])
3707
sage: G1 = StableGraph([1,1],[[2,10],[1,3,11]],[(10,11)])
3708
sage: G2 = StableGraph([0],[[1,2,3]],[])
3709
sage: d1 = decstratum(G1, [[], [0,1]], {1: 1})
3710
sage: d2 = decstratum(G2)
3711
sage: E = prodtautclass(G, [[d1, d2]])
3712
sage: G0 = StableGraph([2],[[1,2,3,4]],[])
3713
sage: E.partial_pushforward(G0, {0:0,1:0}, {3: 6, 4: 4, 2: 2, 1: 1})
3714
Outer graph : [2] [[1, 2, 3, 4]] []
3715
Vertex 0 :
3716
Graph : [1, 1, 0] [[9, 12], [2, 4, 13], [1, 11, 3]] [(9, 11), (12, 13)]
3717
Polynomial : (kappa_2)_1*psi_2
3718
3719
3720
3721
"""
3722
gamma = self.gamma
3723
preimv = [[] for v in gamma0.genera(copy=False)]
3724
for w in dicv:
3725
preimv[dicv[w]].append(w)
3726
usedlegs = dicl.values()
3727
3728
# now create list of preimage-graphs, where leg-names and orders are preserved so that decstratums can be used unchanged
3729
preimgr = []
3730
for v in range(gamma0.num_verts()):
3731
preimgr.append(stgraph([gamma.genera(w) for w in preimv[v]], [gamma.legs(w) for w in preimv[v]], [e for e in gamma.edges(copy=False) if (
3732
gamma.vertex(e[0]) in preimv[v] and gamma.vertex(e[1]) in preimv[v]) and not (e[0] in usedlegs or e[1] in usedlegs)], mutable=True))
3733
3734
# now go through self.terms, split decstrata onto preimgr (creating temporary prodtautclasses)
3735
# push those forward obtaining tautclasses on the moduli spaces corresponding to the vertices of gamma0 (but wrong leg labels)
3736
# rename them according to the inverse of dicl, then multiply all factors together using distributivity
3737
result = prodtautclass(gamma0, [])
3738
rndics = [{dicl[gamma0.legs(v, copy=False)[j]]:j + 1 for j in range(gamma0.num_legs(v))}
3739
for v in range(gamma0.num_verts())]
3740
3741
for v in range(gamma0.num_verts()):
3742
preimgr[v].rename_legs(rndics[v], shift=preimgr[v]._maxleg, tidyup=False)
3743
3744
for t in self.terms:
3745
tempclasses = [prodtautclass(preimgr[v], [[t[i] for i in preimv[v]]]) for v in range(gamma0.num_verts())]
3746
temppush = [s.pushforward() for s in tempclasses]
3747
# for v in range(gamma0.num_verts()):
3748
# temppush[v].rename_legs(rndics[v])
3749
for comb in itertools.product(*[tp._terms.values() for tp in temppush]):
3750
result.terms.append(list(comb))
3751
return result
3752
3753
# converts self into a prodHclass on gamma0=self.gamma, all spaces being distinct with trivial Hurwitz datum (for the trivial group) and not having any identifications among themselves
3754
def toprodHclass(self):
3755
gamma0 = self.gamma.copy(mutable=False)
3756
trivgp = PermutationGroup([()])
3757
trivgpel = trivgp[0]
3758
result = prodHclass(gamma0, [])
3759
3760
for t in self.terms:
3761
# we have to combine the graphs from all terms such that there are no collisions of half-edge names
3762
# * genera will be the concatenation of all t[i].gamma.genera
3763
# * legs will be the concatenation of renamed versions of the t[i].gamma.legs
3764
# * edges will be the union of the renamed edge-lists from the elements t[i] and the edge-list of self.gamma
3765
dicv0 = {}
3766
dicl0 = {l: l for l in self.gamma.leglist()}
3767
spaces = {}
3768
vertdata = []
3769
3770
maxleg = max(self.gamma.leglist() + [0]) + 1
3771
genera = []
3772
legs = []
3773
edges = self.gamma.edges()
3774
numvert = sum(s.gamma.num_verts() for s in t) # total number of vertices in new graph
3775
poly = onekppoly(numvert)
3776
3777
for i in range(len(t)):
3778
# s is a decstratum
3779
s = t[i]
3780
gr = s.gamma
3781
start = len(genera)
3782
genera += gr.genera(copy=False)
3783
dicv0.update({j: i for j in range(start, len(genera))})
3784
3785
# create the renaming-dictionary
3786
renamedic = {j + 1: self.gamma.legs(i, copy=False)[j] for j in range(self.gamma.num_legs(i))}
3787
for (e0, e1) in gr.edges():
3788
renamedic[e0] = maxleg
3789
renamedic[e1] = maxleg + 1
3790
maxleg += 2
3791
3792
legs += [[renamedic[l] for l in ll] for ll in gr._legs]
3793
edges += [(renamedic[e0], renamedic[e1]) for (e0, e1) in gr._edges]
3794
spaces.update({j: [genera[j], HurwitzData(trivgp, [(trivgpel, 1, 0)
3795
for counter in range(len(legs[j]))])] for j in range(start, len(genera))})
3796
vertdata += [[j, {legs[j][lcount]:lcount +
3797
1 for lcount in range(len(legs[j]))}] for j in range(start, len(genera))]
3798
3799
grpoly = s.poly.copy()
3800
grpoly.rename_legs(renamedic)
3801
grpoly.expand_vertices(start, numvert)
3802
poly *= grpoly
3803
3804
gamma = stgraph(genera, legs, edges)
3805
3806
result.terms.append(decHstratum(gamma, spaces, vertdata, dicv0, dicl0, poly))
3807
return result
3808
3809
# takes a set of vertices of self.gamma and a prodtautclass prodcl on a graph with the same number of vertices as the length of this list - returns the multiplication of self with a pullback (under a suitable projection) of prodcl
3810
# more precisely: let vertices=[v_1, ..., v_r], then for j=1,..,r the vertex v_j in self.gamma has same genus and number of legs as vertex j in prodcl.gamma
3811
# multiply the pullback of these classes accordingly
3812
def factor_pullback(self, vertices, prodcl):
3813
other = prodtautclass(self.gamma) # initialize with 1-class
3814
defaultterms = [other.terms[0][i]
3815
for i in range(self.gamma.num_verts())] # collect 1-terms on various factors here
3816
other.terms = []
3817
for t in prodcl.terms:
3818
newterm = [ds.copy() for ds in defaultterms]
3819
for j in range(len(vertices)):
3820
newterm[vertices[j]] = t[j]
3821
other.terms.append(newterm)
3822
return self * other
3823
3824
def __neg__(self):
3825
return (-1) * self
3826
3827
def __add__(self, other):
3828
if other == 0:
3829
return self.copy()
3830
new = self.copy()
3831
new.terms += [[ds.copy() for ds in t] for t in other.terms]
3832
return new.consolidate()
3833
3834
def __radd__(self, other):
3835
if other == 0:
3836
return self
3837
else:
3838
return self + other
3839
3840
def __iadd__(self, other):
3841
if other == 0:
3842
return self
3843
if not isinstance(other, prodtautclass) or other.gamma != self.gamma:
3844
raise ValueError("summing two prodtautclasses on different graphs!")
3845
self.terms += other.terms
3846
return self
3847
3848
def __mul__(self, other):
3849
return self.__rmul__(other)
3850
3851
def __imul__(self, other):
3852
if isinstance(other, prodtautclass):
3853
self.terms = (self.__rmul__(other)).terms
3854
return self
3855
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
3856
else:
3857
for t in self.terms:
3858
t[0] *= other
3859
return self
3860
3861
def __rmul__(self, other):
3862
if isinstance(other, prodtautclass):
3863
# multiply the decstratums on each vertex together separately
3864
if other.gamma != self.gamma:
3865
raise ValueError("product of two prodtautclass on different graphs!")
3866
3867
result = prodtautclass(self.gamma, [])
3868
for T1 in self.terms:
3869
for T2 in other.terms:
3870
# go through vertices, multiply decstratums -> obtain tautclasses
3871
# in the end: must output all possible combinations of those tautclasses
3872
vertextautcl = []
3873
for v, Rfac in enumerate(self.factors()):
3874
vertextautcl.append(list(T1[v].multiply(T2[v], Rfac)._terms.values()))
3875
for choice in itertools.product(*vertextautcl):
3876
result += prodtautclass(self.gamma, [list(choice)])
3877
return result
3878
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
3879
else:
3880
new = self.copy()
3881
for t in new.terms:
3882
t[0] = other * t[0]
3883
return new
3884
3885
def dimension_filter(self):
3886
for t in self.terms:
3887
for s in t:
3888
s.dimension_filter()
3889
return self.consolidate()
3890
3891
def consolidate(self):
3892
revrange = list(range(len(self.terms)))
3893
revrange.reverse()
3894
for i in revrange:
3895
for s in self.terms[i]:
3896
if len(s.poly.monom) == 0:
3897
self.terms.pop(i)
3898
break
3899
return self
3900
3901
def __repr__(self):
3902
s = 'Outer graph : ' + repr(self.gamma) + '\n'
3903
terms = sorted(self.terms, key=lambda l: [x.gamma for x in l])
3904
for i in range(len(terms)):
3905
for j in range(len(terms[i])):
3906
s += 'Vertex ' + repr(j) + ' :\n' + repr(terms[i][j]) + '\n'
3907
s += '\n\n'
3908
return s.rstrip('\n\n')
3909
3910
def factor_reconstruct(self, m, otherfactors):
3911
r"""
3912
Assuming that the prodtautclass is a product v_0 x ... x v_(s-1) of tautclasses v_i on the
3913
vertices, this returns the mth factor v_m assuming that otherfactors=[v_1, ..., v_(m-1), v_(m+1), ... v_(s-1)]
3914
is the list of factors on the vertices not equal to m.
3915
3916
EXAMPLES::
3917
3918
sage: from admcycles.admcycles import StableGraph, prodtautclass, psiclass, kappaclass, fundclass
3919
sage: Gamma = StableGraph([1,2,1],[[1,2],[3,4],[5]],[(2,3),(4,5)])
3920
sage: pt = prodtautclass(Gamma,protaut=[psiclass(1,1,2),kappaclass(2,2,2),fundclass(1,1)])
3921
sage: res = pt.factor_reconstruct(1,[psiclass(1,1,2),fundclass(1,1)])
3922
sage: res
3923
Graph : [2] [[1, 2]] []
3924
Polynomial : (kappa_2)_0
3925
3926
NOTE::
3927
3928
This method works by computing intersection numbers in the other factors. This could be replaced
3929
with computing a basis in the tautological ring of these factors.
3930
In principle, it is also possible to reconstruct all factors v_i (up to scaling) just assuming
3931
that the prodtautclass is a pure tensor (product of pullbacks from factors).
3932
"""
3933
3934
s = self.gamma.num_verts()
3935
if not len(otherfactors) == s - 1:
3936
raise ValueError('otherfactors must have exactly one entry for each vertex not equal to m')
3937
otherindices = list(range(s))
3938
otherindices.remove(m)
3939
3940
othertestclass = {}
3941
otherintersection = []
3942
for i in range(s - 1):
3943
rv = otherfactors[i].degree_list()[0]
3944
rvcomplement = otherfactors[i].parent().socle_degree() - rv
3945
for tc in otherfactors[i].parent().generators(rvcomplement):
3946
intnum = (tc * otherfactors[i]).evaluate()
3947
if intnum != 0:
3948
othertestclass[otherindices[i]] = tc
3949
otherintersection.append(intnum)
3950
break
3951
# R = self._tautological_ring()
3952
from .tautological_ring import TautologicalRing
3953
R = TautologicalRing(self.gamma.genera(m), self.gamma.num_legs(m))
3954
result = R.sum([prod([(t[oi] * othertestclass[oi]).evaluate() for oi in otherindices]) * R([t[m]]) for t in self.terms])
3955
result.simplify()
3956
return 1 / prod(otherintersection, QQ(1)) * result
3957
3958
# returns the cohomological degree 2r part of self, where we see self as a cohomology vector in
3959
# H*(\bar M_{g1,n1} x \bar M_{g2,n2} x ... x \bar M_{gm,nm}) with m the number of vertices of self.gamma
3960
# currently only implemented for m=1 (returns vector) or m=2 (returns list of matrices of length r+1)
3961
# if vecout=True, in the case m=2 a vector is returned that is the concatenation of all row vectors of the matrices
3962
# TODO: make r optional argument?
3963
def totensorTautbasis(self, r, vecout=False):
3964
if self.gamma.num_verts() == 1:
3965
g = self.gamma.genera(0)
3966
n = self.gamma.num_legs(0)
3967
result = vector(QQ, len(generating_indices(g, n, r)))
3968
for t in self.terms:
3969
# t is a one-element list containing a decstratum
3970
result += t[0].toTautbasis(g, n, r)
3971
return result
3972
if self.gamma.num_verts() == 2:
3973
g1 = self.gamma.genera(0)
3974
n1 = self.gamma.num_legs(0)
3975
g2 = self.gamma.genera(1)
3976
n2 = self.gamma.num_legs(1)
3977
rmax1 = 3 * g1 - 3 + n1
3978
rmax2 = 3 * g2 - 3 + n2
3979
3980
rmin = max(0, r - rmax2) # the degree r1 in the first factor can only vary between rmin and rmax
3981
rmax = min(r, rmax1)
3982
3983
result = [matrix(QQ, 0, 0) for i in range(rmin)]
3984
3985
for r1 in range(rmin, rmax + 1):
3986
M = matrix(QQ, len(generating_indices(g1, n1, r1)), len(generating_indices(g2, n2, r - r1)))
3987
for t in self.terms:
3988
# t is a list [d1,d2] of two decstratums
3989
vec1 = t[0].toTautbasis(g1, n1, r1)
3990
vec2 = t[1].toTautbasis(g2, n2, r - r1)
3991
M += matrix(vec1).transpose() * matrix(vec2)
3992
result.append(M)
3993
3994
result += [matrix(QQ, 0, 0) for i in range(rmax + 1, r + 1)]
3995
3996
if vecout:
3997
return vector([M[i, j] for M in result for i in range(M.nrows()) for j in range(M.ncols())])
3998
else:
3999
return result
4000
4001
if self.gamma.num_verts() > 2:
4002
print('totensorTautbasis not yet implemented on graphs with more than two vertices')
4003
return 0
4004
4005
# converts a decstratum or tautclass to a vector in Pixton's basis
4006
# tries to reconstruct g,n,r from first nonzero term on the first decstratum
4007
# if they are explicitly given, only extract the part of D that lies in right degree, ignore others
4008
4009
4010
def converttoTautvect(D, g=None, n=None, r=None, moduli='st'):
4011
# global copyD
4012
# copyD=(D,g,n,r)
4013
if isinstance(D, decstratum):
4014
D.dimension_filter()
4015
if g is None:
4016
g = D.gamma.g()
4017
if n is None:
4018
n = len(D.gamma.list_markings())
4019
if len(D.poly.monom) == 0:
4020
if (r is None):
4021
print('Unable to identify r for empty decstratum')
4022
return 0
4023
else:
4024
return vector(QQ, DR.num_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True)))
4025
if r is None:
4026
polydeg = D.poly.deg()
4027
if polydeg is not None:
4028
r = D.gamma.num_edges() + polydeg
4029
# just assume now that g,n,r are given
4030
length = DR.num_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True))
4031
markings = tuple(range(1, n + 1))
4032
result = vector(QQ, length)
4033
graphdegree = D.gamma.num_edges()
4034
for (kappa, psi, coeff) in D.poly:
4035
if graphdegree + sum([sum((j + 1) * kvec[j] for j in range(len(kvec))) for kvec in kappa]) + sum(psi.values()) == r:
4036
try:
4037
pare = coeff.parent()
4038
except AttributeError:
4039
pare = QQ
4040
result += vector(pare, length, {DR.num_of_stratum(Pixtongraph(D.gamma, kappa, psi),
4041
g, r, markings, moduli_type=get_moduli(moduli, DRpy=True)): coeff})
4042
return result
4043
4044
4045
def Pixtongraph(G, kappa, psi):
4046
r"""
4047
Return a Pixton-style graph given a stgraph ``G`` and data ``kappa`` and
4048
``psi`` as in a :class:`KappaPsiPolynomial`.
4049
4050
The function :func:`Graphtodecstratum` provides the conversion in the other direction.
4051
4052
EXAMPLES::
4053
4054
sage: from admcycles import StableGraph
4055
sage: from admcycles.admcycles import Pixtongraph, Graphtodecstratum
4056
sage: st = StableGraph([0, 2], [[1, 2, 4], [3, 5]], [(4, 5)])
4057
sage: G = Pixtongraph(st, [[], [0, 1]], {1: 1})
4058
sage: G
4059
[ -1 1 2 3 0]
4060
[ 0 X + 1 1 0 1]
4061
[X^2 + 2 0 0 1 1]
4062
sage: ds = Graphtodecstratum(G)
4063
sage: assert ds.gamma.is_isomorphic(st)
4064
"""
4065
firstcol = [-1]
4066
for v in range(G.num_verts()):
4067
entry = 0 * DR.X + G.genera(v)
4068
for k in range(len(kappa[v])):
4069
entry += kappa[v][k] * (DR.X)**(k + 1)
4070
firstcol.append(entry)
4071
columns = [firstcol]
4072
4073
for l in G.list_markings():
4074
vert = G.vertex(l)
4075
entry = 0 * DR.X + 1
4076
if l in psi:
4077
entry += psi[l] * DR.X
4078
columns.append([l] + [0 for i in range(vert)] + [entry] + [0 for i in range(G.num_verts() - vert - 1)])
4079
for (e0, e1) in G.edges(copy=False):
4080
v0 = G.vertex(e0)
4081
v1 = G.vertex(e1)
4082
if v0 == v1:
4083
entry = 0 * DR.X + 2
4084
if (psi.get(e0, 0) != 0) and (psi.get(e1, 0) == 0):
4085
entry += psi[e0] * DR.X
4086
if (psi.get(e1, 0) != 0) and (psi.get(e0, 0) == 0):
4087
entry += psi[e1] * DR.X
4088
if (psi.get(e0, 0) != 0) and (psi.get(e1, 0) != 0):
4089
if psi[e0] >= psi[e1]:
4090
entry += psi[e0] * DR.X + psi[e1] * (DR.X)**2
4091
else:
4092
entry += psi[e1] * DR.X + psi[e0] * (DR.X)**2
4093
columns.append([0] + [0 for i in range(v0)] + [entry] + [0 for i in range(G.num_verts() - v0 - 1)])
4094
else:
4095
col = [0 for i in range(G.num_verts() + 1)]
4096
entry = 0 * DR.X + 1
4097
if e0 in psi:
4098
entry += psi[e0] * DR.X
4099
col[v0 + 1] = entry
4100
4101
entry = 0 * DR.X + 1
4102
if e1 in psi:
4103
entry += psi[e1] * DR.X
4104
col[v1 + 1] = entry
4105
columns.append(col)
4106
4107
M = matrix(columns).transpose()
4108
return DR.Graph(M)
4109
4110
# converts vector v in generating set all_strata(g,r,(1,..,n)) to preferred basis computed by generating_indices(g,n,r)
4111
4112
4113
def Tautvecttobasis(v, g, n, r, moduli='st'):
4114
if (2 * g - 2 + n <= 0) or (r < 0) or (r > socle_degree(g, n, get_moduli(moduli))):
4115
return vector([])
4116
if moduli == 'tl':
4117
res = Tautvecttobasis(v, g, n, r, 'st') # create basis vector on stable locus
4118
W = op_subset_space(g, n, r, moduli)
4119
if not res:
4120
return W.zero()
4121
reslen = len(res)
4122
return sum(res[i] * W(vector(QQ, reslen, {i: 1})) for i in range(reslen) if not res[i].is_zero())
4123
vecs = genstobasis(g, n, r, moduli=moduli)
4124
assert len(vecs) == len(v), (vecs, v)
4125
res = vector(QQ, len(vecs[0]))
4126
for i in range(len(v)):
4127
if v[i] != 0:
4128
res += v[i] * vecs[i]
4129
return res
4130
4131
4132
# gives a list whose ith entry is the representation of the ith generator in all_strata(g,r,(1,..,n)) in the preferred basis computed by generating_indices(g,n,r)
4133
4134
@file_cache.file_cached_function(
4135
directory=os.path.join(DOT_SAGE, "admcycles"),
4136
url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",
4137
remote_database_list="https://modulispaces.gitlab.io/relations-database/genstobasis_files",
4138
env_var="ADMCYCLES_CACHE_DIR",
4139
pickle_wrappers=(file_cache.rational_vectors_to_py, file_cache.py_to_rational_vectors))
4140
def genstobasis(g, n, r, moduli='st'):
4141
r"""
4142
Returns the list of vectors expressing the generators of `R^r(\bar M_{g,n})` in terms of the
4143
standard basis.
4144
4145
INPUT:
4146
4147
moduli : string (default: 'st')
4148
If instead of 'st' one of 'ct', 'rt' or 'sm' is given, compute a basis expression for the
4149
tautological ring of the corresponding open subset of `\bar M_{g,n}`.
4150
4151
EXAMPLES::
4152
4153
sage: from admcycles import TautologicalRing
4154
sage: from admcycles.admcycles import genstobasis
4155
sage: genstobasis(1, 1, 1)
4156
[(1), (1), (24)]
4157
sage: [t.evaluate() for t in TautologicalRing(1,1).generators(1)]
4158
[1/24, 1/24, 1]
4159
sage: genstobasis(2, 1, 1, moduli='ct')
4160
[(1, 0), (0, 1), (5/7, -5/7)]
4161
sage: genstobasis(9, 0, 3, moduli='sm')
4162
[(1, 0, 0), (0, 1, 0), (0, 0, 1)]
4163
4164
TESTS:
4165
4166
Test the oneline database::
4167
4168
sage: from admcycles import generating_indices
4169
sage: from tempfile import mkdtemp
4170
sage: from shutil import rmtree
4171
sage: import os
4172
sage: import pickle
4173
sage: genstobasis.set_online_lookup(True)
4174
sage: filename = "genstobasis_0_4_0_st.pkl"
4175
sage: tmpdir = mkdtemp()
4176
sage: filename_with_path = os.path.join(tmpdir, filename)
4177
sage: genstobasis._FileCachedFunction__download(filename, filename_with_path) # optional - internet
4178
sage: f = open(filename_with_path, "rb") # optional - internet
4179
sage: pickle.load(f) # optional - internet
4180
[(1)]
4181
sage: rmtree(tmpdir)
4182
"""
4183
# We need to work around a caching issue:
4184
# If generating_indices is in the cache, the call to generatig_indices
4185
# will return the cached value without actually running the function,
4186
# in particular without setting the cache for genstobasis.
4187
# If at the same time genstobasis is not in the cache
4188
# (which is the case if this function is actually executed), we have
4189
# created an infinite loop.
4190
# In this case we must force generating_indices to be executed.
4191
if (g, n, r, moduli) in generating_indices.cache:
4192
generating_indices.f(g, n, r, True, moduli=moduli)
4193
else:
4194
generating_indices(g, n, r, True, moduli=moduli)
4195
return genstobasis(g, n, r, moduli=moduli)
4196
4197
4198
def Tautv_to_tautclass(v, g, n, r, moduli='st'):
4199
r"""
4200
Deprecated. Use :func:`admcycles.tautological_ring.TautologicalRing.from_vector` instead.
4201
"""
4202
from .superseded import deprecation
4203
deprecation(109, 'Tautv_to_tautclass is deprecated. Please use the from_vector method from TautologicalRing instead.')
4204
from .tautological_ring import TautologicalRing
4205
return TautologicalRing(g, n, moduli).from_vector(v, r)
4206
4207
4208
def Tautvb_to_tautclass(v, g, n, r):
4209
r"""
4210
Deprecated. Use :func:`admcycles.tautological_ring.TautologicalRing.from_basis_vector` instead.
4211
"""
4212
from .superseded import deprecation
4213
deprecation(109, 'Tautvb_to_tautclass is deprecated. Please use the from_vector method from TautologicalRing instead.')
4214
from .tautological_ring import TautologicalRing
4215
return TautologicalRing(g, n).from_basis_vector(v, r)
4216
4217
4218
class prodHclass:
4219
r"""
4220
A sum of gluing pushforwards of pushforwards of fundamental classes of
4221
Hurwitz spaces under products of forgetful morphisms.
4222
4223
This is all relative to a fixed stable graph gamma0.
4224
"""
4225
4226
def __init__(self, gamma0, terms):
4227
self.gamma0 = gamma0.copy(mutable=False)
4228
self.terms = terms
4229
4230
def __neg__(self):
4231
return (-1) * self
4232
4233
def __add__(self, other):
4234
if other == 0:
4235
return deepcopy(self)
4236
if isinstance(other, prodHclass) and other.gamma0 == self.gamma0:
4237
new = deepcopy(self)
4238
new.terms += deepcopy(other.terms)
4239
return new.consolidate()
4240
4241
def __radd__(self, other):
4242
if other == 0:
4243
return self
4244
else:
4245
return self + other
4246
4247
def __iadd__(self, other):
4248
if other == 0:
4249
return self
4250
if isinstance(other, prodHclass) and other.gamma0 == self.gamma0:
4251
self.terms += other.terms # TODO: should we use a deepcopy here?
4252
else:
4253
raise ValueError("sum of prodHclasses on different graphs")
4254
return self
4255
4256
def __mul__(self, other):
4257
from .tautological_ring import TautologicalClass
4258
if isinstance(other, (TautologicalClass, decstratum)):
4259
return self.__rmul__(other)
4260
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
4261
else:
4262
new = deepcopy(self)
4263
for t in new.terms:
4264
t[0] = other * t[0]
4265
return new
4266
4267
def __rmul__(self, other):
4268
from .tautological_ring import TautologicalClass
4269
if isinstance(other, (TautologicalClass, decstratum)):
4270
pbother = self.gamma0.boundary_pullback(other) # this is a prodtautclass on self.gamma0 now
4271
# this is a prodHclass with gamma0 = self.gamma0 now, but we know that all spaces are \bar M_{g_i,n_i}, which we use below!
4272
pbother = pbother.toprodHclass()
4273
result = prodHclass(deepcopy(self.gamma0), [])
4274
for t in pbother.terms:
4275
# t is a decHstratum and (t.dicv0,t.dicl0) is a morphism from t.gamma to self.gamma0
4276
# we pull back self along this morphism to get a prodHclass on t.gamma
4277
temppullback = self.gamma0_pullback(t.gamma, t.dicv0, t.dicl0)
4278
4279
# now it remains to distribute the kappa and psi-classes from t.poly (living on t.gamma) to the terms of temppullback
4280
for s in temppullback.terms:
4281
# s is now a decHstratum and (s.dicv0,s.dicl0) describes a map s.gamma -> t.gamma
4282
s.poly *= t.poly.graphpullback(s.dicv0, s.dicl0)
4283
# rearrange s to be again a decHstratum with respect to gamma0
4284
s.dicv0 = {v: t.dicv0[s.dicv0[v]] for v in s.dicv0}
4285
s.dicl0 = {l: s.dicl0[t.dicl0[l]] for l in t.dicl0}
4286
result.terms += temppullback.terms
4287
return result
4288
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
4289
else:
4290
new = deepcopy(self)
4291
for t in new.terms:
4292
t[0] = other * t[0]
4293
return new
4294
4295
# evaluates self against the fundamental class of the ambient space (of self.gamma0), returns a rational number
4296
4297
def evaluate(self):
4298
return sum([t.evaluate() for t in self.terms])
4299
4300
# tries to convert self into a prodtautclass on the graph gamma0
4301
# Note: since not all Hurwitz spaces have tautological fundamental class and since not all diagonals have tautological Kunneth decompositions, this will not always work
4302
4303
def toprodtautclass(self):
4304
result = prodtautclass(self.gamma0, [])
4305
for t in self.terms:
4306
# create a prodtautclass on t.gamma, then do partial pushforward under (t.dicv0,t.dicl0)
4307
tempres = decstratum(deepcopy(t.gamma), poly=deepcopy(t.poly))
4308
tempres = tempres.convert_to_prodtautclass()
4309
4310
# now group vertices of t.gamma according to the spaces they access
4311
# create diagonals and insert the identification of the Hurwitz stack fundamental class
4312
# make sure that for this you use the version forgetting the most markings possible
4313
spacelist = {a: [] for a in t.spaces}
4314
for v in range(t.gamma.num_verts()):
4315
spacelist[t.vertdata[v][0]].append(v)
4316
for a in spacelist:
4317
if not spacelist[a]:
4318
spacelist.pop(a)
4319
4320
for a in spacelist:
4321
# find out which of the markings 1, .., N of the space containing the Hurwitz cycle are used
4322
usedmarks = set()
4323
for v in spacelist[a]:
4324
usedmarks.update(set(t.vertdata[v][1].values()))
4325
usedmarks = sorted(usedmarks)
4326
rndic = {usedmarks[i]: i + 1 for i in range(len(usedmarks))}
4327
4328
Hclass = Hidentify(t.spaces[a][0], t.spaces[a][1], markings=usedmarks)
4329
Hclass = Hclass.rename_legs(rndic, inplace=False)
4330
4331
legdics = [{l: rndic[t.vertdata[v][1][l]] for l in t.gamma.legs(v, copy=False)} for v in spacelist[a]]
4332
diagclass = forgetful_diagonal(t.spaces[a][0], len(
4333
usedmarks), [t.gamma.legs(v, copy=False) for v in spacelist[a]], legdics, T=Hclass)
4334
4335
tempres = tempres.factor_pullback(spacelist[a], diagclass)
4336
4337
tempres = tempres.partial_pushforward(self.gamma0, t.dicv0, t.dicl0)
4338
result += tempres
4339
return result
4340
4341
# given a different graph gamma1 carrying a gamma0-structure encoded by dicv, dicl, pull back self under this structure and return a prodHclass with underlying graph gamma1
4342
def gamma0_pullback(self, gamma1, dicv=None, dicl=None):
4343
gamma0 = self.gamma0
4344
4345
if dicv is None:
4346
if gamma0.num_verts() == 1:
4347
dicv = {v: 0 for v in range(gamma1.num_verts())}
4348
else:
4349
raise RuntimeError('dicv not uniquely determined')
4350
if dicl is None:
4351
if gamma0.num_edges() == 0:
4352
dicl = {l: l for l in gamma0.leglist()}
4353
else:
4354
raise RuntimeError('dicl not uniquely determined')
4355
4356
# Step 1: factor the map gamma1 -> gamma0 in a series of 1-edge degenerations
4357
# TODO: choose a smart order to minimize computations later (i.e. start w/ separating edges with roughly equal genus on both sides
4358
4359
imdicl = dicl.values()
4360
extraedges = [e for e in gamma1.edges(copy=False) if e[0] not in imdicl]
4361
delta_e = gamma1.num_edges() - gamma0.num_edges()
4362
if delta_e != len(extraedges):
4363
print('Warning: edge numbers')
4364
global exampullb
4365
exampullb = (deepcopy(self), deepcopy(gamma1), deepcopy(dicv), deepcopy(dicl))
4366
raise ValueError('Edge numbers')
4367
edgeorder = list(range(delta_e))
4368
contredges = [extraedges[i] for i in edgeorder]
4369
4370
contrgraph = [gamma1]
4371
actvert = []
4372
edgegraphs = []
4373
vnumbers = []
4374
contrdicts = []
4375
vimages = [dicv[v] for v in range(gamma1.num_verts())]
4376
count = 0
4377
for e in contredges:
4378
gammatild = contrgraph[count].copy()
4379
(av, edgegraph, vnum, diccv) = gammatild.contract_edge(e, adddata=True)
4380
gammatild.set_immutable()
4381
contrgraph.append(gammatild)
4382
actvert.append(av)
4383
edgegraphs.append(edgegraph)
4384
vnumbers.append(vnum)
4385
contrdicts.append(diccv)
4386
if len(vnum) == 2:
4387
vimages.pop(vnum[1])
4388
count += 1
4389
contrgraph.reverse()
4390
actvert.reverse()
4391
edgegraphs.reverse()
4392
vnumbers.reverse()
4393
contrdicts.reverse()
4394
4395
# Step 2: pull back the decHstratum t one edge-degeneration at a time
4396
# Note that this will convert the decHstratum into a prodHclass, so we need to take care of all the different terms in each step
4397
4398
# First we need to adapt (a copy of) self so that we take into account that gamma0 is isomorphic to the contracted version of gamma1
4399
# The gamma0-structure on all terms of self must be modified to be a contrgraph[0]-structure
4400
result = deepcopy(self)
4401
result.gamma0 = contrgraph[0]
4402
# gives isomorphism V(gamma0) -> V(contrgraph[0])
4403
dicvisom = {dicv[vimages[v]]: v for v in range(gamma0.num_verts())}
4404
diclisom = {dicl[l]: l for l in dicl} # gives isomorphism L(contrgraph[0]) -> L(gamma0)
4405
for t in result.terms:
4406
t.dicv0 = {v: dicvisom[t.dicv0[v]] for v in t.dicv0}
4407
t.dicl0 = {l: t.dicl0[diclisom[l]] for l in diclisom}
4408
# dicv, dicl should have served their purpose and should no longer appear below
4409
4410
for edgenumber in range(delta_e):
4411
newresult = prodHclass(contrgraph[edgenumber + 1], [])
4412
gam0 = contrgraph[edgenumber] # current gamma0, we now pull back under contrgraph[edgenumber+1] -> gam0
4413
av = actvert[edgenumber] # vertex in gam0 where action happens
4414
diccv = contrdicts[edgenumber] # vertex dictionary for contrgraph[edgenumber+1] -> gam0
4415
# section of diccv: bijective if loop is added, otherwise assigns one of the preimages to the active vertex
4416
diccvinv = {diccv[v]: v for v in diccv}
4417
vextractdic = {v: vnumbers[edgenumber][v] for v in range(len(vnumbers[edgenumber]))}
4418
4419
for t in result.terms:
4420
gamma = t.gamma
4421
4422
# Step 2.1: extract the preimage of the active vertex inside t.gamma
4423
actvertices = [v for v in range(gamma.num_verts()) if t.dicv0[v] == av]
4424
# global exsubvar
4425
# exsubvar=(t,gamma,actvertices,gam0, av)
4426
# avloops=[e for e in gam0.edges if (e[0] in gam0.legs(av, copy=False) and e[1] in gam0.legs(av, copy=False))]
4427
# outlegs=set(gam0.legs(av, copy=False))-set([e[0] for e in avloops] + [e[1] for e in avloops])
4428
(gammaprime, dicvextract, diclextract) = gamma.extract_subgraph(actvertices,
4429
outgoing_legs=[t.dicl0[l] for l in gam0.legs(av, copy=False)], rename=False)
4430
4431
dicvextractinv = {dicvextract[v]: v for v in dicvextract}
4432
# dicvextract maps vertex numbers in gamma to vertex-numbers in gammaprime
4433
# diclextract not needed since rename=False
4434
4435
# Step 2.2: find the common degenerations of this preimage and the one-edge graph relevant for this step
4436
# We need to rename edgegraphs[edgenumber] to match the leg-names in gammaprime
4437
egraph = edgegraphs[edgenumber].copy()
4438
eshift = max(list(t.dicl0.values()) + [0]) + 1
4439
egraph.rename_legs(t.dicl0, shift=eshift)
4440
egraph.set_immutable()
4441
4442
# (gammaprime,dicvextract,diclextract) =gamma.extract_subgraph(actvertices,outgoing_legs=[l for l in egraph.list_markings()],rename=False)
4443
4444
# dicvextractinv={dicvextract[v]:v for v in dicvextract}
4445
# dicvextract maps vertex numbers in gamma to vertex-numbers in gammaprime
4446
# diclextract not needed since rename=False
4447
4448
commdeg = common_degenerations(gammaprime, egraph, modiso=True, rename=True)
4449
4450
(e0, e1) = egraph.edges(copy=False)[0]
4451
for (gammadeg, dv1, dl1, dv2, dl2) in commdeg:
4452
if gammadeg.num_edges() == gammaprime.num_edges():
4453
# Step 2.3a: if gammadeg isomorphic to gammaprime, we have excess intersection, introducing -psi - psi'
4454
term = deepcopy(t)
4455
numvert = gamma.num_verts()
4456
dl1inverse = {dl1[l]: l for l in dl1}
4457
term.poly *= -(psicl(dl1inverse[dl2[e0]], numvert) + psicl(dl1inverse[dl2[e1]], numvert))
4458
# adapt term.dicv0 and term.dicl0
4459
4460
# term.dicv0 must be lifted along the contraction map using diccvinv outside of the active area gammaprime; there we use dv2
4461
dv1inverse = {dv1[v]: v for v in dv1} # TODO: is this what we want?
4462
term.dicv0 = {v: diccvinv[term.dicv0[v]] for v in term.dicv0}
4463
term.dicv0.update({v: vextractdic[dv2[dv1inverse[dicvextract[v]]]] for v in actvertices})
4464
4465
# term.dicl0 should now assign to the two half-edges that are new the corresponding edge in term.gamma
4466
term.dicl0[e0 - eshift] = dl1inverse[dl2[e0]]
4467
term.dicl0[e1 - eshift] = dl1inverse[dl2[e1]]
4468
4469
newresult.terms.append(term)
4470
else:
4471
# Step 2.3b: else there is a real degeneration going on
4472
# This means we need to find the vertex of gamma where this new edge would appear,
4473
# then check if the Hurwitz space glued to this spot admits a compatible degeneration.
4474
# Note that here we must take note that we possibly forgot some markings of this Hurwitz space.
4475
4476
vactiveprime = dv1[gammadeg.vertex(dl2[e0])] # vertex in gammaprime where degeneration occurs
4477
vactive = dicvextractinv[vactiveprime] # vertex in gamma where degeneration occurs
4478
4479
# TODO: it is likely much faster to go through elements Gr of Hbdry, go through edges e of Gr, contract everything except e and check if
4480
# compatible with the boundary partition
4481
(a, spacedicl) = t.vertdata[vactive]
4482
(gsp, Hsp) = t.spaces[a]
4483
numHmarks = trivGgraph(gsp, Hsp).gamma.num_legs(0)
4484
4485
# Hbdry=list_Hstrata(gsp,Hsp,1) # these are the Gstgraphs which might get glued in at vertex vactive (+ other vertices accessing space a)
4486
# if Hbdry==[]:
4487
# continue # we would require a degeneration, but none can occur
4488
# numHmarks=len(Hbdry[0].gamma.list_markings())
4489
# presentmarks=spacedicl.values()
4490
# forgetmarks=[i for i in range(1,numHmarks) if i not in presentmarks]
4491
4492
# now create list divlist of divisors D living in the space of Hbdry such that we can start enumerating D-structures on elements of Hbdry
4493
# first we must extract the original boundary graph, which is a subgraph of gammadeg on the preimages of vactiveprime
4494
vactiveprimepreim = [gammadeg.vertex(dl2[e0]), gammadeg.vertex(dl2[e1])]
4495
if vactiveprimepreim[0] == vactiveprimepreim[1]:
4496
vactiveprimepreim.pop(1)
4497
# old, possibly faulty: (egr,dvex,dlex) =gammadeg.extract_subgraph(vactiveprimepreim,outgoing_legs=gammaprime.legs(vactiveprime),rename=False)
4498
(egr, dvex, dlex) = gammadeg.extract_subgraph(vactiveprimepreim, outgoing_legs=[
4499
dl1[le] for le in gammaprime.legs(vactiveprime, copy=False)], rename=False, mutable=True)
4500
4501
# rename egr to fit in the space of Hbdry
4502
rndic = {dl1[l]: spacedicl[l] for l in gammaprime.legs(vactiveprime, copy=False)}
4503
egr.rename_legs(rndic, shift=numHmarks + 1)
4504
egr.set_immutable()
4505
4506
degenlist = Hbdrystructures(gsp, Hsp, egr)
4507
4508
for (Gr, mult, degendicv, degendicl) in degenlist:
4509
term = deepcopy(t)
4510
e = egr.edges(copy=False)[0]
4511
speciale_im = term.replace_space_by_Gstgraph(
4512
a, Gr, specialv=vactive, speciale=(degendicl[e[0]], degendicl[e[1]]))
4513
term.poly *= mult
4514
# term is still a decHstratum with correct gamma, spaces, vertdata and poly, BUT still referencing to ga0
4515
# now repair dicv0, dicl0
4516
4517
# term.dicl0 should now assign to the two half-edges that are new the corresponding edge in term.gamma
4518
dl1inverse = {dl1[l]: l for l in dl1}
4519
eindex = e.index(dl2[e0] + numHmarks + 1)
4520
term.dicl0[e0 - eshift] = speciale_im[eindex]
4521
term.dicl0[e1 - eshift] = speciale_im[1 - eindex]
4522
4523
# term.dicv0 is now reconstructed from dicl0
4524
# TODO: add more bookkeeping to do this directly???
4525
term.dicv0 = dicv_reconstruct(term.gamma, contrgraph[edgenumber + 1], term.dicl0)
4526
4527
newresult.terms.append(term)
4528
4529
result = newresult
4530
4531
return result
4532
4533
# def dimension_filter(self):
4534
# for t in self.terms:
4535
# for s in t:
4536
# s.dimension_filter()
4537
# return self.consolidate()
4538
4539
def consolidate(self):
4540
# revrange=range(len(self.terms))
4541
# revrange.reverse()
4542
# for i in revrange:
4543
# for s in self.terms[i]:
4544
# if len(s.poly.monom)==0:
4545
# self.terms.pop(i)
4546
# break
4547
return self
4548
4549
def __repr__(self):
4550
s = 'Outer graph : ' + repr(self.gamma0) + '\n'
4551
for t in self.terms:
4552
s += repr(t) + '\n\n'
4553
# for i in range(len(self.terms)):
4554
# for j in range(len(self.terms[i])):
4555
# s+='Vertex '+repr(j)+' :\n'+repr(self.terms[i][j])+'\n'
4556
# s+='\n\n'
4557
return s.rstrip('\n\n')
4558
4559
# takes a genus g, a HurwitzData H and a stgraph bdry with exactly one edge, whose markings are a subset of the markings 1,2,..., N of trivGgraph(g,H)
4560
# returns a list with entries (Gr,mult,dicv,dicl) giving all boundary Gstgraphs derived from (g,H) which are specializations of (the forgetful pullback of) Gr, where
4561
# * Gr is a Gstgraph
4562
# * mult is a rational number giving an appropriate multiplicity for the intersection
4563
# * dicv is a surjective dictionary of the vertices of Gr.gamma to the vertices of bdry
4564
# * dicl is an injection from the legs of bdry to the legs of Gr.gamma
4565
4566
4567
def Hbdrystructures(g, H, bdry):
4568
preHbdry, N = preHbdrystructures(g, H)
4569
4570
if bdry.num_verts() == 1:
4571
try:
4572
tempresult = preHbdry[(g)]
4573
except KeyError:
4574
return []
4575
(e0, e1) = bdry.edges(copy=False)[0]
4576
result = []
4577
for (Gr, mult, dicv, dicl) in tempresult:
4578
result.append([Gr, mult, dicv, {e0: dicl[N + 1], e1:dicl[N + 2]}])
4579
return result
4580
4581
if bdry.num_verts() == 2:
4582
presentmarks = bdry.list_markings()
4583
forgetmarks = [i for i in range(1, N + 1) if i not in presentmarks]
4584
possi = [[0, 1] for j in forgetmarks]
4585
4586
(e0, e1) = bdry.edges(copy=False)[0]
4587
lgs = bdry.legs(copy=True)
4588
ve0 = bdry.vertex(e0)
4589
lgs[ve0].remove(e0) # leglists stripped of the legs forming the edge of bdry
4590
lgs[1 - ve0].remove(e1)
4591
4592
result = []
4593
4594
for distri in itertools.product(*possi):
4595
# distri = (0,0,1,0,1,1) means that forgetmarks number 0,1,3 go to erg.legs(0) and 2,4,5 go to erg.legs(1)
4596
lgscpy = deepcopy(lgs)
4597
lgscpy[0] += [forgetmarks[j] for j in range(len(forgetmarks)) if distri[j] == 0]
4598
lgscpy[1] += [forgetmarks[j] for j in range(len(forgetmarks)) if distri[j] == 1]
4599
4600
if bdry.genera(0) > bdry.genera(1) or (bdry.genera(0) == bdry.genera(1) and 1 in lgscpy[1]):
4601
vert = 1
4602
else:
4603
vert = 0
4604
ed = (ve0 + vert) % 2
4605
4606
try:
4607
tempresult = preHbdry[(bdry.genera(vert), tuple(sorted(lgscpy[vert])))]
4608
for (Gr, mult, dicv, dicl) in tempresult:
4609
result.append((Gr, mult, {j: (dicv[j] + vert) % 2 for j in dicv},
4610
{e0: dicl[N + 1 + ed], e1: dicl[N + 2 - ed]}))
4611
except KeyError:
4612
pass
4613
return result
4614
4615
# returns (result,N), where N is the number of markings in Gstgraphs with (g,H) and result is a dictionary sending some tuples (g',(m_1, ..., m_l)), which correspond to boundary divisors, to the list of all entries of the form [Gr,mult,dicv,dicl] like in the definition of Hbdrystructures. Here dicv and dicl assume that the boundary divisor bdry is of the form stgraph([g',g-g'],[[m_1, ..., m_l,N+1],[complementary legs, N+2],[(N+1,N+2)]]
4616
# Convention: we choose the key above such that g'<=g-g'. If there is equality, either there are no markings OR we ask that m_1=1. Note that m_1, ... are assumed to be ordered
4617
# For the unique boundary divisor parametrizing irreducible curves (having a self-node), we reserve the key (g)
4618
4619
4620
@cached_function
4621
def preHbdrystructures(g, H):
4622
Hbdry = list_Hstrata(g, H, 1)
4623
if len(Hbdry) == 0:
4624
return ({}, len(trivGgraph(g, H).gamma.list_markings()))
4625
N = len(Hbdry[0].gamma.list_markings())
4626
result = {}
4627
for Gr in Hbdry:
4628
AutGr = equiGraphIsom(Gr, Gr)
4629
4630
edgs = Gr.gamma.edges()
4631
4632
while edgs:
4633
econtr = edgs[0] # this edge will remain after contraction
4634
4635
# now go through AutGr and eliminate all other edges which are in orbit of econtr, record multiplicity
4636
multiplicity = 0
4637
for (dicv, dicl) in AutGr:
4638
try:
4639
edgs.remove((dicl[econtr[0]], dicl[econtr[1]]))
4640
multiplicity += 1
4641
except (KeyError, ValueError):
4642
pass
4643
4644
# now contract all except econtr and add the necessary data to the dictionary result
4645
contrGr = Gr.gamma.copy(mutable=True)
4646
diccv = {j: j for j in range(contrGr.num_verts())}
4647
for e in Gr.gamma.edges(copy=False):
4648
if e != econtr:
4649
(v0, d1, d2, diccv_new) = contrGr.contract_edge(e, True)
4650
diccv = {j: diccv_new[diccv[j]] for j in diccv}
4651
4652
# OLD: multiplicity*=len(GraphIsom(contrGr,contrGr))
4653
# NOTE: multiplicity must be multiplied by the number of automorphisms of the boundary stratum, 2 in this case
4654
# this compensates for the fact that these automorphisms give rise to different bdry-structures on Gr
4655
4656
if contrGr.num_verts() == 1:
4657
# it remains a self-loop
4658
if g not in result:
4659
result[(g)] = []
4660
result[(g)].append([Gr, QQ(multiplicity) / len(AutGr),
4661
{j: 0 for j in range(Gr.gamma.num_verts())}, {N + 1: econtr[0], N + 2: econtr[1]}])
4662
result[(g)].append([Gr, QQ(multiplicity) / len(AutGr),
4663
{j: 0 for j in range(Gr.gamma.num_verts())}, {N + 2: econtr[0], N + 1: econtr[1]}])
4664
4665
else:
4666
# we obtain a separating boundary; now we must distinguish cases to ensure that g' <= g-g' and for equality m_1=1
4667
if contrGr.genera(0) > contrGr.genera(1) or (contrGr.genera(0) == contrGr.genera(1) and 1 in contrGr.legs(1, copy=False)):
4668
switched = 1
4669
else:
4670
switched = 0
4671
4672
# eswitched=0 means econtr[0] is in the vertex with g',[m_1,..]; eswitched=1 means it is at the other vertex
4673
eswitched = (contrGr.vertex(econtr[0]) + switched) % 2
4674
4675
gprime = contrGr.genera(switched)
4676
markns = contrGr.legs(switched, copy=True)
4677
markns.remove(econtr[eswitched])
4678
markns.sort()
4679
markns = tuple(markns)
4680
4681
try:
4682
result[(gprime, markns)].append([Gr, QQ(multiplicity) / len(AutGr),
4683
{v: (diccv[v] + switched) % 2 for v in diccv}, {N + 1: econtr[eswitched], N + 2: econtr[1 - eswitched]}])
4684
except KeyError:
4685
result[(gprime, markns)] = [[Gr, QQ(multiplicity) / len(AutGr),
4686
{v: (diccv[v] + switched) % 2 for v in diccv}, {N + 1: econtr[eswitched], N + 2: econtr[1 - eswitched]}]]
4687
4688
if contrGr.genera(0) == contrGr.genera(1) and N == 0:
4689
# in this case the resulting boundary divisor has an automorphism, so we need to record also the switched version of the above bdry-structure
4690
result[(gprime, markns)].append([Gr, QQ(multiplicity) / len(AutGr),
4691
{v: (diccv[v] + 1 - switched) % 2 for v in diccv}, {N + 2: econtr[eswitched], N + 1: econtr[1 - eswitched]}])
4692
4693
return (result, N)
4694
4695
4696
# summands in prodHclass
4697
# * gamma is a stgraph on which this summand lives
4698
# * spaces is a dictionary, assigning entries of the form [g,Hdata] of a genus and HurwitzData Hdata to integer labels a - since these change a lot and are replaced frequently by multiple new labels, this data structure hopefully allows for flexibility
4699
# * vertdata gives a list of entries of the form [a, dicl] for each vertex v of gamma, where a is a label (sent to (g,Hdata) by spaces) and dicl is an injective dictionary mapping leg-names of v to the corresponding (standard) numbers of the Hurwitz space of (g,Hdata)
4700
# * poly is a kppoly on gamma
4701
# * dicv0 is a surjective dictionary mapping vertices of gamma to vertices of the including prodHclass
4702
# * dicl0 is an injective dictionary mapping legs of gamma0 to legs of gamma
4703
class decHstratum:
4704
def __init__(self, gamma, spaces, vertdata, dicv0=None, dicl0=None, poly=None):
4705
self.gamma = gamma
4706
self.spaces = spaces
4707
self.vertdata = vertdata
4708
if dicv0 is None:
4709
self.dicv0 = {v: 0 for v in range(gamma.num_verts())}
4710
else:
4711
self.dicv0 = dicv0
4712
4713
if dicl0 is None:
4714
self.dicl0 = {l: l for l in gamma.list_markings()}
4715
else:
4716
self.dicl0 = dicl0
4717
4718
if poly is None:
4719
self.poly = onekppoly(gamma.num_verts())
4720
else:
4721
self.poly = poly
4722
4723
def __repr__(self):
4724
return repr((self.gamma, self.spaces, self.vertdata, self.dicv0, self.dicl0, self.poly))
4725
4726
def __neg__(self):
4727
return (-1) * self
4728
4729
def __rmul__(self, other):
4730
# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):
4731
new = deepcopy(self)
4732
new.poly *= other
4733
return new
4734
4735
# self represents a morphism H_1 x ... x H_r -i-> M_1 x ... x M_r -pf-> M_1'x ... x M_g', where the first map i is the inclusion of Hurwitz stacks and the second map pf is a product of forgetful maps of some of the M_i. The spaces M_j' are those belonging to the vertices of self.gamma
4736
# prodforgetpullback takes a dictionary spacelist mapping space labels a_1, ..., a_r to the lists of vertices j using these spaces (i.e. the M_j'-component of pf depends on the M_a_i argument if j in spacelist[a_i])
4737
# it returns the pullback of self.poly under pf, which is a prodtautclass on a stgraph being the disjoint union of M_1, ..., M_r (in the order of spacelist.keys())
4738
# this function is meant as an auxiliary function for self.evaluate()
4739
def prodforgetpullback(self, spacelist):
4740
alist = list(spacelist)
4741
decs = decstratum(self.gamma, poly=self.poly)
4742
splitdecs = decs.split()
4743
4744
# find number of markings of spaces M_i
4745
N = {a: self.spaces[a][1].nummarks() for a in spacelist}
4746
forgetlegs = [list(set(range(1, N[self.vertdata[v][0]] + 1)) - set(self.vertdata[v][1].values()))
4747
for v in range(self.gamma.num_verts())]
4748
resultgraph = stgraph([self.spaces[a][0] for a in spacelist], [list(range(1, N[a] + 1)) for a in N], [])
4749
4750
# rename splitted kppolys to match the standard names of markings on the M_i and wrap them in decstrata
4751
trivgraphs = [stgraph([self.gamma.genera(i)], [list(self.vertdata[i][1].values())], [])
4752
for i in range(self.gamma.num_verts())]
4753
splitdecs = [[s[i].rename_legs(self.vertdata[i][1]) for i in range(len(s))] for s in splitdecs]
4754
splitdecs = [[decstratum(trivgraphs[i], poly=s[i]) for i in range(len(s))] for s in splitdecs]
4755
4756
result = prodtautclass(resultgraph, [])
4757
for s in splitdecs:
4758
term = prodtautclass(resultgraph)
4759
for j in range(len(alist)):
4760
t = prod([s[i].forgetful_pullback(forgetlegs[i]) for i in spacelist[alist[j]]])
4761
t.dimension_filter()
4762
t = t.toprodtautclass()
4763
term = term.factor_pullback([j], t)
4764
result += term
4765
return result
4766
4767
# evaluates self against the fundamental class of the ambient space (of self.gamma), returns a rational number
4768
4769
def evaluate(self):
4770
spacelist = {a: [] for a in self.spaces}
4771
for v in range(self.gamma.num_verts()):
4772
spacelist[self.vertdata[v][0]].append(v)
4773
for a in spacelist:
4774
if not spacelist[a]:
4775
spacelist.pop(a)
4776
alist = list(spacelist)
4777
4778
pfp = self.prodforgetpullback(spacelist)
4779
Gr = [trivGgraph(*self.spaces[a]) for a in spacelist]
4780
prodGr = [prodHclass(gr.gamma, [gr.to_decHstratum()]) for gr in Gr]
4781
result = 0
4782
for t in pfp.terms:
4783
tempresult = 1
4784
for i in range(len(alist)):
4785
if not t[i].gamma.edges(): # decstratum t[i] is a pure kppoly
4786
tempresult *= (Hdecstratum(Gr[i], poly=t[i].poly)).quotient_pushforward().evaluate()
4787
else:
4788
tempresult *= (prodGr[i] * t[i]).evaluate()
4789
result += tempresult
4790
return result
4791
4792
# replaces the fundamental class of the Hurwitz space with label a with the (fundamental class of the stratum corresponding to) Gstgraph Gr
4793
# in particular, self.gamma is changed by replacing all vertices with vertdata a with the graph Gr (more precisely: with the graph obtained by forgetting the corresponding markings in Gr and then stabilizing
4794
# if the vertex specialv in self and the edge speciale in Gr are given, also return the edge corresponding to the version of speciale that was glued to the vertex specialv (which by assumption has space labelled by a)
4795
# IMPORTANT: here we assume that speciale is not contracted in the forgetful-stabilization process!
4796
def replace_space_by_Gstgraph(self, a, Gr, specialv=None, speciale=None):
4797
# global examinrep
4798
# examinrep=(deepcopy(self),a,deepcopy(Gr),specialv,speciale)
4799
4800
avertices = [v for v in range(self.gamma.num_verts()) if self.vertdata[v][0] == a]
4801
avertices.reverse()
4802
4803
gluein = Gr.to_decHstratum()
4804
4805
maxspacelabel = max(self.spaces)
4806
self.spaces.pop(a)
4807
self.spaces.update({l + maxspacelabel + 1: gluein.spaces[l] for l in gluein.spaces})
4808
4809
for v in avertices:
4810
# First we must create the graph to be glued in at v. For this we take the graph from gluein and forget unnecessary markings, stabilize and then rename the remaining markings as dictated by self.vertdata[v][1]. During all steps we must record which vertices/legs go where
4811
4812
gluegraph = gluein.gamma.copy()
4813
dicl_v = self.vertdata[v][1]
4814
4815
usedlegs = dicl_v.values()
4816
unusedlegs = [l for l in gluegraph.list_markings() if l not in usedlegs]
4817
4818
gluegraph.forget_markings(unusedlegs)
4819
4820
(forgetdicv, forgetdicl, forgetdich) = gluegraph.stabilize()
4821
4822
forgetdicl.update({l: l for l in gluegraph.leglist() if l not in forgetdicl})
4823
4824
# now we must rename the legs in gluegraph so they fit with the leg-names in self.gamma, using the inverse of dicl_v
4825
dicl_v_inverse = {dicl_v[l]: l for l in dicl_v}
4826
shift = max(list(dicl_v) + [0]) + 1
4827
gluegraph.rename_legs(dicl_v_inverse, shift)
4828
4829
divGr = {}
4830
divs = {}
4831
dil = {}
4832
numvert_old = self.gamma.num_verts()
4833
num_newvert = gluegraph.num_verts()
4834
dic_voutgoing = {l: l for l in self.gamma.legs(v, copy=False)}
4835
self.gamma = self.gamma.copy()
4836
self.gamma.glue_vertex(v, gluegraph, divGr, divs, dil)
4837
self.gamma.set_immutable()
4838
dil.update(dic_voutgoing)
4839
dil_inverse = {dil[l]: l for l in dil}
4840
4841
if specialv == v:
4842
speciale_im = (dil[forgetdich.get(speciale[0], speciale[0]) + shift],
4843
dil[forgetdich.get(speciale[1], speciale[1]) + shift])
4844
4845
# now repair vertdata: note that new vertex list is obtained by deleting v and appending vertex list of gluegraph
4846
# also note that in the gluing-procedure ALL leg-labels at vertices different from v have NOT changed, so those vertdatas do not have to be adapted
4847
self.vertdata.pop(v)
4848
for w in range(num_newvert):
4849
# forgetdicv[w] is the vertex number that w corresponded to before stabilizing, inside gluein
4850
(pre_b, pre_diclb) = gluein.vertdata[forgetdicv[w]]
4851
4852
b = pre_b + maxspacelabel + 1
4853
# pre_diclb is injective dictionary from legs of vertex forgetdicv[w] in gluein.gamma to the standard-leg-numbers in self.spaces[b]
4854
# thus we need to reverse the shift/rename and the glue_vertex-rename and compose with diclb
4855
diclb = {l: pre_diclb[forgetdicl[dicl_v.get(dil_inverse[l], dil_inverse[l] - shift)]]
4856
for l in self.gamma.legs(w + numvert_old - 1, copy=False)}
4857
4858
self.vertdata.append([b, diclb])
4859
4860
# now repair dicv0, the surjective map from vertices of self.gamma to the gamma0 of the containing prodHclass
4861
# vertex v went to some vertex w, now all vertices that have replaced v must go to w
4862
# unfortunately, all vertices v+1, v+2, ... have shifted down by 1, so this must be taken care of
4863
w = self.dicv0.pop(v)
4864
for vnumb in range(v, numvert_old - 1):
4865
self.dicv0[vnumb] = self.dicv0[vnumb + 1]
4866
for vnumb in range(numvert_old - 1, numvert_old - 1 + num_newvert):
4867
self.dicv0[vnumb] = w
4868
4869
# now repair dicl0, the injective map from leg-names of gamma0 to leg-names of self.gamma
4870
# ACTUALLY: since gluing does not change any of the old leg-names, dicl0 is already up to date!
4871
4872
# now repair self.poly, term by term, collecting the results in newpoly, which replaces self.poly at the end
4873
newpoly = kppoly([], [])
4874
for (kappa, psi, coeff) in self.poly:
4875
trunckappa = [l[:] for l in kappa]
4876
kappav = trunckappa.pop(v)
4877
trunckappa += [[] for i in range(num_newvert)]
4878
trunckppoly = kppoly([(trunckappa, psi)], [coeff])
4879
trunckppoly *= prod([(sum([kappacl(numvert_old - 1 + k, j + 1, numvert_old + num_newvert - 1)
4880
for k in range(num_newvert)]))**kappav[j] for j in range(len(kappav))])
4881
newpoly += trunckppoly
4882
4883
self.poly = newpoly
4884
4885
spacesused = {a: False for a in self.spaces}
4886
for a, _ in self.vertdata:
4887
spacesused[a] = True
4888
unusedspaces = [a for a in spacesused if not spacesused[a]]
4889
4890
degree = 1
4891
for a in unusedspaces:
4892
trivGraph = trivGgraph(*self.spaces.pop(a))
4893
if trivGraph.dim() == 0:
4894
degree *= trivGraph.delta_degree(0)
4895
else:
4896
degree = 0
4897
break
4898
4899
self.poly *= degree
4900
4901
if specialv is not None:
4902
return speciale_im
4903
4904
# takes genus g, number n of markings and lists leglists, legdics of length r (and possibly a tautclass T on \bar M_{g,n})
4905
# for i=0,..,r-1 the dictionary legdics[i] is an injection of the list leglists[i] in {1,..,n}
4906
# computes the class DELTA of the small diagonal in (\bar M_{g,n})^r, intersects with T on the first factor and then pushes down under a forgetful map
4907
# this map exactly forgets in the ith factor all markings not in the image of legdics[i]
4908
# afterwards it renames the markings on the ith factor such that marking legdics[i][j] is called j
4909
# it returns a prodtautclass on a graph that is the disjoint union of genus g vertices with leglists given by leglists (assumed to be disjoint)
4910
4911
4912
def forgetful_diagonal(g, n, leglists, legdics, T=None):
4913
# global inpu
4914
# inpu = (g,n,leglists,legdics,T)
4915
r = len(leglists)
4916
if T is None:
4917
T = tautclass([decstratum(stgraph([g], [list(range(1, n + 1))], []))])
4918
4919
gamma = stgraph([g for i in range(r)], [list(range(1, n + 1)) for i in range(r)], [])
4920
result = prodtautclass(gamma, [[decstratum(stgraph([g], [list(range(1, n + 1))], [])) for i in range(r)]])
4921
4922
# We want to obtain small diagonal by intersecting Delta_12, Delta_13, ..., Delta_1r
4923
# since we will forget markings in the factors 2,..,r, only components of Delta_1i are relevant which have sufficient cohomological degree in the second factor (all others vanish in the pushforward having positive-dimensional fibres)
4924
forgetlegs = [[j for j in range(1, n + 1) if j not in legdics[i].values()] for i in range(r)]
4925
rndics = [{legdics[i][leglists[i][j]]: j + 1 for j in range(len(leglists[i]))} for i in range(r)]
4926
4927
if r > 1:
4928
# TODO: check that everything is tautological more carefully, only checking those degrees that are really crucial
4929
for rdeg in range(0, 2 * (3 * g - 3 + n) + 1):
4930
if not cohom_is_taut(g, n, rdeg):
4931
print("In computation of diagonal : H^%s(bar M_%s,%s) not necessarily tautological" % (rdeg, g, n))
4932
4933
# now compute diagonal
4934
Dgamma = stgraph([g, g], [list(range(1, n + 1)), list(range(1, n + 1))], [])
4935
Delta = prodtautclass(Dgamma, [])
4936
for deg in range(0, (3 * g - 3 + n) + 1):
4937
gi1 = generating_indices(g, n, deg)
4938
gi2 = generating_indices(g, n, 3 * g - 3 + n - deg)
4939
strata1 = DR.all_strata(g, deg, tuple(range(1, n + 1)))
4940
gens1 = [Graphtodecstratum(strata1[j]) for j in gi1]
4941
strata2 = DR.all_strata(g, 3 * g - 3 + n - deg, tuple(range(1, n + 1)))
4942
gens2 = [Graphtodecstratum(strata2[j]) for j in gi2]
4943
4944
# compute inverse of intersection matrix
4945
invintmat = inverseintmat(g, n, tuple(gi1), tuple(gi2), deg)
4946
for a in range(len(gi1)):
4947
for b in range(len(gi2)):
4948
if invintmat[a, b] != 0:
4949
Delta.terms.append([invintmat[a, b] * gens1[a], gens2[b]])
4950
4951
for i in range(1, r):
4952
result = result.factor_pullback([0, i], Delta)
4953
result = result.factor_pullback([0], T.toprodtautclass())
4954
4955
# now we take care of forgetful pushforwards and renaming
4956
for t in result.terms:
4957
for i in range(r):
4958
t[i] = t[i].forgetful_pushforward(forgetlegs[i])
4959
# note: there can be no half-edge names in the range 1,..,n so simply renaming legs is possible
4960
t[i].rename_legs(rndics[i])
4961
4962
result.gamma = stgraph([g for i in range(r)], [l[:] for l in leglists], [])
4963
4964
return result.consolidate()
4965
4966
4967
@cached_function
4968
def inverseintmat(g, n, gi1, gi2, deg):
4969
if deg <= QQ(3 * g - 3 + n) / 2:
4970
intmat = matrix(DR.pairing_submatrix(gi1, gi2, g, deg, tuple(range(1, n + 1))))
4971
else:
4972
intmat = matrix(DR.pairing_submatrix(gi2, gi1, g, 3 * g - 3 + n - deg, tuple(range(1, n + 1)))).transpose()
4973
return intmat.transpose().inverse()
4974
4975
4976
def inverseintmat2(g, n, gi1, gi2, deg):
4977
tg1 = tautgens(g, n, deg)
4978
tg2 = tautgens(g, n, 3 * g - 3 + n - deg)
4979
intmat = matrix([[(tg1[i] * tg2[j]).evaluate() for i in gi1] for j in gi2])
4980
return intmat.transpose().inverse()
4981
4982
4983
""" # remove all terms that must vanish for dimension reasons
4984
def dimension_filter(self):
4985
for i in range(len(self.poly.monom)):
4986
(kappa,psi)=self.poly.monom[i]
4987
for v in range(len(self.gamma.genera)):
4988
# local check: we are not exceeding dimension at any of the vertices
4989
if sum([(k+1)*kappa[v][k] for k in range(len(kappa[v]))])+sum([psi[l] for l in self.gamma.legs(v) if l in psi])>3*self.gamma.genera(v)-3+len(self.gamma.legs(v)):
4990
self.poly.coeff[i]=0
4991
break
4992
# global check: the total codimension of the term of the decstratum is not higher than the total dimension of the ambient space
4993
#if self.poly.deg(i)+len(self.gamma.edges)>3*self.gamma.g()-3+len(self.gamma.list_markings()):
4994
# self.poly.coeff[i]=0
4995
#break
4996
return self.consolidate()
4997
4998
def consolidate(self):
4999
self.poly.consolidate()
5000
return self """
5001
5002
""" # computes integral against the fundamental class of the corresponding moduli space
5003
# will not complain if terms are mixed degree or if some of them do not have the right codimension
5004
def evaluate(self):
5005
answer=0
5006
for (kappa,psi,coeff) in self.poly:
5007
temp=1
5008
for v in range(len(self.gamma.genera)):
5009
psilist=[psi.get(l,0) for l in self.gamma.legs(v)]
5010
kappalist=[]
5011
for j in range(len(kappa[v])):
5012
kappalist+=[j+1 for k in range(kappa[v][j])]
5013
if sum(psilist+kappalist) != 3*self.gamma.genera(v)-3+len(psilist):
5014
temp = 0
5015
break
5016
temp*=DR.socle_formula(self.gamma.genera(v),psilist,kappalist)
5017
answer+=coeff*temp
5018
return answer """
5019
""" def rename_legs(self,dic):
5020
self.gamma.rename_legs(dic)
5021
self.poly.rename_legs(dic)
5022
return self
5023
def __repr__(self):
5024
return 'Graph : ' + repr(self.gamma) +'\n'+ 'Polynomial : ' + repr(self.poly) """
5025
5026
5027
# old code snippet for boundary pullback of decHstrata
5028
"""
5029
if len(erg.genera)==1:
5030
# loop graph, add all missing markings to it
5031
egr.legs(0)+=forgetmarks
5032
divlist=[egr]
5033
if len(erg.genera)==2:
5034
possi=[[0,1] for j in forgetmarks]
5035
divlist=[]
5036
for distri in itertools.product(*possi):
5037
# distri = (0,0,1,0,1,1) means that forgetmarks number 0,1,3 go to erg.legs(0) and 2,4,5 go to erg.legs(1)
5038
ergcpy=deepcopy(erg)
5039
ergcpy.legs(0)+=[forgetmarks[j] for j in range(len(forgetmarks)) if distri[j]==0]
5040
ergcpy.legs(1)+=[forgetmarks[j] for j in range(len(forgetmarks)) if distri[j]==1]
5041
divlist.append(ergcpy)
5042
"""
5043
5044
# Knowing that there exists a morphism Gamma -> A, we want to reconstruct the corresponding dicv (from vertices of Gamma to vertices of A) from the known dicl
5045
# TODO: we could give dicv as additional argument if we already know part of the final dicv
5046
5047
5048
def dicv_reconstruct(Gamma, A, dicl):
5049
if A.num_verts() == 1:
5050
return {ver: 0 for ver in range(Gamma.num_verts())}
5051
else:
5052
dicv = {Gamma.vertex(dicl[h]): A.vertex(h) for h in dicl}
5053
remaining_vertices = set(range(Gamma.num_verts())) - set(dicv)
5054
remaining_edges = set(e for e in Gamma.edges(copy=False)
5055
if e[0] not in dicl.values())
5056
current_vertices = set(dicv)
5057
5058
while remaining_vertices:
5059
newcurrent_vertices = set()
5060
for e in remaining_edges.copy():
5061
if Gamma.vertex(e[0]) in current_vertices:
5062
vnew = Gamma.vertex(e[1])
5063
remaining_edges.remove(e)
5064
# if vnew is in current vertices, we don't have to do anything (already know it)
5065
# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)
5066
if vnew not in current_vertices:
5067
dicv[vnew] = dicv[Gamma.vertex(e[0])]
5068
remaining_vertices.discard(vnew)
5069
newcurrent_vertices.add(vnew)
5070
continue
5071
if Gamma.vertex(e[1]) in current_vertices:
5072
vnew = Gamma.vertex(e[0])
5073
remaining_edges.remove(e)
5074
# if vnew is in current vertices, we don't have to do anything (already know it)
5075
# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)
5076
if vnew not in current_vertices:
5077
dicv[vnew] = dicv[Gamma.vertex(e[1])]
5078
remaining_vertices.discard(vnew)
5079
newcurrent_vertices.add(vnew)
5080
current_vertices = newcurrent_vertices
5081
return dicv
5082
5083
5084
# approximation to the question if H^{r}(\overline M_{g,n},QQ) is generated by tautological classes
5085
# if True is returned, the answer is yes, if False is returned, the answer is no; if None is returned, we make no claim
5086
# we give a reference for each of the claims, though we do not claim that this is the historically first such reference
5087
def cohom_is_taut(g, n, r):
5088
if g == 0:
5089
return True # [Keel - Intersection theory of moduli space of stable N-pointed curves of genus zero]
5090
if g == 1 and r % 2 == 0:
5091
return True # [Petersen - The structure of the tautological ring in genus one]
5092
if g == 1 and n < 10:
5093
# [Graber, Pandharipande - Constructions of nontautological classes on moduli spaces of curves] TODO: also for n=10 probably, typo in GP ?
5094
return True
5095
if g == 1 and n >= 11 and r == 11:
5096
return False # [Graber, Pandharipande]
5097
if g == 2 and r % 2 == 0 and n < 20:
5098
return True # [Petersen - Tautological rings of spaces of pointed genus two curves of compact type]
5099
if g == 2 and n <= 3:
5100
# [Getzler - Topological recursion relations in genus 2 (above Prop. 16+e_11=-1,e_2=0)] + [Yang - Calculating intersection numbers on moduli spaces of curves]
5101
return True
5102
if g == 2 and n == 4:
5103
# [Bini,Gaiffi,Polito - A formula for the Euler characteristic of \bar M_{2,4}] + [Yang] + [Petersen - Tautological ...]: all even cohomology is tautological and dimensions from Yang sum up to Euler characteristic
5104
return True
5105
# WARNING: [BGP] contains error, later found in [Bini,Harer - Euler characteristics of moduli spaces of curves], does not affect the numbers above
5106
if g == 3 and n == 0:
5107
return True # [Getzler - Topological recursion relations in genus 2 (Proof of Prop. 16)] + [Yang]
5108
if g == 3 and n == 1:
5109
return True # [Getzler,Looijenga - The hodge polynomial of \bar M_3,1] + [Yang]
5110
if g == 3 and n == 2:
5111
return True # [Bergström - Cohomology of moduli spaces of curves of genus three via point counts]
5112
if g == 4 and n == 0:
5113
return True # [Bergström, Tommasi - The rational cohomology of \bar M_4] + [Yang]
5114
5115
# TODO: Getzler computes Serre characteristic in genus 1 ('96)
5116
# TODO: [Bini,Harer - Euler characteristics of moduli spaces of curves]: recursive computation of chi(\bar M_{g,n})
5117
# This suggests that g=3,n=2 should also work (Yang's numbers add up to Euler char.)
5118
5119
if r in [0, 2]:
5120
# r=0: fundamental class is tautological, r=1: [Arbarello, Cornalba - The Picard groups of the moduli spaces of curves]
5121
return True
5122
if r in [1, 3, 5]:
5123
return True # [Arbarello, Cornalba - Calculating cohomology groups of moduli spaces of curves via algebraic geometry]
5124
if 2 * (3 * g - 3 + n) - r in [0, 1, 2, 3, 5]:
5125
return True
5126
# dim=(3*g-3+n), then by Hard Lefschetz, there is isomorphism H^(dim-k) -> H^(dim+k) by multiplication with ample divisor
5127
# this divisor is tautological, so if all of H^(dim-k) is tautological, also H^(dim+k) is tautological
5128
5129
return None
5130
5131
# approximation to the question if the Faber-Zagier relations generate all tautological relations in A^d(\bar M_{g,n})
5132
# if True is returned, the answer is yes, if False is returned, it means that no proof has been registered yet
5133
# we give a reference for each of the claims, though we do not claim that this is the historically first such reference
5134
5135
5136
@file_cache.file_cached_function(
5137
directory=os.path.join(DOT_SAGE, "admcycles"),
5138
url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",
5139
remote_database_list="https://modulispaces.gitlab.io/relations-database/FZ_conjecture_holds_files",
5140
env_var="ADMCYCLES_CACHE_DIR",
5141
key=file_cache.ignore_args_key([4]),
5142
filename=file_cache.ignore_args_filename())
5143
def FZ_conjecture_holds(g, n, d, method='pair', quiet=True):
5144
r"""
5145
Checks if the set of generalized Faber-Zagier relations [PPZ15]_ in R^d(Mbar_{g,n}) is complete.
5146
5147
It does so by comparing the rank of the quotient of the strata algebra by the FZ-relations
5148
to a lower bound for the rank of the cohomology group R^d(Mbar_{g,n}).
5149
5150
For method = 'pair', this lower bound is obtained as the rank of a matrix of intersection
5151
numbers of generators of R^d(Mbar_{g,n}) with generators in opposite degree.
5152
5153
For method = 'pull', the lower bound arises from the rank of a matrix obtained from
5154
intersection numbers with kappa-psi-monomials and boundary pullbacks to products of spaces
5155
Mbar_{gi, ni} for which the FZ-conjecture is checked recursively.
5156
5157
For quiet = False, the program gives status reports about the progress of the computation.
5158
5159
EXAMPLES::
5160
5161
sage: from admcycles.admcycles import FZ_conjecture_holds
5162
sage: FZ_conjecture_holds(1,4,1)
5163
True
5164
sage: FZ_conjecture_holds(2,3,1,method='pull')
5165
True
5166
"""
5167
if g < 0 or n < 0 or d < 0 or d > 3 * g - 3 + n:
5168
return True # nonsensical cases
5169
gi = generating_indices(g, n, d)
5170
quotdim = len(gi)
5171
5172
if method == 'pair':
5173
pgens = tautgens(g, n, d)
5174
gens = [pgens[i] for i in gi]
5175
cogens = tautgens(g, n, 3 * g - 3 + n - d)
5176
per = [i - 1 for i in Permutations(len(cogens)).random_element()]
5177
5178
space = span(QQ, [[0 for i in range(quotdim)]])
5179
5180
for i, p in enumerate(per):
5181
space += span(QQ, [[(a * cogens[p]).evaluate() for a in gens]])
5182
if not quiet:
5183
print((i + 1, space.rank()))
5184
if space.rank() == quotdim:
5185
return True
5186
return False
5187
5188
# Below is the code in case method == 'pull'
5189
5190
# Step 1 : pairings with kappa-psi monomials
5191
A, _ = kpintersection_matrix(g, n, d)
5192
A.echelonize()
5193
if A.rank() == quotdim:
5194
if not quiet:
5195
print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))
5196
return True
5197
5198
# Step 2 : pullbacks by separating boundary morphisms
5199
if all(d - di > 3 * (g - gi) - 3 + (n - ni + 1) or FZ_conjecture_holds(gi, ni, di) for gi in range(1, g) for ni in range(1, n + 2) for di in range(d + 1))\
5200
and FZ_conjecture_holds(g, n - 1, d):
5201
A = block_matrix([[A], [pullback_matrix(g, n, d, irrbdry=False)]], subdivide=False)
5202
A.echelonize()
5203
if A.rank() == quotdim:
5204
if not quiet:
5205
print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))
5206
return True
5207
5208
# Step 3 : pullback by non-separating boundary morphism
5209
if FZ_conjecture_holds(g - 1, n + 2, d):
5210
ibd = StableGraph([g - 1], [list(range(1, n + 3))], [(n + 1, n + 2)])
5211
A = block_matrix([[A], [pullback_matrix(g, n, d, bdry=ibd)]], subdivide=False)
5212
A.echelonize()
5213
if A.rank() == quotdim:
5214
if not quiet:
5215
print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))
5216
return True
5217
# all has failed
5218
if not quiet:
5219
print('FZ-conjecture cannot be verified for (g,n,d) = ' + repr((g, n, d)) + '!')
5220
print('Rank of quotient by FZ-relations : ' + repr(quotdim))
5221
print('Rank of intersection and pullback : ' + repr(A.rank()))
5222
return False
5223
5224
5225
def FZresulttable(output='betti'):
5226
r"""
5227
Returns string representing ranks of the tautological ring (for output='betti') or
5228
cases where the Faber-Zagier conjecture has been verified (for output='FZconfirm').
5229
5230
EXAMPLES::
5231
5232
sage: from admcycles.admcycles import FZresulttable
5233
sage: # FZresulttable throws an error if there aren't any results stored
5234
sage: # To make the success of the doctest independend from the order in which
5235
sage: # the doctests are executed we just store something.
5236
sage: from admcycles import generating_indices
5237
sage: g = generating_indices(0,3,0)
5238
sage: s = FZresulttable(output='betti')
5239
"""
5240
result = 'g\tn\tr=0'
5241
if output == 'betti':
5242
gicache = generating_indices.cache
5243
bettidict = {(a[0], a[1], a[2]): len(b) for a, b in gicache.items()}
5244
if output == 'FZconfirm':
5245
gicache = FZ_conjecture_holds.cache
5246
bettidict = {(a[0], a[1], a[2]): b for a, b in gicache.items()}
5247
gnknowns = sorted(list(set((g, n) for g, n, _ in bettidict)))
5248
5249
rmax = max(r for _, _, r in bettidict)
5250
result = 'g\tn\t'
5251
for r in range(rmax + 1):
5252
result += 'r=' + repr(r) + '\t'
5253
result += '\n'
5254
for i, (g, n) in enumerate(gnknowns):
5255
if i > 0 and g > gnknowns[i - 1][0] or n > gnknowns[i - 1][1]:
5256
result += '\n'
5257
result += repr(g) + '\t' + repr(n) + '\t'
5258
for r in range(rmax + 1):
5259
if (g, n, r) in bettidict:
5260
result += repr(bettidict[(g, n, r)]) + '\t'
5261
else:
5262
result += '\t'
5263
return result
5264
5265
5266
# if g == 0:
5267
# return True # [Keel - Intersection theory of moduli space of stable N-pointed curves of genus zero]
5268
# if g == 1:
5269
# return True # [Petersen - The structure of the tautological ring in genus one] %TODO: verify FZ => Getzler's rel. + WDVV
5270
5271
def kappapsipolys(g, n, d):
5272
r"""
5273
Returns a list of the polynomials in kappa and psi classes on Mbar_{g,n} of degree d.
5274
5275
EXAMPLES::
5276
5277
sage: from admcycles.admcycles import kappapsipolys
5278
sage: for a in kappapsipolys(0,4,1):
5279
....: print(a)
5280
Graph : [0] [[1, 2, 3, 4]] []
5281
Polynomial : (kappa_1)_0
5282
Graph : [0] [[1, 2, 3, 4]] []
5283
Polynomial : psi_1
5284
Graph : [0] [[1, 2, 3, 4]] []
5285
Polynomial : psi_2
5286
Graph : [0] [[1, 2, 3, 4]] []
5287
Polynomial : psi_3
5288
Graph : [0] [[1, 2, 3, 4]] []
5289
Polynomial : psi_4
5290
"""
5291
# TODO: should we deprecate?
5292
from .tautological_ring import TautologicalRing
5293
return TautologicalRing(g, n).kappa_psi_polynomials(d)
5294
5295
# pairing_submatrix(tuple(generating_indices(4,1,4)),tuple(range(300)),4,4,(1,))
5296
5297
############
5298
#
5299
# Lookup and saving functions for Hdatabase
5300
#
5301
############
5302
5303
5304
def save_FZrels():
5305
"""
5306
Saving previously computed Faber-Zagier relations [PPZ15]_ to file new_geninddb.pkl.
5307
5308
Deprecated. Saving and loading of FZ relations is now handled automatically.
5309
"""
5310
from .superseded import deprecation
5311
deprecation(168, 'Saving and loading of FZ relations is now handled automatically.')
5312
5313
5314
def load_FZrels():
5315
"""
5316
Loading values -- Deprecated
5317
Please run convert_old_FZ_database() ones to convert your cache to the new format.
5318
Saving and loading of FZ relations is handled automatically afterwards.
5319
"""
5320
from .superseded import deprecation
5321
deprecation(168, ('The format of the cache file has changed. '
5322
'Please run convert_old_FZ_database() ones to convert your cache to the '
5323
'new format. '
5324
'Saving and loading of FZ relations is handled automatically afterwards.'))
5325
5326
5327
def convert_old_FZ_database():
5328
r"""
5329
Converts the relations saved using the old method.
5330
Converts the `new_geminddb.pkl` file to a different file for each combination of a function and its arguments.
5331
The old file contains data for the functions ``generating_indices``, ``genstobasis`` and ``FZ_conjecture_holds``.
5332
"""
5333
old_file = "new_geninddb.pkl"
5334
with open(old_file, 'rb') as old_rels:
5335
old_dic = pickle.load(old_rels)
5336
5337
for gi in old_dic[0].keys():
5338
generating_indices.set_cache(old_dic[0][gi], *gi[0])
5339
for gb in old_dic[1].keys():
5340
genstobasis.set_cache(old_dic[1][gb], *gb[0])
5341
for fzch in old_dic[2].keys():
5342
FZ_conjecture_holds.set_cache(old_dic[2][fzch], *fzch[0])
5343
5344
5345
def set_online_lookup(b):
5346
r"""
5347
Temporarily set online lookup for all supported functions.
5348
Use func:`set_online_lookup_default` to save a default setting.
5349
5350
Those are currently :func:`genstobasi`, func:`generating_indices`, func:`FZ_conjecture_holds`.
5351
5352
EXAMPLES::
5353
5354
sage: from admcycles.admcycles import set_online_lookup
5355
sage: from admcycles.admcycles import genstobasis, generating_indices, FZ_conjecture_holds
5356
sage: functions = [genstobasis, generating_indices, FZ_conjecture_holds]
5357
sage: set_online_lookup(True)
5358
sage: assert all(f.go_online == True for f in functions)
5359
sage: set_online_lookup(False)
5360
sage: assert all(f.go_online == False for f in functions)
5361
"""
5362
functions = [genstobasis, generating_indices, FZ_conjecture_holds]
5363
for f in functions:
5364
f.set_online_lookup(b)
5365
5366
5367
def set_online_lookup_default(b):
5368
r"""
5369
Sets a default for online lookup for all supported functions.
5370
Use func:`set_online_lookup` for a temporary setting.
5371
5372
Those are currently :func:`genstobasi`, func:`generating_indices`, func:`FZ_conjecture_holds`.
5373
"""
5374
functions = [genstobasis, generating_indices, FZ_conjecture_holds]
5375
for f in functions:
5376
f.set_online_lookup_default(b)
5377
5378
5379
def download_FZ_database():
5380
r"""
5381
Download the remove database for all supported functions.
5382
"""
5383
functions = [genstobasis, generating_indices, FZ_conjecture_holds]
5384
for f in functions:
5385
f.download_all()
5386
5387
5388
# looks up if the Hurwitz cycle for (g,dat) has already been computed. If yes, returns a fresh tautclass representing this
5389
# it takes care of the necessary reordering and possible pushforwards by forgetting markings
5390
# if this cycle has not been computed before, it raises a KeyError
5391
def Hdb_lookup(g, dat, markings):
5392
# first get a sorted version of dat, remembering the reordering
5393
li = dat.l
5394
sort_li = sorted(enumerate(li), key=lambda x: x[1])
5395
# [2,0,1] means that li looking like [a,b,c] now is reordered as li_reord=[c,a,b]
5396
ind_reord = [i[0] for i in sort_li]
5397
li_reord = [i[1] for i in sort_li]
5398
dat_reord = HurwitzData(dat.G, li_reord)
5399
5400
# now create a dictionary legpermute sending the marking-names in the space you want to the corresponding names in the standard-ordered space
5401
mn_list = []
5402
num_marks = 0
5403
Gord = dat.G.order()
5404
for count in range(len(li)):
5405
new_marks = QQ(Gord) / li[count][1]
5406
mn_list.append(list(range(num_marks + 1, num_marks + new_marks + 1)))
5407
5408
mn_reord = []
5409
for i in ind_reord:
5410
mn_reord.append(mn_list[i])
5411
5412
Hdb = Hdatabase[(g, dat_reord)]
5413
for marks in Hdb:
5414
if set(markings).issubset(set(marks)):
5415
forgottenmarks = set(marks) - set(markings)
5416
result = deepcopy(Hdb[marks])
5417
result = result.forgetful_pushforward(list(forgottenmarks))
5418
# TODO: potentially include this result in Hdb
5419
5420
5421
############
5422
#
5423
# Library of interesting cycle classes
5424
#
5425
############
5426
5427
def fundclass(g, n):
5428
r"""
5429
Return the fundamental class of \Mbar_g,n.
5430
"""
5431
from .tautological_ring import TautologicalRing
5432
return TautologicalRing(g, n).fundamental_class()
5433
5434
# returns a list of generators of R^r(\bar M_{g,n}) as tautclasses in the order of Pixton's program
5435
# if decst=True, returns generators as decstrata, else as TautologicalClass
5436
5437
5438
def tautgens(g, n, r, decst=False, moduli='st'):
5439
r"""
5440
Returns a lists of all tautological classes of degree r on \bar M_{g,n}.
5441
5442
INPUT:
5443
5444
g : integer
5445
Genus g of curves in \bar M_{g,n}.
5446
n : integer
5447
Number of markings n of curves in \bar M_{g,n}.
5448
r : integer
5449
The degree r of of the classes.
5450
decst : boolean
5451
If set to True returns generators as decorated strata, else as tautological classes.
5452
moduli : string
5453
The moduli type (one of 'st', 'tl', 'ct', 'rt', 'sm').
5454
5455
EXAMPLES::
5456
5457
sage: from admcycles import *
5458
5459
sage: tautgens(2,0,2)[1]
5460
Graph : [2] [[]] []
5461
Polynomial : (kappa_1^2)_0
5462
5463
::
5464
5465
sage: L=tautgens(2,0,2);2*L[3]+L[4]
5466
Graph : [1] [[2, 3]] [(2, 3)]
5467
Polynomial : (kappa_1)_0
5468
<BLANKLINE>
5469
Graph : [1, 1] [[2], [3]] [(2, 3)]
5470
Polynomial : 2*psi_2
5471
"""
5472
if decst:
5473
L = DR.all_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True))
5474
return [Graphtodecstratum(l) for l in L]
5475
else:
5476
from .tautological_ring import TautologicalRing
5477
return TautologicalRing(g, n, moduli=get_moduli(moduli)).generators(r)
5478
5479
# prints a list of generators of R^r(\bar M_{g,n}) as tautclasses in the order of Pixton's program
5480
5481
5482
def list_tautgens(g, n, r):
5483
r"""
5484
Lists all tautological classes of degree r on \bar M_{g,n}.
5485
5486
INPUT:
5487
5488
g : integer
5489
Genus g of curves in \bar M_{g,n}.
5490
n : integer
5491
Number of markings n of curves in \bar M_{g,n}.
5492
r : integer
5493
The degree r of of the classes.
5494
5495
EXAMPLES::
5496
5497
sage: from admcycles import *
5498
5499
sage: list_tautgens(2,0,2)
5500
[0] : Graph : [2] [[]] []
5501
Polynomial : (kappa_2)_0
5502
[1] : Graph : [2] [[]] []
5503
Polynomial : (kappa_1^2)_0
5504
[2] : Graph : [1, 1] [[2], [3]] [(2, 3)]
5505
Polynomial : (kappa_1)_0
5506
[3] : Graph : [1, 1] [[2], [3]] [(2, 3)]
5507
Polynomial : psi_2
5508
[4] : Graph : [1] [[2, 3]] [(2, 3)]
5509
Polynomial : (kappa_1)_0
5510
[5] : Graph : [1] [[2, 3]] [(2, 3)]
5511
Polynomial : psi_2
5512
[6] : Graph : [0, 1] [[3, 4, 5], [6]] [(3, 4), (5, 6)]
5513
Polynomial : 1
5514
[7] : Graph : [0] [[3, 4, 5, 6]] [(3, 4), (5, 6)]
5515
Polynomial : 1
5516
"""
5517
L = tautgens(g, n, r)
5518
for i in range(len(L)):
5519
print('[' + repr(i) + '] : ' + repr(L[i]))
5520
5521
5522
def kappaclass(a, g=None, n=None):
5523
r"""
5524
Returns the (Arbarello-Cornalba) kappa-class kappa_a on \bar M_{g,n} defined by
5525
5526
kappa_a= pi_*(psi_{n+1}^{a+1})
5527
5528
where pi is the morphism \bar M_{g,n+1} --> \bar M_{g,n}.
5529
5530
INPUT:
5531
5532
a : integer
5533
The degree a of the kappa class.
5534
g : integer
5535
Genus g of curves in \bar M_{g,n}.
5536
n : integer
5537
Number of markings n of curves in \bar M_{g,n}.
5538
5539
EXAMPLES::
5540
5541
sage: from admcycles import kappaclass
5542
sage: a = kappaclass(1, 2, 1)
5543
sage: a
5544
Graph : [2] [[1]] []
5545
Polynomial : (kappa_1)_0
5546
sage: parent(a)
5547
TautologicalRing(g=2, n=1, moduli='st') over Rational Field
5548
5549
When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is
5550
possible to construct a
5551
:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value
5552
of g and n and call its method
5553
:class:`~admcycles.tautological_ring.TautologicalRing.kappa`::
5554
5555
sage: from admcycles import TautologicalRing
5556
sage: R = TautologicalRing(2, 1)
5557
sage: R.kappa(1)
5558
Graph : [2] [[1]] []
5559
Polynomial : (kappa_1)_0
5560
5561
TESTS::
5562
5563
sage: from admcycles import kappaclass, reset_g_n
5564
sage: reset_g_n(1, 2)
5565
doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.
5566
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5567
sage: a = kappaclass(2)
5568
doctest:...: DeprecationWarning: Use of kappaclass without specifying g,n is deprecated.
5569
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5570
sage: a
5571
Graph : [1] [[1, 2]] []
5572
Polynomial : (kappa_2)_0
5573
sage: parent(a)
5574
TautologicalRing(g=1, n=2, moduli='st') over Rational Field
5575
"""
5576
if g is None or n is None:
5577
from .superseded import deprecation
5578
deprecation(109, 'Use of kappaclass without specifying g,n is deprecated.')
5579
if g is None:
5580
g = globals()['g']
5581
if n is None:
5582
n = globals()['n']
5583
from .tautological_ring import TautologicalRing
5584
return TautologicalRing(g, n).kappa(a)
5585
5586
5587
def psiclass(i, g=None, n=None):
5588
r"""
5589
Returns the class psi_i on \bar M_{g,n}.
5590
5591
INPUT:
5592
5593
i : integer
5594
The leg i associated to the psi class.
5595
g : integer
5596
Genus g of curves in \bar M_{g,n}.
5597
n : integer
5598
Number of markings n of curves in \bar M_{g,n}.
5599
5600
EXAMPLES::
5601
5602
sage: from admcycles import psiclass
5603
sage: a = psiclass(1, 2, 1)
5604
sage: a
5605
Graph : [2] [[1]] []
5606
Polynomial : psi_1
5607
sage: parent(a)
5608
TautologicalRing(g=2, n=1, moduli='st') over Rational Field
5609
5610
When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is
5611
possible to construct a
5612
:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value
5613
of g and n and call its method
5614
:class:`~admcycles.tautological_ring.TautologicalRing.psi`::
5615
5616
sage: from admcycles import TautologicalRing
5617
sage: R = TautologicalRing(2, 1)
5618
sage: R.psi(1)
5619
Graph : [2] [[1]] []
5620
Polynomial : psi_1
5621
5622
TESTS::
5623
5624
sage: from admcycles import psiclass, reset_g_n
5625
sage: reset_g_n(2, 2)
5626
doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.
5627
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5628
sage: a = psiclass(1)
5629
doctest:...: DeprecationWarning: Use of psiclass without specifying g,n is deprecated.
5630
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5631
sage: a
5632
Graph : [2] [[1, 2]] []
5633
Polynomial : psi_1
5634
sage: parent(a)
5635
TautologicalRing(g=2, n=2, moduli='st') over Rational Field
5636
"""
5637
if g is None or n is None:
5638
from .superseded import deprecation
5639
deprecation(109, 'Use of psiclass without specifying g,n is deprecated.')
5640
if g is None:
5641
g = globals()['g']
5642
if n is None:
5643
n = globals()['n']
5644
from .tautological_ring import TautologicalRing
5645
return TautologicalRing(g, n).psi(i)
5646
5647
5648
def sepbdiv(g1, A, g=None, n=None):
5649
r"""
5650
Returns the pushforward of the fundamental class under the boundary gluing map \bar M_{g1,A} X \bar M_{g-g1,{1,...,n} \ A} --> \bar M_{g,n}.
5651
5652
INPUT:
5653
5654
g1 : integer
5655
The genus g1 of the first vertex.
5656
A: list
5657
The list A of markings on the first vertex.
5658
g : integer
5659
The total genus g of the graph.
5660
n : integer
5661
The total number of markings n of the graph.
5662
5663
EXAMPLES::
5664
5665
sage: from admcycles import sepbdiv
5666
5667
sage: a = sepbdiv(1, [1,3], 3, 4)
5668
sage: a
5669
Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]
5670
Polynomial : 1
5671
sage: a
5672
Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]
5673
Polynomial : 1
5674
sage: parent(a)
5675
TautologicalRing(g=3, n=4, moduli='st') over Rational Field
5676
5677
When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is
5678
possible to construct a
5679
:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value
5680
of g and n and call its method
5681
:class:`~admcycles.tautological_ring.TautologicalRing.sepbdiv`::
5682
5683
sage: from admcycles import TautologicalRing
5684
sage: R = TautologicalRing(3, 4)
5685
sage: R.sepbdiv(1, [1,3])
5686
Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]
5687
Polynomial : 1
5688
5689
TESTS::
5690
5691
sage: from admcycles import sepbdiv, reset_g_n
5692
sage: reset_g_n(2, 2)
5693
doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.
5694
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5695
sage: a = sepbdiv(1, [1])
5696
doctest:...: DeprecationWarning: Use of sepbdiv without specifying g,n is deprecated.
5697
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5698
sage: parent(a)
5699
TautologicalRing(g=2, n=2, moduli='st') over Rational Field
5700
"""
5701
if g is None or n is None:
5702
from .superseded import deprecation
5703
deprecation(109, 'Use of sepbdiv without specifying g,n is deprecated.')
5704
if g is None:
5705
g = globals()['g']
5706
if n is None:
5707
n = globals()['n']
5708
from .tautological_ring import TautologicalRing
5709
return TautologicalRing(g, n).separable_boundary_divisor(g1, A)
5710
5711
5712
def irrbdiv(g=None, n=None):
5713
r"""
5714
Returns the pushforward of the fundamental class under the irreducible boundary gluing map \bar M_{g-1,n+2} -> \bar M_{g,n}.
5715
5716
INPUT:
5717
5718
g : integer
5719
The total genus g of the graph.
5720
n : integer
5721
The total number of markings n of the graph.
5722
5723
EXAMPLES::
5724
5725
sage: from admcycles import irrbdiv, reset_g_n
5726
5727
sage: a = irrbdiv(2, 2)
5728
sage: a
5729
Graph : [1] [[1, 2, 3, 4]] [(3, 4)]
5730
Polynomial : 1
5731
sage: parent(a)
5732
TautologicalRing(g=2, n=2, moduli='st') over Rational Field
5733
5734
sage: a = irrbdiv(1, 1)
5735
sage: a
5736
Graph : [0] [[1, 2, 3]] [(2, 3)]
5737
Polynomial : 1
5738
sage: parent(a)
5739
TautologicalRing(g=1, n=1, moduli='st') over Rational Field
5740
5741
When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is
5742
possible to construct a
5743
:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value
5744
of g and n and call its method
5745
:class:`~admcycles.tautological_ring.TautologicalRing.irrbdiv`::
5746
5747
sage: from admcycles import TautologicalRing
5748
sage: R = TautologicalRing(1, 1)
5749
sage: R.irreducible_boundary_divisor()
5750
Graph : [0] [[1, 2, 3]] [(2, 3)]
5751
Polynomial : 1
5752
5753
TESTS::
5754
5755
sage: from admcycles import irrbdiv, reset_g_n
5756
sage: reset_g_n(1, 1)
5757
doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.
5758
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5759
sage: irrbdiv()
5760
doctest:...: DeprecationWarning: Use of irrbdiv without specifying g,n is deprecated.
5761
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5762
Graph : [0] [[1, 2, 3]] [(2, 3)]
5763
Polynomial : 1
5764
"""
5765
from .superseded import deprecation
5766
if g is None or n is None:
5767
deprecation(109, 'Use of irrbdiv without specifying g,n is deprecated.')
5768
if g is None:
5769
g = globals()['g']
5770
if n is None:
5771
n = globals()['n']
5772
from .tautological_ring import TautologicalRing
5773
return TautologicalRing(g, n).irreducible_boundary_divisor()
5774
5775
5776
def psi_correlator(*args):
5777
r"""
5778
Return the integral of psi classes.
5779
5780
Given either a sequence k1, ..., kn of non-negative integer arguments or
5781
a single list containing such numbers, this function computes the integral
5782
5783
\int_{Mbar_g,n} psi_1^k1 ... psi_n^kn .
5784
5785
EXAMPLES:
5786
5787
Examples in genus 0::
5788
5789
sage: from admcycles import psi_correlator
5790
sage: psi_correlator(0,0,0)
5791
1
5792
sage: psi_correlator(1,0,0,0)
5793
1
5794
sage: psi_correlator(0,2,0,0,0)
5795
1
5796
sage: psi_correlator(1,1,0,0,0)
5797
2
5798
5799
Examples in genus 1, with argument a list of integers::
5800
5801
sage: psi_correlator([1])
5802
1/24
5803
sage: psi_correlator([2, 0])
5804
1/24
5805
sage: psi_correlator([1, 1])
5806
1/24
5807
sage: psi_correlator([3, 0, 0])
5808
1/24
5809
sage: psi_correlator([2, 1, 0])
5810
1/12
5811
sage: psi_correlator([1, 1, 1])
5812
1/12
5813
5814
genus 2::
5815
5816
sage: psi_correlator(7)
5817
1/82944
5818
sage: psi_correlator(7, 1)
5819
5/82944
5820
sage: psi_correlator(6, 2)
5821
77/414720
5822
sage: psi_correlator(5, 3)
5823
503/1451520
5824
sage: psi_correlator(4, 4)
5825
607/1451520
5826
"""
5827
if len(args) == 1 and isinstance(args[0], Iterable):
5828
# user handed over list/tuple of numbers
5829
args = args[0]
5830
if not all(x in ZZ and x >= 0 for x in args):
5831
raise ValueError('arguments of psi_correlators must be nonnegative integers')
5832
n = len(args)
5833
s = sum(args)
5834
if (s - n) % 3:
5835
raise ValueError("the composition should sum up to 3*g - 3 + n")
5836
g = (s - n) // 3 + 1
5837
from .tautological_ring import TautologicalRing
5838
R = TautologicalRing(g, n)
5839
poly = {i + 1: ki for i, ki in enumerate(args) if ki}
5840
return R(R.trivial_graph(), psi=poly).evaluate()
5841
5842
# tries to return the class lambda_d on \bar M_{g,n}. We hope that this is a variant of the DR-cycle
5843
# def lambdaish(d,g,n):
5844
# dvector=vector([0 for i in range(n)])
5845
# return (-2)**(-g) * DR_cycle(g,dvector,d)
5846
5847
# Returns the chern character ch_d(EE) of the Hodge bundle EE on \bar M_{g,n} as a mixed degree tautological class (up to maxim. degree dmax)
5848
# implements the formula from [Mumford - Towards an enumerative geometry ...]
5849
5850
5851
@cached_function
5852
def hodge_chern_char(g, n, d):
5853
from .superseded import deprecation
5854
deprecation(109, 'hodge_chern_char is deprecated. Please use the hodge_chern_char method from TautologicalRing instead.')
5855
from .tautological_ring import TautologicalRing
5856
TautologicalRing(g, n).hodge_chern_character(d)
5857
5858
# converts the function chclass (sending m to ch_m) to the corresponding chern polynomial, up to degree dmax
5859
5860
5861
def chern_char_to_poly(chclass, dmax, g, n):
5862
result = deepcopy(tautgens(g, n, 0)[0])
5863
5864
argum = sum([factorial(m - 1) * chclass(m) for m in range(1, dmax + 1)])
5865
expo = deepcopy(argum)
5866
result = result + argum
5867
5868
for m in range(2, dmax + 1):
5869
expo *= argum
5870
expo.degree_cap(dmax)
5871
result += (QQ(1) / factorial(m)) * expo
5872
5873
return result
5874
5875
5876
def chern_char_to_class(t, char, g=None, n=None):
5877
r"""
5878
Turns a Chern character into a Chern class in the tautological ring of \Mbar_{g,n}.
5879
5880
INPUT:
5881
5882
t : integer
5883
degree of the output Chern class
5884
char : tautclass or function
5885
Chern character, either represented as a (mixed-degree) tautological class or as
5886
a function m -> ch_m
5887
5888
EXAMPLES:
5889
5890
Note that the output of generalized_hodge_chern takes the form of a chern character::
5891
5892
sage: from admcycles import *
5893
sage: from admcycles.GRRcomp import *
5894
sage: g=2;n=2;l=0;d=[1,-1];a=[[1,[1],-1]]
5895
sage: chern_char_to_class(1,generalized_hodge_chern(l,d,a,1,g,n))
5896
Graph : [2] [[1, 2]] []
5897
Polynomial : 1/12*(kappa_1)_0 - 13/12*psi_1 - 1/12*psi_2
5898
<BLANKLINE>
5899
Graph : [1] [[4, 5, 1, 2]] [(4, 5)]
5900
Polynomial : 1/24
5901
<BLANKLINE>
5902
Graph : [0, 2] [[1, 2, 4], [5]] [(4, 5)]
5903
Polynomial : 1/12
5904
<BLANKLINE>
5905
Graph : [1, 1] [[2, 4], [1, 5]] [(4, 5)]
5906
Polynomial : 13/12
5907
<BLANKLINE>
5908
Graph : [1, 1] [[4], [1, 2, 5]] [(4, 5)]
5909
Polynomial : 1/12
5910
"""
5911
from .tautological_ring import TautologicalRing, TautologicalClass
5912
if isinstance(char, TautologicalClass):
5913
arg = [(-1)**(s - 1) * factorial(s - 1) * char.simplified(r=s) for s in range(1, t + 1)]
5914
else:
5915
arg = [(-1)**(s - 1) * factorial(s - 1) * char(s) for s in range(1, t + 1)]
5916
exp = sum(multinomial(s.to_exp()) / factorial(len(s)) * prod(arg[k - 1] for k in s) for s in Partitions(t))
5917
if t == 0:
5918
if g is None or n is None:
5919
from .superseded import deprecation
5920
deprecation(109, 'Use of chern_char_to_class for t==0 without specifying g,n is deprecated.')
5921
return exp
5922
if g is None:
5923
g = globals()['g']
5924
if n is None:
5925
n = globals()['n']
5926
return exp * TautologicalRing(g, n).fundamental_class()
5927
return exp.simplified(r=t)
5928
5929
# the degree t part of the exponential function
5930
# we sum over the partitions of degree element to give a degree t element
5931
# The length of a partition is the power in the exponent we are looking at
5932
# Then we need to see the number of ways to divide the degrees over the x's to get the right thing.
5933
5934
5935
def lambdaclass(d, g=None, n=None, pull=True):
5936
r"""
5937
Returns the tautological class lambda_d on \bar M_{g,n} defined as the d-th Chern class
5938
5939
lambda_d = c_d(E)
5940
5941
of the Hodge bundle E. The result is represented as a sum of stable graphs with kappa and psi classes.
5942
5943
INPUT:
5944
5945
d : integer
5946
The degree d.
5947
g : integer
5948
Genus g of curves in \bar M_{g,n}.
5949
n : integer
5950
Number of markings n of curves in \bar M_{g,n}.
5951
pull : boolean, default = True
5952
Whether lambda_d on \bar M_{g,n} is computed as pullback from \bar M_{g}
5953
5954
EXAMPLES::
5955
5956
sage: from admcycles import *
5957
5958
sage: lambdaclass(1,2,0)
5959
Graph : [2] [[]] []
5960
Polynomial : 1/12*(kappa_1)_0
5961
<BLANKLINE>
5962
Graph : [1] [[2, 3]] [(2, 3)]
5963
Polynomial : 1/24
5964
<BLANKLINE>
5965
Graph : [1, 1] [[2], [3]] [(2, 3)]
5966
Polynomial : 1/24
5967
5968
When working with fixed g and n for the moduli space `\bar M_{g,n}`,
5969
it is possible to construct a :class:`~admcycles.tautological_ring.TautologicalRing`
5970
with the desired value of g and n::
5971
5972
sage: R = TautologicalRing(1, 1)
5973
sage: R.lambdaclass(1)
5974
Graph : [1] [[1]] []
5975
Polynomial : 1/12*(kappa_1)_0 - 1/12*psi_1
5976
<BLANKLINE>
5977
Graph : [0] [[3, 4, 1]] [(3, 4)]
5978
Polynomial : 1/24
5979
5980
TESTS::
5981
5982
sage: from admcycles import lambdaclass, reset_g_n
5983
sage: inputs = [(0,0,4), (1,1,3), (1,2,1), (2,2,1), (3,2,1), (-1,2,1), (2,3,2)]
5984
sage: for d,g,n in inputs:
5985
....: assert (lambdaclass(d, g, n)-lambdaclass(d, g, n, pull=False)).is_zero()
5986
5987
sage: reset_g_n(1,1)
5988
doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.
5989
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5990
sage: lambdaclass(1)
5991
doctest:...: DeprecationWarning: Use of lambdaclass without specifying g,n is deprecated.
5992
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
5993
Graph : [1] [[1]] []
5994
Polynomial : 1/12*(kappa_1)_0 - 1/12*psi_1
5995
<BLANKLINE>
5996
Graph : [0] [[3, 4, 1]] [(3, 4)]
5997
Polynomial : 1/24
5998
"""
5999
from .superseded import deprecation
6000
if g is None or n is None:
6001
deprecation(109, 'Use of lambdaclass without specifying g,n is deprecated.')
6002
if g is None:
6003
g = globals()['g']
6004
if n is None:
6005
n = globals()['n']
6006
from .tautological_ring import TautologicalRing
6007
return TautologicalRing(g, n).lambdaclass(d, pull=pull)
6008
6009
6010
def barH(g, dat, markings=None):
6011
"""Returns \\bar H on genus g with Hurwitz datum dat as a prodHclass on the trivial graph, remembering only the marked points given in markings"""
6012
Hbar = trivGgraph(g, dat)
6013
n = len(Hbar.gamma.list_markings())
6014
6015
# global Hexam
6016
# Hexam=(g,dat,method,vecout,redundancy,markings)
6017
6018
if markings is None:
6019
markings = list(range(1, n + 1))
6020
6021
N = len(markings)
6022
msorted = sorted(markings)
6023
markingdictionary = {i + 1: msorted[i] for i in range(N)}
6024
return prodHclass(stgraph([g], [list(range(1, N + 1))], []), [decHstratum(stgraph([g], [list(range(1, N + 1))], []), {0: (g, dat)}, [[0, markingdictionary]])])
6025
6026
6027
def Hyperell(g, n=0, m=0):
6028
"""Returns the cycle class of the hyperelliptic locus of genus g curves with n marked fixed points and m pairs of conjugate points in \\barM_{g,n+2m}.
6029
6030
TESTS::
6031
6032
sage: from admcycles import Hyperell
6033
sage: H=Hyperell(3) # long time
6034
sage: H.basis_vector() # long time
6035
(3/4, -9/4, -1/8)
6036
sage: H2 = Hyperell(1,1,1)
6037
sage: H2.forgetful_pushforward([3]).fund_evaluate()
6038
1
6039
6040
"""
6041
if n > 2 * g + 2:
6042
print('A hyperelliptic curve of genus ' + repr(g) + ' can only have ' + repr(2 * g + 2) + ' fixed points!')
6043
return 0
6044
if 2 * g - 2 + n + 2 * m <= 0:
6045
print('The moduli space \\barM_{' + repr(g) + ',' + repr(n + 2 * m) + '} does not exist!')
6046
return 0
6047
6048
G = PermutationGroup([(1, 2)])
6049
H = HurData(G, [G[1] for i in range(2 * g + 2)] + [G[0] for i in range(m)])
6050
marks = list(range(1, n + 1)) + list(range(2 * g + 2 + 1, 2 * g + 2 + 1 + 2 * m))
6051
factor = QQ(1) / factorial(2 * g + 2 - n)
6052
result = factor * Hidentify(g, H, markings=marks)
6053
6054
# currently, the markings on result are named 1,2,..,n, 2*g+3, ..., 2*g+2+2*m
6055
# shift the last set of markings down by 2*g+3-(n+1)
6056
rndict = {i: i for i in range(1, n + 1)}
6057
rndict.update({j: j - (2 * g + 2 - n) for j in range(2 * g + 2 + 1, 2 * g + 2 + 1 + 2 * m)})
6058
return result.rename_legs(rndict, inplace=False)
6059
6060
6061
def Biell(g, n=0, m=0):
6062
r"""
6063
Returns the cycle class of the bielliptic locus of genus ``g`` curves with ``n`` marked fixed points and ``m`` pairs of conjugate points in `\bar M_{g,n+2m}`.
6064
6065
TESTS::
6066
6067
sage: from admcycles import Biell
6068
sage: B=Biell(2)
6069
sage: B.basis_vector()
6070
(15/2, -9/4)
6071
sage: B1 = Biell(2,0,1)
6072
sage: B.forgetful_pullback([1]).basis_vector()
6073
(15/2, -15/2, -9/2)
6074
sage: B1.forgetful_pushforward([2]).basis_vector()
6075
(15, -15, -9)
6076
6077
"""
6078
if g == 0:
6079
print('There are no bielliptic curves of genus 0!')
6080
return 0
6081
if n > 2 * g - 2:
6082
print('A bielliptic curve of genus ' + repr(g) + ' can only have ' + repr(2 * g - 2) + ' fixed points!')
6083
return 0
6084
if 2 * g - 2 + n + 2 * m <= 0:
6085
print('The moduli space \\barM_{' + repr(g) + ',' + repr(n + 2 * m) + '} does not exist!')
6086
return 0
6087
6088
G = PermutationGroup([(1, 2)])
6089
H = HurData(G, [G[1] for i in range(2 * g - 2)] + [G[0] for i in range(m)])
6090
marks = list(range(1, n + 1)) + list(range(2 * g - 2 + 1, 2 * g - 2 + 1 + 2 * m))
6091
factor = QQ((1, factorial(2 * g - 2 - n)))
6092
if g == 2 and n == 0 and m == 0:
6093
factor /= 2
6094
result = factor * Hidentify(g, H, markings=marks)
6095
6096
# currently, the markings on result are named 1,2,..,n, 2*g-1, ..., 2*g-2+2*m
6097
# shift the last set of markings down by 2*g-1-(n+1)
6098
rndict = {i: i for i in range(1, n + 1)}
6099
rndict.update({j: j - (2 * g - 2 - n) for j in range(2 * g - 2 + 1, 2 * g - 2 + 1 + 2 * m)})
6100
return result.rename_legs(rndict, inplace=False)
6101
6102
6103
############
6104
#
6105
# Transfer maps
6106
#
6107
############
6108
6109
def Hpullpush(g, dat, alpha):
6110
"""Pulls the class alpha to the space \\bar H_{g,dat} via map i forgetting the action, then pushes forward under delta."""
6111
Hb = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])
6112
Hb = Hb * alpha
6113
if not Hb.terms:
6114
gam = trivGgraph(g, dat).quotient_graph()
6115
gd = gam.g()
6116
nd = gam.n()
6117
from .tautological_ring import TautologicalRing
6118
return TautologicalRing(gd, nd).zero()
6119
return Hb.quotient_pushforward()
6120
6121
6122
def FZreconstruct(g, n, r, moduli='st'):
6123
genind = generating_indices(g, n, r, moduli=moduli)
6124
gentob = genstobasis(g, n, r, moduli=moduli)
6125
numgens = len(gentob)
6126
M = []
6127
6128
for i in range(numgens):
6129
v = insertvec(gentob[i], numgens, genind)
6130
v[i] -= 1
6131
M.append(v)
6132
6133
return matrix(M)
6134
6135
6136
def insertvec(w, length, positions):
6137
v = vector(QQ, length)
6138
for j in range(len(positions)):
6139
v[positions[j]] = w[j]
6140
return v
6141
6142
############
6143
#
6144
# Test routines
6145
#
6146
############
6147
6148
6149
def pullpushtest(g, dat, r):
6150
r"""Test if for Hurwitz space specified by (g,dat), pulling back codimension r relations under the source map and pushing forward under the target map gives relations.
6151
"""
6152
Gr = trivGgraph(g, dat)
6153
n = Gr.gamma.n()
6154
6155
M = FZreconstruct(g, n, r)
6156
6157
N = matrix([Hpullpush(g, dat, alpha).basis_vector(r) for alpha in tautgens(g, n, r)])
6158
6159
return (N.transpose() * M.transpose()).is_zero()
6160
6161
for v in M.rows():
6162
if not v.is_zero():
6163
alpha = Tautv_to_tautclass(v, g, n, r)
6164
pb = Hpullpush(g, dat, alpha)
6165
print(pb.is_zero())
6166
6167
6168
# compares intersection numbers of Pixton generators computed by Pixton's program and by own program
6169
# computes all numbers between generators for \bar M_{g,n} in degrees r, 3g-3+n-r, respectively
6170
def checkintnum(g, n, r):
6171
from .tautological_ring import TautologicalRing
6172
R = TautologicalRing(g, n)
6173
markings = tuple(range(1, n + 1))
6174
Mpixton = DR.pairing_matrix(g, r, markings)
6175
6176
strata1 = [Graphtodecstratum(Grr) for Grr in DR.all_strata(g, r, markings)]
6177
strata2 = [Graphtodecstratum(Grr) for Grr in DR.all_strata(g, 3 * g - 3 + n - r, markings)]
6178
Mself = [[s1.multiply(s2, R).evaluate() for s2 in strata2] for s1 in strata1]
6179
6180
return Mpixton == Mself
6181
6182
6183
# pulls back all generators of degree r to the boundary divisors and identifies them, checks if results are compatible with FZ-relations between generators
6184
def pullandidentify(g, n, r):
6185
markings = tuple(range(1, n + 1))
6186
M = DR.FZ_matrix(g, r, markings)
6187
Mconv = matrix([DR.convert_vector_to_monomial_basis(M.row(i), g, r, markings) for i in range(M.nrows())])
6188
6189
L = list_strata(g, n, 1)
6190
strata = DR.all_strata(g, r, markings)
6191
decst = [Graphtodecstratum(Grr) for Grr in strata]
6192
6193
for l in L:
6194
pullbacks = [((l.boundary_pullback(st)).dimension_filter()).totensorTautbasis(r, True) for st in decst]
6195
for i in range(Mconv.nrows()):
6196
vec = 0 * pullbacks[0]
6197
for j in range(Mconv.ncols()):
6198
if Mconv[i, j] != 0:
6199
vec += Mconv[i, j] * pullbacks[j]
6200
if not vec.is_zero():
6201
return False
6202
return True
6203
6204
6205
# computes all intersection numbers of generators of R^(r1)(\bar M_{g,n1}) with pushforwards of generators of R^(r2)(\bar M_{g,n2}), where n1<n2
6206
# compares with result when pulling back and intersecting
6207
# condition: r1 + r2- (n2-n1) = 3*g-3+n1
6208
def pushpullcompat(g, n1, n2, r1):
6209
# global cu
6210
# global cd
6211
6212
r2 = 3 * g - 3 + n2 - r1
6213
6214
from .tautological_ring import TautologicalRing
6215
Rdown = TautologicalRing(g, n1)
6216
Rup = TautologicalRing(g, n2)
6217
strata_down = Rdown.generators(r1)
6218
strata_up = Rup.generators(r2)
6219
6220
fmarkings = list(range(n1 + 1, n2 + 1))
6221
6222
for class_down in strata_down:
6223
for class_up in strata_up:
6224
intersection_down = (class_down * class_up.forgetful_pushforward(fmarkings)).evaluate()
6225
intersection_up = (class_down.forgetful_pullback(fmarkings) * class_up).evaluate()
6226
6227
if not intersection_down == intersection_up:
6228
return False
6229
return True
6230
6231
6232
def deltapullpush(g, H, r):
6233
r"""
6234
Test that delta-pullback composed with delta-pushforward is multiplication with delta-degree
6235
for classes in R^r(\bar M_{g',b}) on space of quotient curves for Hurwitz data (g, H).
6236
6237
TESTS::
6238
6239
sage: from admcycles.admcycles import deltapullpush, HurData
6240
sage: G = SymmetricGroup(2)
6241
sage: H = HurData(G,[G[1],G[1],G[0]])
6242
sage: deltapullpush(2, H, 2)
6243
True
6244
"""
6245
trivGg = trivGgraph(g, H)
6246
trivGclass = Htautclass([Hdecstratum(trivGg)])
6247
ddeg = trivGg.delta_degree(0)
6248
quotgr = trivGg.quotient_graph()
6249
gprime = quotgr.genera(0)
6250
bmarkings = quotgr.list_markings()
6251
6252
from .tautological_ring import TautologicalRing
6253
R = TautologicalRing(gprime, bmarkings)
6254
gens = R.generators(r)
6255
6256
for cl in gens:
6257
clpp = trivGclass.quotient_pullback(cl).quotient_pushforward()
6258
if not (clpp.vector(r) == ddeg * cl.vector(r)):
6259
return False
6260
6261
# print 'The class \n'+repr(cl)+'\npulls back to\n'+repr(trivGclass.quotient_pullback(cl))+'\nunder delta*.\n'
6262
return True
6263
6264
6265
# Lambda-classes must satisfy this:
6266
def lambdaintnumcheck(g):
6267
from .tautological_ring import TautologicalRing
6268
R = TautologicalRing(g)
6269
intnum = ((R.lambdaclass(g) * R.lambdaclass(g - 1)).simplified() * R.lambdaclass(g - 2)).evaluate()
6270
result = QQ((-1, 2)) / factorial(2 * (g - 1)) * bernoulli(2 * (g - 1)) / (2 * (g - 1)) * bernoulli(2 * g) / (2 * g)
6271
return intnum == result
6272
6273
6274
@cached_function
6275
def op_subset_space(g, n, r, moduli):
6276
r"""
6277
Returns a vector space W over QQ which is the quotient W = V / U, where
6278
V = space of vectors representing classes in R^r(\Mbar_{g,n}) in a basis
6279
U = subspace of vectors representing classes supported outside the open subset
6280
of \Mbar_{g,n} specified by moduli (= 'sm', 'rt', 'ct' or 'tl')
6281
Thus for v in V, the vector W(v) is the representation of the class represented by v
6282
in a basis of R^r(M^{moduli}_{g,n}).
6283
"""
6284
from .tautological_ring import TautologicalRing
6285
L = TautologicalRing(g, n).generators(r)
6286
Msm = [(0 * L[0]).basis_vector(r)] # start with zero vector to get dimensions right later
6287
6288
moduli = get_moduli(moduli)
6289
6290
for t in L:
6291
gamma = next(iter(t._terms)) # we have a unique graph
6292
if gamma.vanishes(moduli):
6293
# t is supported on a graph outside the locus specified by moduli
6294
Msm.append(t.basis_vector())
6295
6296
MsmM = matrix(QQ, Msm)
6297
U = MsmM.row_space()
6298
W = U.ambient_vector_space() / U
6299
return W
6300
6301
6302
############
6303
#
6304
# Further doctests
6305
#
6306
############
6307
r"""
6308
EXAMPLES::
6309
6310
sage: from admcycles import *
6311
sage: from admcycles.admcycles import pullpushtest, deltapullpush, checkintnum, pullandidentify, pushpullcompat, lambdaintnumcheck
6312
sage: G=PermutationGroup([(1,2)])
6313
sage: H=HurData(G,[G[1],G[1]])
6314
sage: pullpushtest(2,H,1)
6315
True
6316
sage: pullpushtest(2,H,2) # long time
6317
True
6318
sage: deltapullpush(2,H,1)
6319
True
6320
sage: deltapullpush(2,H,2) # long time
6321
True
6322
sage: G=PermutationGroup([(1,2,3)])
6323
sage: H=HurData(G,[G[0]])
6324
sage: c=Hidentify(1,H)
6325
sage: c.basis_vector()
6326
(5, -3, 2, 2, 2)
6327
sage: checkintnum(2,0,1)
6328
True
6329
sage: checkintnum(2,1,2)
6330
True
6331
sage: pullandidentify(2,1,2)
6332
True
6333
sage: pullandidentify(3,0,2)
6334
True
6335
sage: pushpullcompat(2,0,1,2) # long time
6336
True
6337
sage: lambdaintnumcheck(2)
6338
True
6339
sage: lambdaintnumcheck(3) # long time
6340
True
6341
"""
6342
6343