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