unlisted
ubuntu2004# -*- coding: utf-8 -*-12from copy import deepcopy3import itertools4import os5import pickle6try:7from collections.abc import Iterable8except ImportError:9from collections import Iterable1011from sage.env import DOT_SAGE1213from sage.structure.sage_object import SageObject14from sage.arith.all import factorial, bernoulli, gcd, lcm, multinomial15from sage.arith.misc import binomial16from sage.groups.perm_gps.permgroup import PermutationGroup17from sage.matrix.constructor import matrix18from sage.matrix.special import block_matrix19from sage.combinat.all import Permutations, Subsets, Partitions20from sage.misc.cachefunc import cached_function21from sage.misc.misc_c import prod22from sage.misc.flatten import flatten23from sage.modules.free_module_element import vector24from sage.rings.all import Integer, Rational, QQ, ZZ25from sage.modules.free_module import span2627from .moduli import MODULI_ST, get_moduli, socle_degree28from .stable_graph import StableGraph, GraphIsom29from . import DR30from . import file_cache3132initialized = True33g = Integer(0)34n = Integer(3)353637def reset_g_n(gloc, nloc):38from .superseded import deprecation39deprecation(109, 'reset_g_n is deprecated. Please use TautologicalRing instead.')40gloc = Integer(gloc)41nloc = Integer(nloc)42if gloc < 0 or nloc < 0:43raise ValueError44global g, n45g = gloc46n = nloc474849Hdatabase = {}5051# stgraph is a class modelling stable graphs G52# all vertices and all legs are uniquely globally numbered by integers, vertices numbered by 1,2...,m53# G.genera = list of genera of m vertices54# G.legs = list of length m, where ith entry is set of legs attached to vertex i55# CONVENTION: usually legs are labelled 1,2,3,4,....56# G.edges = edges of G, given as list of ordered 2-tuples, sorted in ascending order by smallest element57# TODO: do I really need sorted in ascending order ...58#59# G.g() = total genus of G6061# Examples62# --------63# Creating a stgraph with two vertices of genera 3,5 joined64# by an edge with a self-loop at the genus 3 vertex.65# sage: stgraph([3,5],[[1,3,5],[2]],[(1,2),(3,5)])66# [3, 5] [[1, 3, 5], [2]] [(1, 2), (3, 5)]6768stgraph = StableGraph697071def trivgraph(g, n):72"""73Return the trivial graph in genus `g` with `n` markings.7475EXAMPLES::7677sage: from admcycles.admcycles import trivgraph78sage: trivgraph(1,1)79[1] [[1]] []80"""81return stgraph([g], [list(range(1, n + 1))], [])828384# gives a list of dual graphs for \bar M_{g,n} of codimension r, i.e. with r edges85# TODO: Huge potential for optimization, at the moment consecutively degenerate and check for isomorphisms86def list_strata(g, n, r):87if r == 0:88return [stgraph([g], [list(range(1, n + 1))], [])]89Lold = list_strata(g, n, r - 1)90Lnew = []91for G in Lold:92for v in range(G.num_verts()):93Lnew += G.degenerations(v)94# now Lnew contains a list of all possible degenerations of graphs G with r-1 edges95# need to identify duplicates9697if not Lnew:98return []99100Ldupfree = [Lnew[0]]101count = 1102# duplicate-free list of strata103104for i in range(1, len(Lnew)):105newgraph = True106for j in range(count):107if Lnew[i].is_isomorphic(Ldupfree[j]):108newgraph = False109break110if newgraph:111Ldupfree += [Lnew[i]]112count += 1113114return Ldupfree115116# 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 graph117# 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 returned118# the function will look for previous calls in the dictionary of cached_function and restart from there119# output = (deglist,invlist)120# 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 edges121# 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)]122# a morphism A->B is of the form (DT,halfedgeimages), where123# * DT is the number of B in L(j-1) or of A in L(j+1)124# * 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 A125# invlist is a list [I0,I1,I2,...] where Ik is a list [invariants,boundaries], where126# * invariants is a list of all Gr.invariant() for Gr in Lk127# * 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))128129130@cached_function131def degeneration_graph(g, n, rmax=None):132if rmax is None:133rmax = 3 * g - 3 + n134if rmax > 3 * g - 3 + n:135rmax = 3 * g - 3 + n136137# initialize deglist and invlist138try:139deglist, invlist = degeneration_graph.cached(g, n)140r0 = 3 * g - 3 + n141except KeyError:142for r0 in range(3 * g - 3 + n, -2, -1):143try:144deglist, invlist = degeneration_graph.cached(g, n, r0)145break146except KeyError:147pass148149if r0 == -1: # function has not been called before150deglist = [[[stgraph([g], [list(range(1, n + 1))], []), (), set(), set()]]]151invlist = [[[deglist[0][0][0].invariant()], [-1, 0]]]152153for r in range(r0 + 1, rmax + 1):154deglist += [[]] # add empty list L(r)155templist = []156157# first we must degenerate all stgraphs in degree r-1 and record how the degeneration went158for i in range(len(deglist[r - 1])):159Gr = deglist[r - 1][i][0]160deg_Gr = Gr.degenerations()161for dGr in deg_Gr:162# 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_Gr163lis = tuple(range(len(deglist[r - 1][i][1])))164templist += [[dGr, dGr.halfedges(), set([(i, lis)]), set([])]]165166# now templist contains a temporary list of all possible degenerations of deglist[r-1]167# we now proceed to consolidate this list to deglist[r], combining isomorphic entries168def inva(o):169return o[0].invariant()170templist.sort(key=inva)171172# first we reconstruct the blocks of entries having the same graph invariants173current_invariant = None174tempbdry = []175tempinv = []176count = 0177for Gr in templist:178inv = Gr[0].invariant()179if inv != current_invariant:180tempinv += [inv]181tempbdry += [count - 1]182current_invariant = inv183count += 1184tempbdry += [len(templist) - 1]185186# now go through blocks and check for actual isomorphisms187count = 0 # counts the number of isomorphism classes already added to deglist[r]188189invlist += [[[], []]]190for i in range(len(tempinv)):191# go through templist[tempbdry[i]+1], ..., templist[tempbdry[i+1]]192isomcl = []193for j in range(tempbdry[i] + 1, tempbdry[i + 1] + 1):194Gr = templist[j][0] # must check if stgraph Gr is isomorphic to any of the graphs in isomcl195new_graph = True196for k in range(len(isomcl)):197Gr2 = isomcl[k]198ans, Iso = Gr.is_isomorphic(Gr2[0], certificate=True)199if ans:200new_graph = False201dicl = Iso[1]202# modify isomcl[k] to add new graph upstairs, and record this isomorphism in the data of this upstairs graph203upstairs = list(templist[j][2])[0][0] # number of graph upstairs204# 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 Gr2205# and we record the index of this image leg in the list of half-edges of Gr2206lisnew = tuple((Gr2[1].index(dicl[h]) for h in deglist[r - 1][upstairs][1]))207Gr2[2].add((upstairs, lisnew))208deglist[r - 1][upstairs][3].add((count + k, lisnew))209break210if new_graph:211isomcl += [templist[j]]212# now also add the info upstairs213upstairs = list(templist[j][2])[0][0]214lisnew = list(templist[j][2])[0][1]215deglist[r - 1][upstairs][3].add((count + len(isomcl) - 1, lisnew))216217deglist[r] += isomcl218invlist[r][0] += [isomcl[0][0].invariant()]219invlist[r][1] += [count - 1]220221count += len(isomcl)222223invlist[r][1] += [count - 1]224225# remove old entry from cache_function dict if more has been computed now226if rmax > r0 and r0 != -1:227degeneration_graph.cache.pop(((g, n, r0), ()))228229return (deglist, invlist)230231# finds Gr in the degeneration_graph of the corresponding (g,n), where markdic is a dictionary sending markings of Gr to 1,2,..,n232# returns a tuple (r,i,dicv,dicl), where233# * the graph isomorphic to Gr is in degeneration_graph(g,n)[0][r][i][0]234# * dicv, dicl are dictionaries giving the morphism of stgraphs from Gr to degeneration_graph(g,n)[0][r][i][0]235236237def deggrfind(Gr, markdic=None):238# global Graph1239# global Graph2240# global Grdeggrfind241# Grdeggrfind = deepcopy(Gr)242g = Gr.g()243n = len(Gr.list_markings())244r = Gr.num_edges()245246if markdic is not None:247Gr_rename = Gr.copy(mutable=True)248Gr_rename.rename_legs(markdic)249else:250markdic = {i: i for i in range(1, n + 1)}251Gr_rename = Gr252Gr_rename.set_immutable()253Inv = Gr_rename.invariant()254(deglist, invlist) = degeneration_graph(g, n, r)255256InvIndex = invlist[r][0].index(Inv)257for i in range(invlist[r][1][InvIndex] + 1, invlist[r][1][InvIndex + 1] + 1):258# (Graph1,Graph2)= (Gr_rename,deglist[r][i][0])259ans, Isom = Gr_rename.is_isomorphic(deglist[r][i][0], certificate=True)260if ans:261dicl = {l: Isom[1][markdic[l]] for l in markdic}262dicl.update({l: Isom[1][l] for l in Gr.halfedges()})263return (r, i, Isom[0], dicl)264print('Help, cannot find ' + repr(Gr))265print((g, n, r))266print((Inv, list_strata(2, 2, 0)[0].invariant()))267268269# returns the list of all A-structures on Gamma, for two stgraphs A,Gamma270# the output is a list [(dicv1,dicl1),(dicv2,dicl2),...], where271# * dicv are (surjective) dictionaries from the vertices of Gamma to the vertices of A272# * dicl are (injective) dictionaries from the legs of A to the legs of Gamma273# optional input: identGamma, identA of the form (rA,iA,dicvA,diclA) are result of deggrfind(A,markdic)274# TODO: for now assume that markings are 1,2,...,n275def Astructures(Gamma, A, identGamma=None, identA=None):276if A.num_edges() > Gamma.num_edges():277return []278g = Gamma.g()279mark = Gamma.list_markings()280n = len(mark)281282if g != A.g():283print('Error, Astructuring graphs of different genera')284print(Gamma)285print(A)286raise ValueError('A very specific bad thing happened')287return []288if set(mark) != set(A.list_markings()):289print('Error, Astructuring graphs with different markings')290return []291markdic = {mark[i - 1]: i for i in range(1, n + 1)}292293# first identify A and Gamma inside the degeneration graph294(deglist, invlist) = degeneration_graph(g, n, Gamma.num_edges())295296if identA is None:297(rA, iA, dicvA, diclA) = deggrfind(A, markdic)298else:299(rA, iA, dicvA, diclA) = identA300if identGamma is None:301(rG, iG, dicvG, diclG) = deggrfind(Gamma, markdic)302else:303(rG, iG, dicvG, diclG) = identGamma304305# go through the graph in deglist, starting from Gamma going upwards the necessary amount of steps and collect all morphisms in a set306# 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])307# and i0,i1,..,im give the numbers of the half-edges in Gamma, to which the m+1 half-edges of the target correspond308309morphisms = set([(iG, tuple(range(len(deglist[rG][iG][1]))))]) # start with the identity on Gamma310311for r in range(rG, rA, -1):312new_morphisms = set()313for mor in morphisms:314# add to new_morphisms all compositions of mor with a morphism starting at the target of mor315for (tar, psi) in deglist[r][mor[0]][2]:316composition = tuple((mor[1][j] for j in psi))317new_morphisms.add((tar, composition))318morphisms = new_morphisms319320# now pick out the morphisms actually landing in position (rA,iA)321new_morphisms = set(mor for mor in morphisms if mor[0] == iA)322morphisms = new_morphisms323324# at the end we have to precompose with the automorphisms of Gamma325Auto = GraphIsom(deglist[rG][iG][0], deglist[rG][iG][0])326new_morphisms = set()327for (Autov, Autol) in Auto:328for mor in morphisms:329# now composition is no longer of the format above330# instead composition[i] is the label of the half-edge in deglist[rG][iG][0] to which half-edge number i in the target corresponds331composition = tuple((Autol[deglist[rG][iG][1][j]] for j in mor[1]))332new_morphisms.add((mor[0], composition))333morphisms = new_morphisms334335# now we must translate everything back to the original graphs Gamma,A and their labels,336# also reconstructing the vertex-action from the half-edge action337finalmorphisms = []338dicmarkings = {h: h for h in mark}339Ahalfedges = A.halfedges()340dicAtild = {deglist[rA][iA][1][i]: i for i in range(len(deglist[rA][iA][1]))}341diclGinverse = {diclG[h]: h for h in Gamma.halfedges()}342343for (targ, mor) in morphisms:344dicl = {h: diclGinverse[mor[dicAtild[diclA[h]]]] for h in Ahalfedges} # final dictionary for the half-edges345dicl.update(dicmarkings)346347# now reconstruct dicv from this348if A.num_verts() == 1:349dicv = {ver: 0 for ver in range(Gamma.num_verts())}350else:351# Now we pay the price for not recording the morphisms on the vertices before352# we need to reconstruct it by the known morphisms of half-edges by determining the connected components of the complement of beta(H_A)353# TODO: do we want to avoid this reconstruction by more bookkeeping?354355dicv = {Gamma.vertex(dicl[h]): A.vertex(h) for h in dicl}356remaining_vertices = set(range(Gamma.num_verts())) - set(dicv)357remaining_edges = set(e for e in Gamma.edges(copy=False)358if e[0] not in dicl.values())359current_vertices = set(dicv)360361while remaining_vertices:362newcurrent_vertices = set()363for e in list(remaining_edges):364if Gamma.vertex(e[0]) in current_vertices:365vnew = Gamma.vertex(e[1])366remaining_edges.remove(e)367# if vnew is in current vertices, we don't have to do anything (already know it)368# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)369if vnew not in current_vertices:370dicv[vnew] = dicv[Gamma.vertex(e[0])]371remaining_vertices.discard(vnew)372newcurrent_vertices.add(vnew)373continue374if Gamma.vertex(e[1]) in current_vertices:375vnew = Gamma.vertex(e[0])376remaining_edges.remove(e)377# if vnew is in current vertices, we don't have to do anything (already know it)378# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)379if vnew not in current_vertices:380dicv[vnew] = dicv[Gamma.vertex(e[1])]381remaining_vertices.discard(vnew)382newcurrent_vertices.add(vnew)383current_vertices = newcurrent_vertices384# now dicv and dicl are done, so they are added to the list of known A-structures385finalmorphisms += [(dicv, dicl)]386return finalmorphisms387388389# find a list commdeg of generic (G1,G2)-stgraphs G3390# Elements of this list will have the form (G3,vdict1,ldict1,vdict2,ldict2), where391# * G3 is a stgraph392# * vdict1, vdict2 are the vertex-maps from vertices of G3 to G1 and G2393# * ldict1, ldict2 are the leg-maps from legs of G1 and G2 to G3394# modiso = True means we give a list of generic (G1,G2)-stgraphs G3 up to isomorphisms of G3395# 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 faster396def common_degenerations(G1, G2, modiso=False, rename=False):397# TODO: fancy graph-theoretic implementation in case that G1 has a single edge?398399# almost brute force implementation below400g = G1.g()401mkings = G1.list_markings()402n = len(mkings)403404# for convenience, we want r1 <= r2405switched = False406if G1.num_edges() > G2.num_edges():407temp = G1408G1 = G2409G2 = temp410switched = True411412if G1.num_edges() == 0 and modiso:413# G1 is trivial graph, we want to avoid having to compute entire degeneration tree here414if switched:415return [(G2, {v: v for v in range(G2.num_verts())}, {l: l for l in G2.leglist()}, {v: 0 for v in range(G2.num_verts())}, {l: l for l in mkings})]416else:417return [(G2, {v: 0 for v in range(G2.num_verts())}, {l: l for l in mkings}, {v: v for v in range(G2.num_verts())}, {l: l for l in G2.leglist()})]418419if rename:420markdic = {mkings[i]: i + 1 for i in range(n)}421renamedicl1 = {l: l + n + 1 for l in G1.leglist()}422renamedicl2 = {l: l + n + 1 for l in G2.leglist()}423renamedicl1.update(markdic)424renamedicl2.update(markdic)425426G1 = G1.relabel({}, renamedicl1, mutable=False)427G2 = G2.relabel({}, renamedicl2, mutable=False)428429(r1, i1, dicv1, dicl1) = deggrfind(G1)430(r2, i2, dicv2, dicl2) = deggrfind(G2)431432(deglist, invlist) = degeneration_graph(g, n, r1 + r2)433434commdeg = []435436descendants1 = set([i1])437for r in range(r1 + 1, r2 + 1):438descendants1 = set(d[0] for i in descendants1439for d in deglist[r - 1][i][3])440441descendants2 = set([i2])442443for r in range(r2, r1 + r2 + 1):444# look for intersection of descendants1, descendants2 and find generic (G1,G2)-struct445for i in descendants1.intersection(descendants2):446Gamma = deglist[r][i][0]447Gammafind = (r, i, {j: j for j in range(Gamma.num_verts())}, {l: l for l in Gamma.leglist()})448G1structures = Astructures(Gamma, G1, identGamma=Gammafind, identA=(r1, i1, dicv1, dicl1))449G2structures = Astructures(Gamma, G2, identGamma=Gammafind, identA=(r2, i2, dicv2, dicl2))450451if modiso is True:452AutGamma = GraphIsom(Gamma, Gamma)453AutGammatild = [(dicv, {dicl[l]:l for l in dicl}) for (dicv, dicl) in AutGamma]454tempdeg = []455456# now need to identify the generic G1,G2-structures457numlegs = len(Gamma.leglist())458for (dv1, dl1) in G1structures:459for (dv2, dl2) in G2structures:460numcoveredlegs = len(set(list(dl1.values()) + list(dl2.values())))461if numcoveredlegs == numlegs:462if switched:463if rename:464tempdeg.append((Gamma, dv2, {l: dl2[renamedicl2[l]] for l in renamedicl2}, dv1, {465l: dl1[renamedicl1[l]] for l in renamedicl1}))466else:467tempdeg.append((Gamma, dv2, dl2, dv1, dl1))468else:469if rename:470tempdeg.append((Gamma, dv1, {l: dl1[renamedicl1[l]] for l in renamedicl1}, dv2, {471l: dl2[renamedicl2[l]] for l in renamedicl2}))472else:473tempdeg.append((Gamma, dv1, dl1, dv2, dl2))474475if modiso:476# eliminate superfluous isomorphic elements from tempdeg477while tempdeg:478(Gamma, dv1, dl1, dv2, dl2) = tempdeg.pop(0)479commdeg.append((Gamma, dv1, dl1, dv2, dl2))480for (dicv, dicltild) in AutGammatild:481try:482tempdeg.remove((Gamma, {v: dv1[dicv[v]] for v in dicv}, {l: dicltild[dl1[l]] for l in dl1}, {483v: dv2[dicv[v]] for v in dicv}, {l: dicltild[dl2[l]] for l in dl2}))484except ValueError:485pass486else:487commdeg += tempdeg488489# (except in last step) compute new descendants490if r < r1 + r2:491descendants1 = set(d[0] for i in descendants1 for d in deglist[r][i][3])492descendants2 = set(d[0] for i in descendants2 for d in deglist[r][i][3])493494return commdeg495496497# Gstgraph is a class modelling stable graphs Gamma with action of a group G and character data at legs498#499# Gamma.G = finite group acting on Gamma500# Gamma.gamma = underlying stable graph gamma of type stgraph501# Gamma.vertact and G.legact = dictionary, assigning to tuples (g,x) of g in G and x a vertex/leg a new vertex/leg502# Gamma.character = dictionary, assigning to leg l a tuple (h,e,k) of503# * a generator h in G of the stabilizer of l504# * the order e of the stabilizer505# * 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)506507class Gstgraph:508def __init__(self, G, gamma, vertact, legact, character, hdata=None):509self.G = G510self.gamma = gamma511self.vertact = vertact512self.legact = legact513self.character = character514if hdata is None:515self.hdata = self.hurwitz_data()516else:517self.hdata = hdata518519def copy(self, mutable=True):520G = Gstgraph.__new__(Gstgraph)521G.G = self.G522G.gamma = self.gamma.copy(mutable=mutable)523G.vertact = self.vertact.copy()524G.legact = self.legact.copy()525G.character = self.character.copy()526return G527528def __repr__(self):529return repr(self.gamma)530531def __deepcopy__(self, memo):532raise RuntimeError("do not deepcopy")533return Gstgraph(self.G, deepcopy(self.gamma), deepcopy(self.vertact), deepcopy(self.legact), deepcopy(self.character), hdata=deepcopy(self.hdata))534535# returns dimension of moduli space at vertex v; if v is None, return dimension of entire stratum for self536def dim(self, v=None):537if v is None:538return self.quotient_graph().dim()539# TODO: optimization potential540return self.extract_vertex(v).dim()541542# renames legs according to dictionary di543# TODO: no check that this renaming is legal, i.e. produces well-defined graph544def rename_legs(self, di):545if not self.gamma.is_mutable():546self.gamma = self.gamma.copy()547self.gamma.relabel({}, di, inplace=True)548temp_legact = {}549temp_character = {}550for k in self.legact:551# 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 otherwise552temp_legact[(k[0], di.get(k[1], k[1]))] = di.get(self.legact[k], self.legact[k])553for k in self.character:554temp_character[di.get(k, k)] = self.character[k]555556# returns the stabilizer group of vertex i557def vstabilizer(self, i):558gen = []559for g in self.G:560if self.vertact[(g, i)] == i:561gen += [g]562try:563return self.G.ambient_group().subgroup(gen)564except AttributeError:565return self.G.subgroup(gen)566567# returns the stabilizer group of leg j568def lstabilizer(self, j):569try:570return self.G.ambient_group().subgroup([self.character[j][0]])571except AttributeError:572return self.G.subgroup([self.character[j][0]])573574# converts self into a prodHclass (with gamma0 being the corresponding trivial graph)575576def to_prodHclass(self):577return prodHclass(stgraph([self.gamma.g()], [list(self.gamma.list_markings())], []), [self.to_decHstratum()])578579# converts self into a decHstratum (with gamma0 being the corresponding trivial graph)580def to_decHstratum(self):581dicv = {}582dicvinv = {}583dicl = {}584quotgr = self.quotient_graph(dicv, dicvinv, dicl)585586spaces = {v: (self.gamma.genera(dicvinv[v]), HurwitzData(self.vstabilizer(dicvinv[v]), [587self.character[l] for l in quotgr.legs(v, copy=False)])) for v in range(quotgr.num_verts())}588589masterdicl = {}590591for v in range(quotgr.num_verts()):592n = 1593gord = spaces[v][1].G.order()594tGraph = trivGgraph(*spaces[v])595vinv = dicvinv[v]596597# for l in quotgr.legs(v) find one leg l' in the orbit of l which belongs to vinv598legreps = []599for l in quotgr.legs(v, copy=False):600for g in self.G:601if self.legact[(g, l)] in self.gamma.legs(vinv, copy=False):602legreps.append(self.legact[(g, l)])603break604605# make a list quotrep of representatives of self.G / spaces[v][1].G (quotient by Stab(vinv))606(quotrep, di) = leftcosetaction(self.G, spaces[v][1].G)607608for l in legreps:609for g in spaces[v][1].G:610for gprime in quotrep:611masterdicl[self.legact[(gprime * g, l)]] = tGraph.legact[(g, n)]612n += ZZ(gord) // self.character[l][1]613614vertdata = []615for w in range(self.gamma.num_verts()):616a = dicv[w]617618wdicl = {l: masterdicl[l] for l in self.gamma.legs(w, copy=False)}619620vertdata.append([a, wdicl])621622return decHstratum(self.gamma.copy(mutable=False), spaces, vertdata)623624# 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_i625def extract_vertex(self, i):626G_new = self.vstabilizer(i)627lgs = self.gamma.legs(i, copy=True) # takes list of legs attached to i628egs = self.gamma.edges_between(i, i)629gamma_new = stgraph([self.gamma.genera(i)], [lgs], egs)630631vertact_new = {}632for g in G_new:633vertact_new[(g, 0)] = 0634635legact_new = {}636for g in G_new:637for j in lgs:638legact_new[(g, j)] = self.legact[(g, j)]639640character_new = {}641for j in lgs:642character_new[j] = self.character[j] # self.character[j] is a tuple, so no deepcopy is required643644return Gstgraph(G_new, gamma_new, vertact_new, legact_new, character_new)645646# glues the Gstgraph Gr (with group H) at the vertex i of a Gstgraph self647# and performs this operation equivariantly under the G-action on self648# all legs at the elements of the orbit of i are not renamed and the G-action and character on them is not changed649# 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)650# 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 self651# necessary conditions:652# * the stabilizer of vertex i in self = H653# * every leg of i is also a leg in Gr654# 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 name655def equivariant_glue_vertex(self, i, Gr, divGr={}, divs={}, dil={}):656divGr.clear()657divs.clear()658dil.clear()659660for j in range(self.gamma.num_verts()):661divs[j] = j662663G = self.G664H = Gr.G665666# the orbit of vertex i is given by gH*i for g in L667(L, di) = leftcosetaction(G, H)668oldvertno = self.gamma.num_verts() - len(L) # number of old vertices that will remain in end669670# first we rename every leg in Gr that is not a leg of i such that the new leg-names do not appear in self671# TODO: to be changed (access to private _maxleg attribute)672Gr_mod = Gr.copy()673m = max(self.gamma._maxleg, Gr.gamma._maxleg)674675a = set().union(*Gr.gamma.legs(copy=False)) # legs of Gr676b = set(self.gamma.legs(i, copy=False)) # legs of self at vertex i677e = a - b # contains the set of legs of Gr that are not legs of vertex i678augment_dic = {}679for l in e:680m += 1681augment_dic[l] = m682683Gr_mod.rename_legs(augment_dic)684685# collects the dictionaries translating legs in Gr_mod not belonging to vertex i to legs in the glued graph at elements g in L686legdiclist = {}687688# records orbit of i under elements of L689orbi = {}690oldlegsi = self.gamma.legs(i, copy=False)691692# go through the orbit of i693for g in L:694orbi[g] = self.vertact[(g, i)]695# create a translated copy of Gr using the information of the G-action on self696Gr_transl = Gr_mod.copy()697transl_dic = {l: self.legact[(g, l)] for l in oldlegsi}698Gr_transl.rename_legs(transl_dic)699700# glue G_transl to vertex g*i of self (use divs to account for deletions in meantime) and remember how internal edges are relabelled701divGr_temp = {}702divs_temp = {}703dil_temp = {}704self.gamma.glue_vertex(divs[self.vertact[(g, i)]], Gr_transl.gamma,705divGr=divGr_temp, divs=divs_temp, dil=dil_temp)706707# update various dictionaries now708divs.pop(self.vertact[(g, i)]) # no longer needed709for j in divs:710divs[j] = divs_temp[divs[j]]711legdiclist.update({(g, j): dil_temp[j] for j in dil_temp})712713# adjust vertact714# ERROR here, from renaming vetices -> rather use vertact_reconstruct()715# for g in L:716# for h in G:717# self.vertact.pop((h,orbi[g])) # remove action of h on g*i718#719#720# for g1 in range(len(L)):721# for g2 in range(len(L)):722# for h in H:723# for j in range(len(Gr.gamma.genera)):724# # record where vertex number j in Gr(translated by g1) goes under g2*h725# self.vertact[(L[g2]*h,oldvertno+g1*len(Gr.gamma.genera)+j)]=oldvertno+di[(L[g2]*h,g1)]*len(Gr.gamma.genera)+j726727# adjust legact and character728for j in e:729for g1 in range(len(L)):730# we are adjusting here the data for the leg corresponding to j in Gr that was glued to vertex g1*i731for g2 in range(len(L)):732for h in H:733# the element g2*h*(g1)**{-1} acts on g1*i resulting in g2*h*i734# for g1 fixed, the expression g2*h*(g1)^{-1} runs through the elements of G735self.legact[(L[g2] * h * L[g1].inverse(), legdiclist[(L[g1], augment_dic[j])])736] = legdiclist[(L[g2], augment_dic[Gr.legact[(h, j)]])]737# character given by conjugation738self.character[legdiclist[(L[g1], augment_dic[j])]] = (739L[g1] * Gr.character[j][0] * L[g1].inverse(), Gr.character[j][1], Gr.character[j][2])740741self.vertact_reconstruct()742743dil.update({j: legdiclist[(L[0], augment_dic[j])] for j in e}) # here we assume that L[0]=id_G744divGr.update({j: oldvertno + j for j in range(Gr.gamma.num_verts())})745746def quotient_graph(self, dicv={}, dicvinv={}, dicl={}, relabel=False):747r"""748Computes the quotient graph of self under the group action.749750dicv, dicl are dictionaries that record the quotient map of vertices and legs,751dicvinv is a dictionary giving some section of dicv (going from vertices of752quotient to vertices of self).753If relabel=False, the legs of the quotient graph are labeled with the754label of their smallest preimage. Otherwise, the labels are normalized755to 1,...,n such that the ordering of the labels is preserved.756757EXAMPLES::758759sage: from admcycles.admcycles import trivGgraph, HurData760sage: G = PermutationGroup([(1, 2)])761sage: H = HurData(G, [G[1], G[1], G[1], G[1]])762sage: tG = trivGgraph(1, H); tG.quotient_graph()763[0] [[1, 2, 3, 4]] []764sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[0]])765sage: tG = trivGgraph(1, H); tG.quotient_graph()766[0] [[1, 2, 3, 4, 5]] []767sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[1]])768sage: tG = trivGgraph(1, H); tG.quotient_graph() # no such cover exists (RH formula)769Traceback (most recent call last):770...771ValueError: Riemann-Hurwitz formula not satisfied in this Gstgraph.772773TESTS::774775sage: from admcycles.admcycles import trivGgraph, HurData776sage: G = PermutationGroup([(1, 2, 3)])777sage: H = HurData(G, [G[1]])778sage: tG = trivGgraph(2, H); tG.quotient_graph()779[1] [[1]] []780sage: H = HurData(G, [G[1] for i in range(6)])781sage: tG = trivGgraph(1, H); tG.quotient_graph() # quotient vertex would have genus -1782Traceback (most recent call last):783...784ValueError: Riemann-Hurwitz formula not satisfied in this Gstgraph.785786Check issues related to previous bug in quotient_graph:787788sage: from admcycles.admcycles import StableGraph, HurData, trivGgraph, Htautclass, Hdecstratum789sage: gamma = StableGraph([0, 0, 1], [[1, 2, 5], [3, 6, 14], [13]], [(5, 6), (13, 14)])790sage: G = SymmetricGroup(2)791sage: H = HurData(G,[G[1],G[1],G[0]])792sage: trivGg = trivGgraph(2, H)793sage: trivGclass = Htautclass([Hdecstratum(trivGg)])794sage: A = trivGclass.quotient_pullback(gamma.to_tautological_class())795sage: all(u.Gr.quotient_graph().is_isomorphic(gamma) for u in A.terms)796True797798Check that the markings of the quotient graphs are the same for all graphs in a given stratum.799800sage: from admcycles.admcycles import HurData, list_Hstrata801sage: G = CyclicPermutationGroup(4)802sage: g = G.gen()803sage: H = HurData(G, [g, g^2, g^3, g^2])804sage: all(sorted(G.quotient_graph().list_markings()) == [1, 2, 4, 5] for G in list_Hstrata(2, H, 1))805True806"""807dicv.clear()808dicvinv.clear()809dicl.clear()810811vertlist = list(range(self.gamma.num_verts()))812leglist = []813for v in vertlist:814leglist += self.gamma.legs(v, copy=False)815816countv = 0817# compute orbits of G-action818for v in vertlist[:]:819if v in vertlist: # has not yet been part of orbit of previous point820dicvinv[countv] = v821for g in self.G:822if self.vertact[(g, v)] in vertlist:823dicv[self.vertact[(g, v)]] = countv824vertlist.remove(self.vertact[(g, v)])825countv += 1826827for l in leglist[:]:828if l in leglist: # has not yet been part of orbit of previous point829orbit = set(self.legact[(g, l)] for g in self.G)830min_l = min(orbit)831for ll in orbit:832# note that leg-names in quotient graph equal the name of one preimage in self833# and as a "canonical" choice we choose the smallest one834dicl[ll] = min_l835leglist.remove(ll)836837# create new stgraph with vertices/legs given by values of dicv, dicl838quot_leg = [[] for j in range(countv)] # list of legs of quotient839legset = set(dicl.values())840for l in legset:841quot_leg[dicv[self.gamma.vertex(l)]] += [l]842843quot_genera = []844for v in range(countv):845Gv = self.vstabilizer(dicvinv[v]).order()846# self.character[l][1] = e = order of Stab_l847b = sum([1 - QQ(1) / (self.character[l][1]) for l in quot_leg[v]])848# genus of quotient vertex by Riemann-Hurwitz formula849qgenus = ((2 * self.gamma.genera(dicvinv[v]) - 2) / QQ(Gv) + 2 - b) / QQ(2)850if not (qgenus.is_integer() and qgenus >= 0):851raise ValueError('Riemann-Hurwitz formula not satisfied in this Gstgraph.')852quot_genera += [ZZ(qgenus)]853854unused_legs = legset.copy()855quot_edges = []856for e in self.gamma.edges(copy=False):857if e[0] in unused_legs:858quot_edges += [(e[0], dicl[e[1]])]859unused_legs.remove(e[0])860unused_legs.remove(dicl[e[1]])861862quot_graph = stgraph(quot_genera, quot_leg, quot_edges, mutable=True)863if relabel:864markings = sorted(quot_graph.list_markings())865legdict = {old: new + 1 for new, old in enumerate(markings)}866max_marking = max(legdict.values())867halfedges = quot_graph.halfedges()868for h in halfedges:869assert h > max_marking870legdict[h] = h871quot_graph.relabel({}, legdict, inplace=True)872dicl_new = {l1: legdict[l2] for l1, l2 in dicl.items()}873dicl.clear()874dicl.update(dicl_new)875quot_graph.tidy_up()876quot_graph.set_immutable()877878return quot_graph879880# returns Hurwitz data corresponding to the Hurwitz space in which self is a boundary stratum881# HOWEVER: since names of legs are not necessarily canonical, there is no guarantee that they will be compatible882# i.e. self is not necessarily a degeneration of trivGgraph(g,hurwitz_data(self))883def hurwitz_data(self):884quotgr = self.quotient_graph()885marks = sorted(quotgr.list_markings())886return HurwitzData(self.G, [self.character[l] for l in marks])887888# TODO: currently ONLY works for cyclic groups G889def delta_degree(self, v):890r"""891Gives the degree of the delta-map from the Hurwitz space parametrizing the curve892placed on vertex v to the corresponding stable-maps space.893894Note: currently only works for cyclic groups G.895896EXAMPLES::897898sage: from admcycles.admcycles import trivGgraph, HurData899sage: G = PermutationGroup([(1, 2)])900sage: H = HurData(G, [G[1], G[1], G[1], G[1]])901sage: tG = trivGgraph(1, H); tG.delta_degree(0) # unique cover with 2 automorphisms9021/2903sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[0]])904sage: tG = trivGgraph(1, H); tG.delta_degree(0) # unique cover with 1 automorphism9051906sage: H = HurData(G, [G[1], G[1], G[1], G[1], G[1]])907sage: tG = trivGgraph(1, H); tG.delta_degree(0) # no such cover exists (RH formula)9080909sage: G = PermutationGroup([(1, 2, 3)])910sage: H = HurData(G, [G[1]])911sage: tG = trivGgraph(2, H); tG.delta_degree(0) # no such cover exists (repr. theory)9120913"""914if self.gamma.num_verts() > 1:915return self.extract_vertex(v).delta_degree(0)916try:917quotgr = self.quotient_graph()918except ValueError: # Riemann-Hurwitz formula not satisfied, so space empty and degree 0919return 0920gprime = quotgr.genera(0)921n = self.G.order()922923# verify that Hurwitz space is nonempty #TODO: this assumes G cyclic (like the rest of the function)924if (n == 2 and len([1 for l in quotgr.legs(0, copy=False) if self.character[l][1] == 2]) % 2) or (n > 2 and prod([self.character[l][0]**(ZZ(self.character[l][2]).inverse_mod(n)) for l in quotgr.legs(0, copy=False) if self.character[l][1] != 1]) != self.G.one()):925return 0926927# compute number of homomorphisms of pi_1(punctured base curve) to G giving correct Hurwitz datum by inclusion-exclusion type formula928sigmagcd = QQ(n) / lcm([self.character[l][1] for l in quotgr.legs(0, copy=False)])929primfac = set(sigmagcd.ceil().prime_factors())930numhom = 0931for S in Subsets(primfac):932numhom += (-1)**len(S) * (QQ(n) / prod(S))**(2 * gprime)933934# TODO: why not divide by gcd([self.character[l][1] for l in quotgr.legs(0)]) instead???935return numhom * prod([QQ(n) / self.character[l][1] for l in quotgr.legs(0, copy=False)]) / QQ(n)936937# reconstructs action of G on vertices from action on legs. Always possible for connected stable graphs938def vertact_reconstruct(self):939if self.gamma.num_verts() == 1:940# only one vertex means trivial action941self.vertact = {(g, 0): 0 for g in self.G}942else:943self.vertact = {(g, v): self.gamma.vertex(self.legact[(g, self.gamma.legs(944v, copy=False)[0])]) for g in self.G for v in range(self.gamma.num_verts())}945946def degenerations(self, dege, contracted_edge):947r"""948For a codimension 1 degeneration dege of self.quotient_graph(relabel=True) list all949degenerations [G0, ...] of self such that Gi.quotient_graph(relabel=True) is isomorphic to dege.950contracted_edge is the edge of dege that needs to be contracted to obtain951a graph that is isomorphic to self.quotient_graph(rename=True).952If dege has more than two vertices, we do not guarantee that the graphs Gi are953unique up to isomorphism.954"""955956dicv = {}957dicvinv = {}958dicl = {}959quot_Graph = self.quotient_graph(dicv=dicv, dicvinv=dicvinv, dicl=dicl, relabel=True)960961# We need for each leg of quot_Graph the preimages of this leg in self962dicl_pre = {l: [] for l in dicl.values()}963for l1, l2 in dicl.items():964dicl_pre[l2].append(l1)965966# A priory dege_contr is only isomorphic to self.quotient_graph967# We rename the edges of dege such that dege_contr IS self.quotient_graph968dege_contr = dege.copy(mutable=True)969dege_contr.contract_edge(contracted_edge)970is_iso, dicts = dege_contr.is_isomorphic(quot_Graph, certificate=True)971if not is_iso:972raise ValueError("The supplied graph and edge contraction do not give rise to a graph isomorphic to self.quotient_graph(relabel=True)")973(_, legdict) = dicts974max_leg = max(legdict.values())975legdict[contracted_edge[0]] = max_leg + 1976legdict[contracted_edge[1]] = max_leg + 2977dege = dege.relabel({}, legdict)978contracted_edge = (max_leg + 1, max_leg + 2)979980# if self has more than one vertex, extract v and see it as a Gstgraph with981# its own stabilizer group as symmetries and then split up according982# to the classification of boundary divisors in Hurwitz stacks.983# Then all possible degenerations of v are glued back in equivariantly984# to the original graph985if self.gamma.num_verts() > 1:986# determine a vertex v of self that lies in the fiber above the987# vertex of self.quotient_graph() the edge is contracted to.988v = None989v1_dege = dege.vertex(contracted_edge[0])990v2_dege = dege.vertex(contracted_edge[1])991if quot_Graph.num_verts() == 1:992# Any vertex of self is ok993v = 0994else:995# At least on of the vertices of dege adjacent to the contracted996# edge has an additional adjacent leg. The vertex of997# self.quotient_graph adjacent to the image of this leg998# is the vertex we are looking for.999l_dege = None1000for legs in (dege.legs(v1_dege, copy=False), dege.legs(v2_dege, copy=False)):1001if len(legs) <= 1:1002continue1003l_dege = [l for l in legs if l not in contracted_edge][0]1004break1005assert l_dege is not None1006v_quot = None1007for v_tmp, legs in enumerate(quot_Graph.legs(copy=False)):1008if l_dege in legs:1009v_quot = v_tmp1010break1011assert v_quot is not None1012v = None1013for v1, v2 in dicv.items():1014if v2 == v_quot:1015v = v11016break1017assert v is not None10181019v0 = self.extract_vertex(v)10201021# In theory we need to extract the corresponding subgraph from dege,1022# but we need to adjust the markings: leg i of self is mapped to1023# some leg j1 in self.quotien_graph(), but to some other leg1024# j2 in v0.quotient_graph(). Hence we need to relabel leg j1 of1025# the extracted subgraph to j2. While doing so, we need to make1026# sure that the new edge is relabled to some valid value.1027v_tmp = [v1_dege, v2_dege] if v1_dege != v2_dege else [v1_dege]1028dege_sub = dege.extract_subgraph(v_tmp, rename=False, mutable=True)[0]1029dicl_v0 = {}1030v0_quot = v0.quotient_graph(dicl=dicl_v0, relabel=True)10311032# For the relevant legs j1 in self.quotient_graph() we need a pre-1033# image i that is actually contained in v0.1034dicl_pre_v0_section = {}1035legs_v0 = flatten(v0.gamma.legs(copy=False))1036for l1, ls in dicl_pre.items():1037for l2 in ls:1038if l2 in legs_v0:1039dicl_pre_v0_section[l1] = l21040break1041lm = {l: dicl_v0[dicl_pre_v0_section[l]]1042for l in flatten(dege_sub.legs(copy=True))1043if l in dicl_pre_v0_section}1044max_leg = max(lm.values())1045lm[contracted_edge[0]] = max_leg + 11046lm[contracted_edge[1]] = max_leg + 21047dege_sub.relabel({}, lm, inplace=True)1048contracted_edge_sub = (max_leg + 1, max_leg + 2)1049# We need to remove all the edges that are not the edges of the1050# quotient of v0 or the contracted edge (those edges are the images1051# of edges between different G-images of v0).1052dege_sub._edges = v0_quot.edges() + [contracted_edge_sub]1053dege_sub.set_immutable()10541055L = v0.degenerations(dege_sub, contracted_edge_sub)1056M = [self.copy() for j in range(len(L))]1057for j in range(len(L)):1058M[j].equivariant_glue_vertex(v, L[j])1059return M10601061if self.gamma.num_verts() == 1:1062quot_degen = [dege]1063# If self has a self-loop that is connecting both vertices1064# of dege, we need to consider both possible "orientations" as1065# the G-action on each end differs by sign.1066# TODO: There is some optimization potential: If there are multiple1067# such edges, we may not need to consider all the possible combinations.1068if dege.num_verts() == 2:1069edges_between = [tuple(sorted(e)) for e in dege.edges_between(0, 1)]1070edges_between.remove(contracted_edge)1071if edges_between:1072for edges in itertools.chain.from_iterable(itertools.combinations(edges_between, r) for r in range(1, len(edges_between) + 1)):10731074lm = {}1075for l1, l2 in edges:1076lm[l1] = l21077lm[l2] = l11078quot_degen.append(dege.relabel({}, lm))1079degen = [] # list of degenerations10801081if self.G.is_cyclic():1082# first create dictionaries GtoNum, NumtoG translating elements of G to numbers 0,1, ..., G.order()-11083G = self.G1084n = G.order()10851086# cumbersome method to obtain some generator. since G.gens() is not guaranteed to be minimal, go through and check order1087Ggenerators = G.gens()1088for ge in Ggenerators:1089if ge.order() == n:1090sigma = ge1091break10921093# now sigma is fixed generator of G, want to have dictionaries such that sigma <-> 1, sigma**2 <-> 2, ..., sigma**n <-> 01094GtoNum = {}1095NumtoG = {}1096mu = sigma10971098for j in range(1, n + 1):1099GtoNum[mu] = j % n1100NumtoG[j % n] = mu1101mu = mu * sigma11021103# extract data of e_alpha, k_alpha, nu_alpha, where alpha runs through legs of dege1104e = {}1105k = {}1106nu = {}11071108for dege in quot_degen:1109# all legs in dege come from legs of self, except the one added by the degeneration1110legs = set(dege.leglist())1111legs.difference_update(contracted_edge)1112for alpha in legs:1113alpha_pre = dicl_pre[alpha][0]1114e[alpha] = self.character[alpha_pre][1]1115r = (GtoNum[self.character[alpha_pre][0]] * e[alpha] / QQ(n)).floor()1116k[alpha] = (self.character[alpha_pre][2] * r.inverse_mod(e[alpha])) % e[alpha]1117nu[alpha] = k[alpha].inverse_mod(e[alpha]) # case e[alpha]=1 works, produces nu[alpha]=011181119if dege.num_verts() == 2:1120I1 = dege.legs(0, copy=True)1121I2 = dege.legs(1, copy=True)1122if contracted_edge[0] in I1:1123I1.remove(contracted_edge[0])1124I2.remove(contracted_edge[1])1125else:1126I1.remove(contracted_edge[1])1127I2.remove(contracted_edge[0])1128I = I1 + I211291130n1_min = lcm([e[alpha] for alpha in I1])1131n2_min = lcm([e[alpha] for alpha in I2])11321133# TODO: following for-loop has great optimization potential, but current form should be sufficient for now1134for n1_add in range(1, ZZ(n) // n1_min + 1):1135for n2_add in range(1, ZZ(n) // n2_min + 1):1136n1 = n1_add * n1_min # nj is the order of the stabilizer of the preimage of vertex j-1 in dege1137n2 = n2_add * n2_min1138# lcm condition from connectedness of final graph1139if n1.divides(n) and n2.divides(n) and lcm(n1, n2) == n:1140# we must make sure that the order e of the stabilizer of the preimage of the separating edge and the1141# corresponding characters (described by k1,k2) at the legs produce nonempty Hurwitz spaces1142# this amounts to a congruence relation mod n1, n2, which can be explicitly solved1143A1 = -QQ(sum([QQ(n1) / e[alpha] * nu[alpha] for alpha in I1])).floor()1144e_new = ZZ(n1) // n1.gcd(A1)1145nu1 = (QQ(A1 % n1) / (QQ(n1) / e_new)).floor()1146nu2 = e_new - nu11147k1 = nu1.inverse_mod(e_new)1148k2 = nu2.inverse_mod(e_new)11491150# now we list all ways to distribute the markings, avoiding duplicates by symmetries1151I1_temp = I1[:]1152I2_temp = I2[:]1153# dictionary associating to a leg l in I the possible positions of l (from 0 to n/n1-1 if l in I1, ...)1154possi = {}11551156if I1:1157possi[I1[0]] = [0] # use G-action to move marking I1[0] to zeroth vertex on the left1158I1_temp.pop(0)1159if I2: # if there is one more leg on the other vertex, use stabilizer of zeroth left vertex to move1160possi[I2[0]] = list(range(gcd(n1, n // n2)))1161I2_temp.pop(0)1162else:1163if I2:1164possi[I2[0]] = [0]1165I2_temp.pop(0)1166for i in I1_temp:1167possi[i] = list(range(n // n1))1168for i in I2_temp:1169possi[i] = list(range(n // n2))11701171# mark_dist is a list of the form [(v1,v2,...), ...] where leg I[0] goes to vertex number v1 in its orbit, ....1172mark_dist = itertools.product(*[possi[j] for j in I])11731174# now all choices are made, and we construct the degenerated graph, to add it to degen1175# first: use Riemann-Hurwitz to compute the genera of the vertices above 0,11176g1 = ((n1 * (2 * dege.genera(0) - 2) + n1 *1177(sum([1 - QQ(1) / e[alpha] for alpha in I1]) + 1 - QQ(1) / e_new)) / QQ(2) + 1).floor()1178g2 = ((n2 * (2 * dege.genera(1) - 2) + n2 *1179(sum([1 - QQ(1) / e[alpha] for alpha in I2]) + 1 - QQ(1) / e_new)) / QQ(2) + 1).floor()11801181# nonemptyness condition #TODO: is this the only thing you need to require?1182if g1 < 0 or g2 < 0:1183continue11841185new_genera = [g1 for j in range(n // n1)] + [g2 for j in range(QQ(n) / n2)]1186new_legs = [[] for j in range(n // n1 + n // n2)]1187new_legact = self.legact.copy()1188new_edges = self.gamma.edges()1189new_character = self.character.copy()11901191# now go through the orbit of the edge and adjust all necessary data1192# TODO: to be changed (access to private maxleg attribute)1193m0 = self.gamma._maxleg + 11194m = self.gamma._maxleg + 111951196for j in range(ZZ(n) // e_new):1197new_legs[j % (n // n1)] += [m]1198new_legs[(j % (n // n2)) + (n // n1)] += [m + 1]1199new_edges += [(m, m + 1)]1200new_legact[(sigma, m)] = m0 + ((m + 2 - m0) % (2 * n // e_new))1201new_legact[(sigma, m + 1)] = new_legact[(sigma, m)] + 11202new_character[m] = (NumtoG[(n // e_new) % n], e_new, k1)1203new_character[m + 1] = (NumtoG[(n // e_new) % n], e_new, k2)12041205m += 212061207# take care of remaining actions of g in G-{sigma} on legs belonging to orbit of new edge1208for jplus in range(2, n + 1):1209for m in range(m0, m0 + 2 * n // e_new):1210new_legact[(NumtoG[jplus % n], m)] = new_legact[(1211sigma, new_legact[(NumtoG[(jplus - 1) % n], m)])]12121213for md in mark_dist:1214new_legs_plus = [l[:] for l in new_legs]1215for alpha in range(len(I1)):1216for j in range(n // e[I[alpha]]):1217new_legs_plus[(md[alpha] + j) % (n // n1)1218] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]1219for alpha in range(len(I1), len(I)):1220for j in range(n // e[I[alpha]]):1221new_legs_plus[(md[alpha] + j) % (n // n2) + (n // n1)1222] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]12231224# all the data is now ready to generate the new graph1225new_Ggraph = Gstgraph(G, stgraph(new_genera, new_legs_plus, new_edges), {1226}, new_legact, new_character, hdata=self.hdata)1227new_Ggraph.vertact_reconstruct()1228if not any(equiGraphIsom(G, new_Ggraph)1229for G in degen):1230degen += [new_Ggraph]1231if dege.num_verts() == 1: # case of self-loop1232I = dege.legs(0, copy=True)1233I.remove(contracted_edge[0]) # remove the legs corresponding to the degenerated edge1234I.remove(contracted_edge[1])12351236n0_min = lcm([e[alpha] for alpha in I])12371238# TODO: following for-loop has great optimization potential, but current form should be sufficient for now1239for n0_add in range(1, ZZ(n) // n0_min + 1):1240n0 = n0_add * n0_min # n0 is the order of the stabilizer of any of the vertices1241if n0.divides(n):1242for e_new in n0.divisors():1243for g0 in [x for x in range(ZZ(n) // (2 * n0) + 1) if gcd(x, n // n0) == 1]:1244for nu1 in [j for j in range(e_new) if gcd(j, e_new) == 1]:1245# g0==0 means only one vertex and then there is a symmetry inverting edges1246if g0 == 0 and nu1 > QQ(e_new) / 2:1247continue1248nu2 = e_new - nu11249k1 = Integer(nu1).inverse_mod(e_new)1250k2 = Integer(nu2).inverse_mod(e_new)12511252# now we list all ways to distribute the markings, avoiding duplicates by symmetries1253I_temp = I[:]12541255# dictionary associating to a leg l in I the possible positions of l (from 0 to n/n1-1 if l in I1, ...)1256possi = {}12571258if I:1259possi[I[0]] = [0] # use G-action to move marking I1[0] to zeroth vertex1260I_temp.pop(0)1261for i in I_temp:1262possi[i] = list(range(n // n0))12631264# mark_dist is a list of the form [(v1,v2,...), ...] where leg I[0] goes to vertex number v1 in its orbit, ....1265mark_dist = itertools.product(*[possi[j] for j in I])12661267# now all choices are made, and we construct the degenerated graph, to add it to degen1268# first: use Riemann-Hurwitz to compute the genera of the vertices above 0,11269gen0 = (1270(n0 * (2 * dege.genera(0) - 2) + n0 * (sum([1 - QQ(1) / e[alpha] for alpha in I]) + 2 - QQ(2) / e_new)) / QQ(2) + 1).floor()12711272# nonemptyness condition #TODO: is this the only thing you need to require?1273if gen0 < 0:1274continue12751276new_genera = [gen0 for j in range(n // n0)]1277new_legs = [[] for j in range(n // n0)]1278new_legact = self.legact.copy()1279new_edges = self.gamma.edges()1280new_character = self.character.copy()12811282# now go through the orbit of the edge and adjust all necessary data1283# TODO: to be changed (access to private _maxleg attribute)1284m0 = self.gamma._maxleg + 11285m = self.gamma._maxleg + 112861287for j in range(ZZ(n) // e_new):1288new_legs[j % (n // n0)] += [m]1289new_legs[(j + g0) % (n // n0)] += [m + 1]1290new_edges += [(m, m + 1)]1291new_legact[(sigma, m)] = m0 + ((m + 2 - m0) % (2 * n // e_new))1292new_legact[(sigma, m + 1)] = new_legact[(sigma, m)] + 11293new_character[m] = (NumtoG[(n // e_new) % n], e_new, k1)1294new_character[m + 1] = (NumtoG[(n // e_new) % n], e_new, k2)12951296m += 212971298# take care of remaining actions of g in G-{sigma} on legs belonging to orbit of new edge1299for jplus in range(2, n + 1):1300for m in range(m0, m0 + 2 * n // e_new):1301new_legact[(NumtoG[jplus % n], m)] = new_legact[(1302sigma, new_legact[(NumtoG[(jplus - 1) % n], m)])]13031304for md in mark_dist:1305new_legs_plus = [l[:] for l in new_legs]1306for alpha in range(len(I)):1307for j in range(n // e[I[alpha]]):1308new_legs_plus[(md[alpha] + j) % (n // n0)1309] += [self.legact[(NumtoG[j], dicl_pre[I[alpha]][0])]]13101311# all the data is now ready to generate the new graph1312new_Ggraph = Gstgraph(G, stgraph(new_genera, new_legs_plus, new_edges), {1313}, new_legact, new_character, hdata=self.hdata)1314new_Ggraph.vertact_reconstruct()1315if not any(equiGraphIsom(G, new_Ggraph)1316for G in degen):1317degen += [new_Ggraph]1318return degen1319else:1320print('Degenerations for non-cyclic groups not implemented!')1321return []13221323# returns the list of all G-isomorphisms of the Gstgraphs Gr1 and Gr21324# isomorphisms must respect markings (=legs that don't appear in edges)1325# they are given as [dictionary of images of vertices, dictionary of images of legs]1326# TODO:IDEA: first take quotients, compute isomorphisms between them, then lift??132713281329def equiGraphIsom(Gr1, Gr2):1330if Gr1.G != Gr2.G:1331return []1332Iso = GraphIsom(Gr1.gamma, Gr2.gamma) # list of all isomorphisms of underlying dual graphs1333GIso = []13341335# TODO: once Gequiv=False, can abort other loops1336# TODO: check character data!!1337for I in Iso:1338Gequiv = True1339for g in Gr1.G.gens():1340# check if isomorphism I is equivariant with respect to g: I o g = g o I1341# action on vertices:1342for v in I[0]:1343if I[0][Gr1.vertact[(g, v)]] != Gr2.vertact[(g, I[0][v])]:1344Gequiv = False1345break1346# action on legs:1347for l in I[1]:1348if I[1][Gr1.legact[(g, l)]] != Gr2.legact[(g, I[1][l])]:1349Gequiv = False1350break1351# character:1352for l in I[1]:1353if not Gequiv:1354break1355for j in [t for t in range(Gr1.character[l][1]) if gcd(t, Gr1.character[l][1]) == 1]:1356if 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:1357Gequiv = False1358break1359if Gequiv:1360GIso += [I]13611362return GIso13631364# HurwitzData models the ramification data D of a Galois cover of curves in the following way:1365#1366# D.G = finite group1367# D.l = list of elements of the form (h,e,k) corresponding to orbits of markings with1368# * a generator h in G of the stabilizer of the element p of an orbit1369# * the order e of the stabilizer1370# * 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)137113721373class HurwitzData:1374def __init__(self, G, l):1375self.G = G1376self.l = l13771378def __eq__(self, other):1379if not isinstance(other, HurwitzData):1380return False1381return (self.G == other.G) and (self.l == other.l)13821383def __hash__(self):1384return hash((self.G, tuple(self.l)))13851386def __repr__(self):1387return repr((self.G, self.l))13881389def __deepcopy__(self, memo):1390return HurwitzData(self.G, self.l.copy())13911392def nummarks(self):1393g = self.G.order()1394N = ZZ.sum(g // r[1] for r in self.l)1395return N13961397# HurData takes a group G and a list l of group elements (giving monodromy around the corresponding markings) and gives back the corresponding Hurwitz data1398# This is just a more convenient way to enter HurwitzData139914001401def HurData(G, l):1402return HurwitzData(G, [(h, h.order(), min(h.order(), 1)) for h in l])14031404# returns the trivial stable graph of genus gen with group action corresponding to the HurwitzData D140514061407def trivGgraph(gen, D):1408G = D.G14091410# graph has only one vertex with trivial G-action1411vertact = {}1412for g in D.G:1413vertact[(g, 0)] = 014141415n = 0 # counts number of legs as we go through Hurwitz data1416legact = {}1417character = {}14181419for e in D.l:1420# h=e[0] generates the stabilizer of (one element of) the orbit corresponding to e1421H = G.subgroup([e[0]])1422# markings corresponding to entry e are labelled by cosets gH and G-action is given by left-action of G1423(L, dic) = leftcosetaction(G, H)1424for g in G:1425for j in range(len(L)):1426# shift comes from fact that the n legs 1,2,...,n have already been assigned1427legact[(g, j + 1 + n)] = dic[(g, j)] + 1 + n14281429for j in range(len(L)):1430# Stabilizer at point gH generated by ghg**{-1}, of same order e, acts still by exp(2 pi i/e * k)1431character[j + 1 + n] = (L[j] * e[0] * L[j].inverse(), e[1], e[2])1432n += len(L)14331434gamma = stgraph([gen], [list(range(1, n + 1))], [])14351436return Gstgraph(G, gamma, vertact, legact, character, hdata=D)143714381439@cached_function1440def Hcovers_degeneration_graph(g, H, rmax=None):1441r"""1442Returns a nested list L where L[r][i] is a (duplicate-free) list of all Gstgraphs G for which1443G.quotient_graph(relabel=True) is isomorphic to the graph given by degeneration_graph(...)[r][i].14441445EXAMPLES::14461447sage: from admcycles.admcycles import degeneration_graph, Hcovers_degeneration_graph, HurData1448sage: G = CyclicPermutationGroup(2)1449sage: g = G.gen()1450sage: H = HurData(G, [g, g, g, g])1451sage: degGr = degeneration_graph(0, 4)[0]1452sage: Hcovs = Hcovers_degeneration_graph(1, H)1453sage: for r in range(2):1454....: for l_Ggr, (stGr, _, _, _) in zip(Hcovs[r], degGr[r]):1455....: assert all(Ggr.quotient_graph(relabel=True).is_isomorphic(stGr) for Ggr in l_Ggr)1456"""14571458g_base = trivGgraph(g, H).quotient_graph().g()1459n_base = len(H.l)14601461# initialize covermap1462if rmax is None or rmax > 3 * g_base - 3 + n_base:1463rmax = 3 * g_base - 3 + n_base14641465r0 = -11466for r0 in range(3 * g_base - 3 + n, -2, -1):1467try:1468covermap = Hcovers_degeneration_graph.cached(g, H, r0)1469break1470except KeyError:1471pass14721473if r0 == -1: # function has not been called before1474covermap = [[[trivGgraph(g, H)]]]14751476# compute the possible covers for each degeneration1477degGr = degeneration_graph(g_base, n_base, rmax)[0]14781479for r in range(max(r0 + 1, 1), rmax + 1):1480covermap.append([]) # add empty list L(r)14811482for (Gr, halfedges, edge_contractions, _) in degGr[r]:1483templist = []1484(gr_index, halfedgeimages) = next(iter(edge_contractions)) # get an arbitrary edge contraction1485# determine which edge of Gr needs to be contracted to obtain1486# the graph degGr[r-1][gr_index].1487halfedges_in_image = [halfedges[h] for h in halfedgeimages]14881489halfedges = set(Gr.halfedges())1490halfedges.difference_update(halfedges_in_image)1491assert len(halfedges) == 21492contracted_edge = tuple(sorted(halfedges))1493for cover_contracted in covermap[r - 1][gr_index]:1494for G in cover_contracted.degenerations(Gr, contracted_edge):1495if not any(equiGraphIsom(G, H) for H in templist):1496templist.append(G)1497covermap[-1].append(templist)14981499# remove old entry from cached_function dict if more has been computed now1500if rmax > r0 and r0 != -1:1501Hcovers_degeneration_graph.cache.pop(((g, H, r0), ()))15021503return covermap150415051506def list_Hstrata(g, H, r):1507r"""1508Gives a (duplicate-free) list of Gstgraphs for the Hurwitz space of genus g with HurwitzData H in codimension r.1509"""15101511if r == 0:1512return [trivGgraph(g, H)]15131514g_base = trivGgraph(g, H).quotient_graph().g()1515n_base = len(H.l)1516if r > 3 * g_base - 3 + n_base:1517return []15181519covermap = Hcovers_degeneration_graph(g, H, r)1520return flatten(covermap[r])152115221523def list_Hcovers(Gr, H, g=None):1524r"""1525For a given stabel graph list all possible H-covers.1526The markings of Gr must be [1, ..., len(H.l)].1527If g is not given, it is computed from Gr and H.15281529EXAMPLES::15301531sage: from admcycles.admcycles import list_Hcovers, HurData, stgraph, trivGgraph, equiGraphIsom1532sage: G = CyclicPermutationGroup(2)1533sage: g = G.gen()1534sage: H = HurData(G, [g, g, g, g])15351536sage: Gr = stgraph([0], [[1,2,3,4]], [])1537sage: list_Hcovers(Gr, H)1538[[1] [[1, 2, 3, 4]] []]15391540sage: Gr = stgraph([0, 0], [[1,2,5], [3,4,6]], [(5,6)])1541sage: list_Hcovers(Gr, H)1542[[0, 0] [[5, 7, 1, 2], [6, 8, 3, 4]] [(5, 6), (7, 8)]]15431544# Test that a previous bug in Gstgraph.degenerations is fixed1545sage: H = HurData(G, [g*g, g, g])1546sage: gr = stgraph([1, 0], [[4], [2, 3, 1, 5]], [(4, 5)])1547sage: list_Hcovers(gr, H)1548[[1, 1, 0] [[5], [7], [6, 8, 3, 4, 1, 2]] [(5, 6), (7, 8)],1549[1, 0] [[5, 7], [6, 8, 3, 4, 1, 2]] [(5, 6), (7, 8)]]1550"""15511552if g is None:1553d = H.G.order()1554g = (d * (2 * Gr.g() - 2) + sum(d / l[1] * (l[1] - 1) for l in H.l) + 2) / 21555assert g.is_integer()1556g = ZZ(g)15571558if Gr.num_edges() == 0:1559return [trivGgraph(g, H)]15601561# We contract an arbitrary edge, list all covers for the contracted1562# graph and degenerate those.1563contracted_edge = Gr.edges(copy=False)[0]1564Gr_contr = Gr.copy(mutable=True)1565Gr_contr.contract_edge(contracted_edge)1566return flatten([G.degenerations(Gr, contracted_edge)1567for G in list_Hcovers(Gr_contr, H, g)])156815691570# gives a list of the quotgraphs of the entries of list_Hstrata(g,H,r) and the corresponding dictionaries1571# list elements are of the form [quotient graph,deggrfind(quotient graph),dicv,dicvinv,dicl]1572# if localize=True, finds the quotient graphs in the corresponding degeneration_graph and records their index as well as an isomorphism to the standard-graph1573# then the result has the form: [quotient graph,index in degeneration_graph,dicv,dicvinv,dicl,diclinv], where1574# * dicv is the quotient map sending vertices of Gr in list_Hstrata(g,H,r) to vertices IN THE STANDARD GRAPH inside degeneration_graph1575# * dicvinv gives a section of dicv1576# * dicl sends leg names in Gr to leg names IN THE STANDARD GRAPH1577# * diclinv gives a section of dicl157815791580@cached_function1581def list_quotgraphs(g, H, r, localize=True):1582Hgr = list_Hstrata(g, H, r)1583result = []1584for gr in Hgr:1585dicv = {}1586dicvinv = {}1587dicl = {}1588qgr = gr.quotient_graph(dicv, dicvinv, dicl)1589if localize:1590dgfind = deggrfind(qgr)1591defaultdicv = dgfind[2]1592defaultdicl = dgfind[3]1593defaultdicvinv = {defaultdicv[v]: v for v in defaultdicv}1594defaultdiclinv = {defaultdicl[l]: l for l in defaultdicl}15951596result.append([qgr, dgfind[1], {v: defaultdicv[dicv[v]] for v in dicv}, {1597w: dicvinv[defaultdicvinv[w]] for w in defaultdicvinv}, {l: defaultdicl[dicl[l]] for l in dicl}, defaultdiclinv])1598else:1599result.append([qgr, dicv, dicvinv, dicl])1600return result16011602# cyclicGstgraph is a convenient method to create a Gstgraph for a cyclic group action. It takes as input1603# * a stgraph Gr1604# * a number n giving the order of the cyclic group G1605# * 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 cha1606# * 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)1607# and returns the corresponding Gstgraph1608# optionally, the generator sigma can be given as input directly and G is computed as its parent1609# Here we use that the G-action on a connected graph Gr is uniquely determined by the G-action on the legs1610# TODO: user-friendliness: allow (3) instead of (3,)161116121613def cyclicGstgraph(Gr, n, perm, cha, sigma=None):1614if sigma is None:1615G = PermutationGroup(tuple(range(1, n + 1)))1616sigma = G.gens()[0]1617else:1618G = sigma.parent()16191620# first legaction and character1621perm_dic = {} # converts action of sigma into dictionary1622character = {}16231624for cycleno in range(len(perm)):1625e = ZZ(n) // len(perm[cycleno]) # order of stabilizer1626for j in range(len(perm[cycleno])):1627perm_dic[perm[cycleno][j]] = perm[cycleno][(j + 1) % len(perm[cycleno])]1628character[perm[cycleno][j]] = (sigma**(len(perm[cycleno])), e, cha[cycleno])16291630legact = {(sigma, k): perm_dic[k] for k in perm_dic}16311632mu = sigma1633for j in range(2, n + 1):1634mu2 = mu * sigma1635legact.update({(mu2, k): perm_dic[legact[(mu, k)]] for k in perm_dic})1636mu = mu216371638# TODO: insert appropriate hdata1639# we don't bother to fill in vertact, since it is reconstructable anyway1640Gr_new = Gstgraph(G, Gr, {}, legact, character)1641Gr_new.vertact_reconstruct()16421643return Gr_new16441645# returns a tuple (L,d) of1646# * a list L =[g_0, g_1, ...] of representatives g_i of the cosets g_i H of H in G1647# * 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 H1648# TODO: assumes first element of L is id_G164916501651def leftcosetaction(G, H):1652C = G.cosets(H, side='left')1653L = [C[i][0] for i in range(len(C))]1654d = {}16551656for g in G:1657for i in range(len(C)):1658gtild = g * L[i]1659for j in range(len(C)):1660if gtild in C[j]:1661d[(g, i)] = j1662return (L, d)166316641665def tautclass(arg, g=None, n=None):1666r"""1667Construct a tautological class.16681669The return object has type :class:`admcycles.tautological_ring.TautlogicalClass`.16701671INPUT:16721673arg : a tuple or list of :class:`decstratum`16741675g : integer1676the genus16771678n : integer1679the number of marked points16801681EXAMPLES::16821683sage: from admcycles.admcycles import *16841685sage: gamma = StableGraph([1,2],[[1,2],[3]],[(2,3)])1686sage: ds1 = decstratum(gamma, kappa=[[1],[]]); ds11687Graph : [1, 2] [[1, 2], [3]] [(2, 3)]1688Polynomial : (kappa_1)_01689sage: ds2 = decstratum(gamma, kappa=[[],[1]]); ds21690Graph : [1, 2] [[1, 2], [3]] [(2, 3)]1691Polynomial : (kappa_1)_11692sage: t = tautclass([ds1, ds2])1693sage: (t - gamma.to_tautological_class() * kappaclass(1,3,1)).is_zero()1694True16951696sage: t = 7*fundclass(0,4) + psiclass(1,0,4) + 3 * psiclass(2,0,4) - psiclass(3,0,4)1697sage: s = t.FZsimplify(); s1698Graph : [0] [[1, 2, 3, 4]] []1699Polynomial : 7 + 3*(kappa_1)_01700sage: u = t.FZsimplify(r=0); u1701Graph : [0] [[1, 2, 3, 4]] []1702Polynomial : 717031704sage: t = kappaclass(1,1,1)1705sage: t.evaluate()17061/2417071708sage: t = psiclass(1,2,1)1709sage: s = t.forgetful_pushforward([1]); s1710Graph : [2] [[]] []1711Polynomial : 21712sage: s.fund_evaluate()1713217141715sage: a = psiclass(1,2,3)1716sage: t = a + a*a1717sage: t.degree_list()1718[1, 2]17191720sage: diff = kappaclass(1,3,0) - 12*lambdaclass(1,3,0)1721sage: diff.is_zero()1722False1723sage: diff.is_zero(moduli='sm')1724True17251726Non-standard markings::17271728sage: gamma = StableGraph([1],[[3,7]],[])1729sage: ds = decstratum(gamma, kappa=[[1]])1730sage: tautclass([ds])1731Graph : [1] [[3, 7]] []1732Polynomial : (kappa_1)_01733sage: tautclass([ds]).parent()1734TautologicalRing(g=1, n=(3, 7), moduli='st') over Rational Field17351736Test the ``g`` and ``n`` parameters::17371738sage: tautclass([], g=2, n=2)173901740sage: tautclass([], g=2, n=2).parent()1741TautologicalRing(g=2, n=2, moduli='st') over Rational Field1742"""1743from .tautological_ring import TautologicalRing, TautologicalClass17441745if not arg:1746if g is None or n is None:1747# NOTE: in this situation we do not return a TautologicalClass since we do not know1748# a priori the parameters g and n. Since we have proper coercions it should not be1749# a problem in most situations.1750from .superseded import deprecation1751deprecation(109, 'can not guess g, n from the input, return an integer. Please provde g and n to tautclass')1752return ZZ(0)1753return TautologicalRing(g, n).zero()17541755if isinstance(arg, TautologicalClass):1756return arg1757elif isinstance(arg, (tuple, list)):1758if g is None:1759g = arg[0].gamma.g()1760if n is None:1761n = tuple(sorted(arg[0].gamma.list_markings()))1762return TautologicalRing(g, n)(arg)1763else:1764raise TypeError('invalid input for tautclass')176517661767# TODO: storing the data as (unordered) lists is very inefficient!1768# Simpler if we had a dictionary: (monomial) -> coefficient1769# that would require a hashable version of monomial1770class KappaPsiPolynomial(SageObject):1771r"""1772Polynomial in kappa and psi-classes on a common stable graph.17731774The data is stored as a list monomials of entries (kappa,psi) and a list1775coeff of their coefficients. Here (kappa,psi) is a monomial in kappa, psi1776classes, represented as17771778- ``kappa``: list of length self.gamma.num_verts() of lists of the form [3,0,2]1779meaning that this vertex carries kappa_1**3*kappa_3**217801781- ``psi``: dictionary, associating nonnegative integers to some legs, where1782psi[l]=3 means that there is a psi**3 at this leg for a kppoly p. The values1783can not be zero.17841785If ``p`` is such a polynomial, ``p[i]`` is of the form ``(kappa,psi,coeff)``.17861787EXAMPLES::17881789sage: from admcycles.admcycles import kppoly1790sage: p1 = kppoly([([[0, 1], [0, 0, 2]], {1:2, 2:3}), ([[], [0, 1]], {})], [-3, 5])1791sage: p11792-3*(kappa_2)_0*(kappa_3^2)_1*psi_1^2*psi_2^3 + 5*(kappa_2)_11793"""17941795def __init__(self, monom, coeff):1796self.monom = monom1797self.coeff = coeff1798assert len(self.monom) == len(self.coeff)17991800def __bool__(self):1801return bool(self.monom)18021803def copy(self):1804r"""1805TESTS::18061807sage: from admcycles.admcycles import kppoly1808sage: p = kppoly([([[], [0, 1]], {0:3, 1:2})], [5])1809sage: p.copy()18105*(kappa_2)_1*psi_0^3*psi_1^21811"""1812res = KappaPsiPolynomial.__new__(KappaPsiPolynomial)1813res.monom = [([x[:] for x in k], p.copy()) for k, p in self.monom]1814res.coeff = self.coeff[:]1815return res18161817def __iter__(self): # TODO fix1818return iter([self[i] for i in range(len(self))])18191820def __getitem__(self, i):1821return self.monom[i] + (self.coeff[i],)18221823def __len__(self):1824return len(self.monom)18251826def __neg__(self):1827return (-1) * self18281829def __add__(self, other): # TODO: Add __radd__1830r"""1831TESTS:18321833Check that mutable data are not shared between the result and the terms::18341835sage: from admcycles.admcycles import kappacl, psicl1836sage: A = kappacl(0,2,2)1837sage: B = psicl(3,2)1838sage: C = A + B1839sage: C.monom[1][1].update({2:3})1840sage: A1841(kappa_2)_01842sage: B1843psi_31844"""1845if other == 0:1846return self.copy()1847new = self.copy()1848for (k, p, c) in other:1849try:1850ind = new.monom.index((k, p))1851new.coeff[ind] += c1852if new.coeff[ind] == 0:1853new.monom.pop(ind)1854new.coeff.pop(ind)1855except ValueError:1856if c != 0:1857new.monom.append((k[:], p.copy()))1858new.coeff.append(c)1859return new18601861# returns the degree of the ith term of self (default: i=0). If self is empty, give back None1862def deg(self, i=0):1863if not self.monom:1864return None1865else:1866(kappa, psi) = self.monom[i]1867return sum([sum((j + 1) * kvec[j] for j in range(len(kvec))) for kvec in kappa]) + sum(psi.values())18681869# computes the pullback of self under a graph morphism described by dicv, dicl1870# returns a kppoly on this graph1871def graphpullback(self, dicv, dicl):1872if len(self.monom) == 0:1873return kppoly([], [])18741875numvert_self = len(self.monom[0][0])1876numvert = len(dicv)18771878preim = [[] for i in range(numvert_self)]1879for v in dicv:1880preim[dicv[v]].append(v)18811882resultpoly = kppoly([], [])1883for (kappa, psi, coeff) in self:1884# pullback of psi-classes1885psipolydict = {dicl[l]: psi[l] for l in psi}1886psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])18871888# pullback of kappa-classes1889kappapoly = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim[v]])**kappa[v][k]1890for k in range(len(kappa[v]))]) for v in range(numvert_self)])18911892resultpoly += coeff * psipoly * kappapoly1893return resultpoly18941895def rename_legs(self, dic):1896r"""1897Rename the legs according to ``dic``.1898"""1899for count in range(len(self.monom)):1900self.monom[count] = (self.monom[count][0], {dic.get(1901l, l): self.monom[count][1][l] for l in self.monom[count][1]})1902return self19031904# takes old kappa-data and embeds self in larger graph with numvert vertices on vertices start, start+1, ...1905def expand_vertices(self, start, numvert):1906for count in range(len(self.monom)):1907self.monom[count] = ([[] for i in range(start)] + self.monom[count][0] + [[]1908for i in range(numvert - start - len(self.monom[count][0]))], self.monom[count][1])1909return self19101911def __radd__(self, other):1912if other == 0:1913return self.copy()19141915def __mul__(self, other):1916if isinstance(other, kppoly):1917return 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()1918# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational):1919else:1920return self.__rmul__(other)19211922def __rmul__(self, other):1923if isinstance(other, kppoly):1924return self.__mul__(other)1925# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):1926else:1927new = self.copy()1928for i in range(len(self)):1929new.coeff[i] *= other1930return new19311932def __pow__(self, exponent):1933if isinstance(exponent, (Integer, Rational, int)):1934return prod(exponent * [self])19351936def _monomial_str(self, kappa, psi, unicode=False):1937r"""1938Return the string representation of the monomial ``kappa``, ``psi``.1939"""1940if unicode:1941kappa_name = "κ"1942psi_name = "ψ"1943index_prefix = ""1944exponent_prefix = ""1945indices = "₀₁₂₃₄₅₆₇₈₉"1946exponents = "⁰¹²³⁴⁵⁶⁷⁸⁹"1947else:1948kappa_name = "kappa"1949psi_name = "psi"1950index_prefix = "_"1951exponent_prefix = "^"1952indices = exponents = "0123456789"19531954def num_str(n, digits_str, minus='-'):1955if n < 0:1956raise ValueError1957elif n == 0:1958return digits_str[0]1959else:1960return ''.join(digits_str[d] for d in reversed(ZZ(n).digits(10)))19611962result = []19631964# kappa part1965for v, kappa_v in enumerate(kappa):1966kappa_str = []1967for deg, power in enumerate(kappa_v):1968if power:1969deg += 11970kappa_str.append(kappa_name + index_prefix + num_str(deg, indices) + exponent_prefix + num_str(power, exponents) if power != 1 else kappa_name + index_prefix + num_str(deg, indices))1971if kappa_str:1972result.append(('({})' + index_prefix + '{}').format('*'.join(kappa_str), num_str(v, indices)))19731974# psi part1975for i, j in psi.items():1976result.append(psi_name + index_prefix + num_str(i, indices) + exponent_prefix + num_str(j, exponents) if j != 1 else psi_name + index_prefix + num_str(i, indices))19771978return '*'.join(result)19791980def str(self, unicode=False):1981result = ''1982for i, (c, m) in enumerate(zip(self.coeff, self.monom)):1983cs = repr(c)1984ms = self._monomial_str(kappa=m[0], psi=m[1], unicode=unicode)1985has_add_or_sub = '+' in cs[1:] or '-' in cs[1:]1986if i:1987if cs[0] == '-':1988if not has_add_or_sub:1989result += ' - '1990cs = repr(-c)1991elif not ms:1992result += ' - '1993cs = cs[1:]1994else:1995result += ' + '1996else:1997result += ' + '19981999if not ms:2000result += cs2001elif has_add_or_sub:2002result += '(' + cs + ')*' + ms2003elif cs == '1':2004result += ms2005elif cs == '-1':2006result += '-' + ms2007else:2008result += cs + '*' + ms20092010return result if result else '0'20112012def _repr_(self):2013r"""2014TESTS::20152016sage: from admcycles.admcycles import KappaPsiPolynomial2017sage: x = polygen(ZZ)2018sage: m1 = ([[0,1,1]], {1: 2})2019sage: m2 = ([[1,0,1]], {})2020sage: m3 =([[]], {1:1})2021sage: m4 = ([], {1:1,2:1})2022sage: m5 = ([[]], {})2023sage: KappaPsiPolynomial([m1, m2, m3, m4, m5], [x-1, -1, x+2, -3, - x + 3])2024(x - 1)*(kappa_2*kappa_3)_0*psi_1^2 - (kappa_1*kappa_3)_0 + (x + 2)*psi_1 - 3*psi_1*psi_2 - x + 320252026sage: m1 = ([[]], {1:1})2027sage: m2 = ([[]], {})2028sage: KappaPsiPolynomial([m1, m2], [3, 4])20293*psi_1 + 42030"""2031return self.str(unicode=False)20322033def _unicode_art_(self):2034r"""2035TESTS::20362037sage: from admcycles.admcycles import KappaPsiPolynomial2038sage: x = polygen(ZZ)2039sage: m1 = ([[0,1,1]], {13: 25})2040sage: m2 = ([[1,0,1] + [0] * 10 + [1]], {})2041sage: m3 =([[]], {1:123})2042sage: m4 = ([], {1:1,2:1})2043sage: m5 = ([[]], {})2044sage: p = KappaPsiPolynomial([m1, m2, m3, m4, m5], [x-1, -1, x+2, -3, - x + 3])2045sage: unicode_art(p)2046(x - 1)*(κ₂*κ₃)₀*ψ₁₃²⁵ - (κ₁*κ₃*κ₁₄)₀ + (x + 2)*ψ₁¹²³ - 3*ψ₁*ψ₂ - x + 32047sage: m1 = ([[]], {1:1})2048sage: m2 = ([[]], {})2049sage: p = KappaPsiPolynomial([m1, m2], [3, 4])2050sage: unicode_art(p)20513*ψ₁ + 42052"""2053from sage.typeset.unicode_art import UnicodeArt2054return UnicodeArt([self.str(unicode=True)])20552056# TODO: this should rather be called "normalize"2057def consolidate(self, force=True):2058r"""2059Remove trailing zeroes in kappa and l with psi[l]=0 and things with coeff=02060and sum up again.20612062TESTS::20632064sage: from admcycles.admcycles import kppoly2065sage: kppoly([([[], [0, 1]], {})], [5]).consolidate()20665*(kappa_2)_12067sage: kppoly([([[0, 0], [0,1,0]], {})], [3]).consolidate()20683*(kappa_2)_12069sage: kppoly([([[], [0,1]], {})], [0]).consolidate() # known bug207002071"""2072modified = force2073for i in range(len(self.monom)):2074kappa, psi = self.monom[i]2075for kap in kappa:2076if kap and kap[-1] == 0:2077modified = True2078while kap and not kap[-1]:2079kap.pop()2080for l in list(psi):2081if not psi[l]:2082modified = True2083psi.pop(l)20842085# now we check for possibly newly identified duplicates2086if modified:2087i = 02088while i < len(self.monom):2089if not self.coeff[i]:2090self.coeff.pop(i)2091self.monom.pop(i)2092continue2093m = self.monom[i]2094j = i + 12095while True:2096try:2097j = self.monom.index(m, j)2098except ValueError:2099break2100else:2101self.coeff[i] += self.coeff[j]2102self.monom.pop(j)2103self.coeff.pop(j)2104if not self.coeff[i]:2105self.monom.pop(i)2106self.coeff.pop(i)2107else:2108i += 121092110return self211121122113kppoly = KappaPsiPolynomial21142115# some functions for entering kappa and psi classes211621172118def kappacl(vertex, index, numvert, g=None, n=None):2119r"""2120Returns the polynomial (kappa_index)_vertex21212122INPUT:21232124vertex : integer2125the vertex on which the kappa class is supported2126index : integer2127the index of the kappa class2128numvert : integer2129the total number of vertices21302131EXAMPLES::21322133sage: from admcycles.admcycles import kappacl2134sage: kappacl(0, 2, 4)2135(kappa_2)_02136sage: kappacl(2, 1, 3)2137(kappa_1)_22138"""2139# if g is not None or n is not None:2140# raise ValueError('g or n should not be provided')2141if index == 0:2142return (2 * g - 2 + n) * onekppoly(numvert)2143if index < 0:2144return KappaPsiPolynomial([], [])2145li = [[] for i in range(numvert)]2146li[vertex] = (index - 1) * [0] + [1]2147return KappaPsiPolynomial([(li, {})], [1])214821492150def psicl(leg, numvert):2151r"""2152Return the polynomial (psi)_leg - must give total number of vertices21532154EXAMPLES::21552156sage: from admcycles.admcycles import psicl2157sage: psicl(1, 4)2158psi_12159"""2160li = [[] for i in range(numvert)]2161return KappaPsiPolynomial([(li, {leg: 1})], [1])216221632164def onekppoly(numvert):2165r"""2166Return one as a kappa-psi polynomial21672168INPUT:21692170numvert : integer2171the number of vertices21722173EXAMPLES::21742175sage: from admcycles.admcycles import onekppoly2176sage: onekppoly(4)217712178"""2179return KappaPsiPolynomial([([[] for cnt in range(numvert)], {})], [1])21802181# def clean(self):2182# for k in self.coeff.keys():2183# if self.coeff[k]==0:2184# self.coeff.pop(k)2185# return self2186# TODO: Multiplication with other kppoly218721882189class decstratum(SageObject):2190r"""2191A tautological class given by a boundary stratum, decorated by a polynomial in2192kappa-classes on the vertices and psi-classes on the legs (i.e. half-edges2193and markings)21942195The internal structure is as follows21962197- ``gamma``: underlying stgraph for the boundary stratum2198- ``kappa``: list of length gamma.num_verts() of lists of the form [3,0,2]2199meaning that this vertex carries kappa_1^3*kappa_3^22200- ``psi``: dictionary, associating nonnegative integers to some legs, where2201psi[l]=3 means that there is a psi^3 at this leg22022203We adopt the convention that: kappa_a = pi_*(psi_{n+1}^{a+1}), where2204pi is the universal curve over the moduli space, psi_{n+1} is the psi-class2205at the marking n+1 that is forgotten by pi.2206"""22072208def __init__(self, gamma, kappa=None, psi=None, poly=None):2209self.gamma = gamma.copy(mutable=False) # pick an immutable copy (possibly avoids copy)2210if kappa is None:2211kappa = [[] for i in range(gamma.num_verts())]2212if psi is None:2213psi = {}2214if poly is None:2215# NOTE: the following might be wrong as the Python integer 1 is unlikely to2216# be an element of the base ring2217# See https://gitlab.com/modulispaces/admcycles/-/issues/832218self.poly = kppoly([(kappa, psi)], [1])2219else:2220if isinstance(poly, kppoly):2221self.poly = poly.copy()2222# if isinstance(poly,sage.rings.integer.Integer) or isinstance(poly,sage.rings.rational.Rational):2223else:2224self.poly = poly * onekppoly(gamma.num_verts())22252226# def deg(self):2227# return len(self.gamma.edges)+sum([sum([k[i]*(i+1) for i in range(len(k))]) for k in kappa])+sum(psi.values())22282229def __bool__(self):2230return bool(self.poly)22312232def copy(self, mutable=False):2233S = decstratum.__new__(decstratum)2234S.gamma = self.gamma.copy(mutable)2235S.poly = self.poly.copy()2236return S22372238def automorphism_number(self):2239r"""Returns number of automorphisms of underlying graph fixing decorations by kappa and psi-classes.2240Currently assumes that self has exaclty one nonzero term."""2241kappa, psi = self.poly.monom[0]2242g, n, r = self.gnr_list()[0]2243markings = range(1, n + 1)2244num = DR.num_of_stratum(Pixtongraph(self.gamma, kappa, psi), g, r, markings)2245return DR.autom_count(num, g, r, markings, MODULI_ST)22462247def rename_legs(self, dic, rename=None, inplace=True, return_dicts=False):2248r"""2249Rename the markings according to ``dic``.22502251EXAMPLES::22522253sage: from admcycles.stable_graph import StableGraph2254sage: from admcycles.admcycles import decstratum2255sage: g = StableGraph([0, 0], [[1, 3], [2, 4, 5, 6]], [(3, 4), (5, 6)])2256sage: D = decstratum(g, [[0,1], []], {1: 1})2257sage: D2258Graph : [0, 0] [[1, 3], [2, 4, 5, 6]] [(3, 4), (5, 6)]2259Polynomial : (kappa_2)_0*psi_12260sage: D.rename_legs({1: 2, 2: 1})2261Graph : [0, 0] [[2, 3], [1, 4, 5, 6]] [(3, 4), (5, 6)]2262Polynomial : (kappa_2)_0*psi_22263"""2264if rename is not None:2265from .superseded import deprecation2266deprecation(109, 'the rename argument in decstratum.rename_legs is deprecated (and actually ignored)')2267if inplace:2268result = self2269new_gamma = self.gamma.copy(mutable=True)2270else:2271result = self.copy(mutable=True)2272new_gamma = result.gamma2273_, markings_relabelling, legs_relabelling = new_gamma.rename_legs(dic, inplace=True, return_dicts=True)2274new_gamma.set_immutable()2275result.gamma = new_gamma2276result.gamma.set_immutable() # always better2277result.poly.rename_legs(legs_relabelling)2278return (result, markings_relabelling, legs_relabelling) if return_dicts else result22792280def __repr__(self):2281return 'Graph : ' + repr(self.gamma) + '\n' + 'Polynomial : ' + repr(self.poly)22822283def _unicode_art_(self):2284r"""2285TESTS::22862287sage: from admcycles.stable_graph import StableGraph2288sage: from admcycles.admcycles import decstratum2289sage: g = StableGraph([0, 0], [[1, 3], [2, 4, 5, 6]], [(3, 4), (5, 6)])2290sage: D = decstratum(g, [[0,1], []], {1: 1})2291sage: unicode_art(D)2292Graph :2293╭───╮2294│ │╭╮22953 4562296╭┴╮ ╭┴┴┴╮2297│0│ │0 │2298╰┬╯ ╰┬──╯22991 22300<BLANKLINE>2301Polynomial : (κ₂)₀*ψ₁2302"""2303from sage.typeset.unicode_art import unicode_art2304return unicode_art('Graph :') * unicode_art(self.gamma) * (unicode_art('\nPolynomial : ') + unicode_art(self.poly))23052306# 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)2307def split(self):2308result = []2309numvert = self.gamma.num_verts()2310lvdict = {l: v for v in range(numvert) for l in self.gamma.legs(v, copy=False)}23112312for (kappa, psi, coeff) in self.poly:2313psidiclist = [{} for v in range(numvert)]2314for l in psi:2315psidiclist[lvdict[l]][l] = psi[l]2316newentry = [kppoly([([kappa[v]], psidiclist[v])], [1]) for v in range(numvert)]2317newentry[0] *= coeff2318result.append(newentry)2319return result23202321def __neg__(self):2322return (-1) * self23232324def __mul__(self, other):2325return self.__rmul__(other)23262327def __rmul__(self, other):2328r"""2329TESTS::2330sage: from admcycles import TautologicalRing2331sage: P = TautologicalRing(1,2).psi(1)2332sage: P * next(iter(P._terms.values()))2333Graph : [1] [[1, 2]] []2334Polynomial : psi_1^22335sage: next(iter(P._terms.values())) * P2336Graph : [1] [[1, 2]] []2337Polynomial : psi_1^22338"""2339from .tautological_ring import TautologicalClass2340if isinstance(other, Hdecstratum):2341return other.__mul__(self)23422343if isinstance(other, decstratum):2344# TODO: to be changed (direct access to _edges attribute)2345if other.gamma.num_edges() > self.gamma.num_edges():2346return other.__rmul__(self)2347if self.gamma.num_edges() == other.gamma.num_edges() == 0:2348# both are just vertices with some markings and some kappa-psi-classes2349# multiply those kappa-psi-classes and return the result2350return tautclass([decstratum(self.gamma, poly=self.poly * other.poly)])2351p1 = self.convert_to_prodtautclass()2352p2 = self.gamma.boundary_pullback(other)2353p = p1 * p22354return p.pushforward()2355elif isinstance(other, TautologicalClass):2356return other.parent()([self]) * other2357else:2358# assume scalar multiplication2359new = self.copy(mutable=False)2360new.poly *= other2361return new23622363def multiply(self, other, R):2364r"""2365Return the result of the multiplication of the decstratum ``self``2366and the decstratum ``other`` in the tautological ring ``R``2367"""2368if type(self) is not type(other):2369raise TypeError23702371# TODO: be more clever in the multiplication. Many terms could be2372# zero because of the moduli encoded in R.2373if other.gamma.num_edges() > self.gamma.num_edges():2374return other.multiply(self, R)2375if self.gamma.num_edges() == other.gamma.num_edges() == 0:2376# both are just vertices with some markings and some kappa-psi-classes2377# multiply those kappa-psi-classes and return the result2378return R(self.gamma, poly=self.poly * other.poly)2379p1 = self.convert_to_prodtautclass()2380p2 = self.gamma.boundary_pullback(other)2381return (p1 * p2).pushforward(R)23822383def to_tautological_class(self):2384r"""2385Converts decstratum to TautologicalClass.23862387EXAMPLES::23882389sage: from admcycles.admcycles import decstratum, StableGraph2390sage: d = decstratum(StableGraph([1],[[1,2,3]],[(2,3)]),psi={1:1})2391sage: dt = d.to_tautological_class()2392sage: dt.parent()2393TautologicalRing(g=2, n=1, moduli='st') over Rational Field2394"""2395from .tautological_ring import TautologicalRing2396g = self.gamma.g()2397n = self.gamma.n() # TODO: adapt to nonstandard markings2398return TautologicalRing(g, n)([self])23992400def toTautvect(self, g=None, n=None, r=None):2401return converttoTautvect(self, g, n, r)24022403def toTautbasis(self, g=None, n=None, r=None):2404return Tautvecttobasis(converttoTautvect(self, g, n, r), g, n, r)24052406# remove all terms that must vanish for dimension reasons2407def dimension_filter(self, moduli='st'):2408for i in range(len(self.poly.monom) - 1, -1, -1):2409(kappa, psi) = self.poly.monom[i]2410for v in range(self.gamma.num_verts()):2411# local check: we are not exceeding the socle degree at any of the vertices2412kappa_deg = sum([(k + 1) * kappa[v][k] for k in range(len(kappa[v]))])2413psi_deg = sum([psi[l] for l in self.gamma.legs(v, copy=False) if l in psi])2414socle = socle_degree(self.gamma.genera(v), self.gamma.num_legs(v), get_moduli(moduli))2415if kappa_deg + psi_deg > socle:2416self.poly.monom.pop(i)2417self.poly.coeff.pop(i)2418break2419return self24202421# remove all terms of degree higher than dmax2422def degree_cap(self, dmax):2423for i in range(len(self.poly.monom) - 1, -1, -1):2424if self.poly.deg(i) + self.gamma.num_edges() > dmax:2425self.poly.monom.pop(i)2426self.poly.coeff.pop(i)2427return self24282429# returns degree d part of self2430def degree_part(self, d):2431result = self.copy(mutable=False)2432for i in range(len(result.poly.monom)):2433if result.poly.deg(i) + result.gamma.num_edges() != d:2434result.poly.coeff[i] = 02435return result.consolidate()24362437def consolidate(self):2438self.poly.consolidate()2439return self24402441# computes integral against the fundamental class of the corresponding moduli space2442# will not complain if terms are mixed degree or if some of them do not have the right codimension2443def evaluate(self, moduli='st'):2444DRmoduli = get_moduli(moduli, DRpy=True)2445answer = 02446for (kappa, psi, coeff) in self.poly:2447temp = 12448for v in range(self.gamma.num_verts()):2449psilist = [psi.get(l, 0) for l in self.gamma.legs(v, copy=False)]2450kappalist = []2451for j in range(len(kappa[v])):2452kappalist += [j + 1 for k in range(kappa[v][j])]2453if sum(psilist + kappalist) != socle_degree(self.gamma.genera(v), len(psilist), DRmoduli):2454temp = 02455break2456temp *= DR.socle_formula(self.gamma.genera(v), psilist, kappalist, moduli_type=DRmoduli)2457answer += coeff * temp2458return answer24592460def forgetful_pushforward(self, markings, dicv=False):2461r"""2462Return the decstratum corresponding to the pushforward under2463the map forgetting the ``markings``.24642465The obtained ``decstratum`` has underlying stable graph the one obtained by2466forgetting and stabilizing. The algorithm currently works by forgetting one2467marking at a time.2468"""2469result = self.copy(mutable=True)2470result.dimension_filter()2471vertim = list(range(self.gamma.num_verts()))2472for m in markings:2473# take current result and forget m2474v = result.gamma.vertex(m)2475if result.gamma.genera(v) == 0 and len(result.gamma.legs(v, copy=False)) == 3:2476# this vertex will become unstable, but the previous dimension filter took care that no term in result.poly has kappa or psi on vertex v2477# thus we can simply forget this vertex and go on, but we must adjust the kappa entries in result.poly24782479# 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'2480# TODO: use more elegant (dicv,dicl)-return of .stabilize()2481vlegs = result.gamma.legs(v, copy=True)2482vlegs.remove(m)2483vmarks = set(vlegs).intersection(set(result.gamma.list_markings()))24842485if vmarks:2486mprime = list(vmarks)[0]2487vlegs.remove(mprime)2488leg = vlegs[0]2489legprime = result.gamma.leginversion(leg)2490for (kappa, psi, coeff) in result.poly:2491if legprime in psi:2492temp = psi.pop(legprime)2493psi[mprime] = temp24942495result.gamma.forget_markings([m])2496result.gamma.stabilize()2497vertim.pop(v)2498for (kappa, psi, coeff) in result.poly:2499kappa.pop(v)2500else:2501# no vertex becomes unstable, hence result.gamma is unchanged2502# we proceed to push forward on the space corresponding to marking v2503g = result.gamma.genera(v)2504n = result.gamma.num_legs(v) - 1 # n of the target space2505numvert = result.gamma.num_verts()25062507newpoly = kppoly([], [])2508for (kappa, psi, coeff) in result.poly:2509trunckappa = [kap[:] for kap in kappa]2510trunckappa[v] = []2511truncpsi = psi.copy()2512for l in result.gamma.legs(v, copy=False):2513truncpsi.pop(l, 0)2514trunckppoly = kppoly([(trunckappa, truncpsi)], [coeff]) # kppoly-term supported away from vertex v25152516kappav = kappa[v]2517psiv = {l: psi.get(l, 0) for l in result.gamma.legs(v, copy=False) if l != m}2518psivpoly = kppoly([([[] for i in range(numvert)], psiv)], [1])2519a_m = psi.get(m, 0)25202521cposs = []2522for b in kappav:2523cposs.append(list(range(b + 1)))2524# if kappav=[3,0,1] then cposs = [[0,1,2,3],[0],[0,1]]25252526currpoly = kppoly([], [])25272528for cvect in itertools.product(*cposs):2529currpoly += prod([binomial(kappav[i], cvect[i]) for i in range(len(cvect))]) * prod([kappacl(v, i + 1, numvert)**cvect[i] for i in range(2530len(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)2531if a_m == 0:2532kappapoly = prod([kappacl(v, i + 1, numvert)**kappav[i] for i in range(len(kappav))])25332534def psiminuspoly(l):2535psivminus = psiv.copy()2536psivminus[l] -= 12537return kppoly([([[] for i in range(numvert)], psivminus)], [1])25382539currpoly += sum([psiminuspoly(l) for l in psiv if psiv[l] > 0]) * kappapoly25402541newpoly += currpoly * trunckppoly25422543result.poly = newpoly2544result.gamma.forget_markings([m])25452546result.dimension_filter()25472548result.gamma.set_immutable()2549if dicv:2550return (result, {v: vertim[v] for v in range(len(vertim))})2551else:2552return result25532554# TODO: why the rename argument is here? shouldn't it always be True?25552556def forgetful_pullback_list(self, newmark, rename=True):2557r"""2558Return a list of :class:`decstratum` that corresponds to the pullback under2559the forgetful map which forget the markings in ``newmark``.2560"""2561if not newmark:2562return [self]2563if rename:2564# Make sure that none of the newmark appears as a leg of the graph2565mleg = max(self.gamma._maxleg + 1, max(newmark) + 1)2566rnself = self.copy(mutable=True)25672568dic = {i: i for l in self.gamma._legs for i in l}2569for e in self.gamma.edges(copy=False):2570for i in e:2571if i in newmark:2572dic[i] = mleg2573mleg += 12574rnself.gamma.relabel({}, dic, inplace=True)25752576# Now fix psi-data2577rnself.poly.rename_legs(dic)2578else:2579rnself = self25802581# TODO: change recursive definition to explicit, direct definition?2582# mdist = itertools.product(*[list(range(len(rnself.gamma.genera))) for j in range(len(newmark))])25832584# Strategy: first compute pullback under forgetting newmark[0], then by recursion pull this back under forgetting other elements of newmark2585a = newmark[0]2586nextnewmark = newmark[1:]2587partpullback = [] # list of decstratums to construct tautological class obtained by forgetful-pulling-back a25882589for v in range(self.gamma.num_verts()):2590# v gives the vertex where the marking a goes25912592# first the purely kappa-psi-term2593# grv is the underlying stgraph2594# TODO: to be changed (direct access to private attributes _legs)2595grv = rnself.gamma.copy()2596grv._legs[v] += [a]2597grv.tidy_up()2598grv.set_immutable()25992600# polyv is the underlying kppoly2601polyv = kppoly([], [])2602for (kappa, psi, coeff) in rnself.poly:2603# for kappa[v]=(3,0,2) gives [[0,1,2,3],[0],[0,1,2]]2604binomdist = [list(range(0, m + 1)) for m in kappa[v]]26052606for bds in itertools.product(*binomdist):2607kappa_new = [kap[:] for kap in kappa]2608kappa_new[v] = list(bds)26092610psi_new = psi.copy()2611psi_new[a] = sum([(i + 1) * (kappa[v][i] - bds[i]) for i in range(len(bds))])2612polyv = polyv + kppoly([(kappa_new, psi_new)], [((-1)**(sum([(kappa[v][i] - bds[i]) for i in range(len(bds))])))2613* coeff * prod([binomial(kappa[v][i], bds[i]) for i in range(len(bds))])])26142615# now the terms with boundary divisors2616# TODO: to be changed (access to private attribute _maxleg)2617rnself.gamma.tidy_up()2618# this is the leg landing on vertex v, connecting it to the rational component below2619newleg = max(rnself.gamma._maxleg + 1, a + 1)2620grl = {} # dictionary: leg l -> graph with rational component attached where l was2621for l in rnself.gamma.legs(v, copy=False):2622# create a copy of rnself.gamma with leg l replaced by a rational component attached to v, containing a and l2623# TODO: use functions!!2624new_gr = rnself.gamma.copy()2625new_gr._legs[v].remove(l)2626new_gr._legs[v] += [newleg]2627new_gr._genera += [0]2628new_gr._legs += [[newleg + 1, a, l]]2629new_gr._edges += [(newleg, newleg + 1)]2630new_gr.tidy_up()2631new_gr.set_immutable()26322633grl[l] = new_gr26342635# contains kappa, psi polynomials attached to the graphs above2636polyl = {l: kppoly([], []) for l in rnself.gamma.legs(v, copy=False)}26372638for (kappa, psi, coeff) in rnself.poly:2639for j in set(psi).intersection(set(rnself.gamma.legs(v, copy=False))):2640if psi[j] == 0: # no real psi class here2641continue2642# rational component, which is placed last in the graphs above, doesn't get kappa classes2643kappa_new = [kap[:] for kap in kappa] + [[]]2644psi_new = psi.copy()26452646# psi**b at leg l is replaced by -psi^(b-1) at the leg connecting v to the new rational component2647psi_new[newleg] = psi_new.pop(j) - 126482649polyl[j] = polyl[j] + kppoly([(kappa_new, psi_new)], [-coeff])2650partpullback += [decstratum(grv, poly=polyv)] + [decstratum(grl[l], poly=polyl[l])2651for l in rnself.gamma.legs(v, copy=False) if len(polyl[l]) > 0]26522653# now partpullback is list of decstratums; call recursively on the next newmark2654result = []2655for ds in partpullback:2656result.extend(ds.forgetful_pullback_list(nextnewmark))2657return result26582659def forgetful_pullback(self, newmark, rename=True):2660return tautclass(self.forgetful_pullback_list(newmark, rename))26612662# converts the decstratum self to a prodtautclass on self.gamma2663def convert_to_prodtautclass(self):2664# TODO: to be changed (direct access to private attributes _genera, _legs)2665terms = []2666for (kappa, psi, coeff) in self.poly:2667currterm = []2668for v in range(self.gamma.num_verts()):2669psiv = {(lnum + 1): psi[self.gamma._legs[v][lnum]]2670for lnum in range(len(self.gamma._legs[v])) if self.gamma._legs[v][lnum] in psi}2671currterm.append(decstratum(stgraph([self.gamma.genera(v)], [list(2672range(1, len(self.gamma._legs[v]) + 1))], []), kappa=[kappa[v]], psi=psiv))2673currterm[0].poly *= coeff2674terms.append(currterm)2675return prodtautclass(self.gamma, terms)26762677# returns a list [(g,n,r), ...] of all genera g, number n of markings and degrees r for the terms appearing in self2678def gnr_list(self):2679g = self.gamma.g()2680n = self.gamma.n()2681e = self.gamma.num_edges()2682L = ((g, n, e + self.poly.deg(i)) for i in range(len(self.poly.monom)))2683return list(set(L))26842685# stores list of Hdecstratums, together with methods for scalar multiplication from left and sum268626872688class Htautclass:2689def __init__(self, terms):2690self.terms = terms26912692def copy(self):2693ans = Htautclass.__new__(Htautclass)2694ans.terms = [Hds.copy(mutable=False) for Hds in self.terms]2695return ans26962697def __neg__(self):2698return (-1) * self26992700def __add__(self, other): # TODO: add += operation2701if other == 0:2702return self.copy()2703new = self.copy()2704new.terms += [Hds.copy(mutable=False) for Hds in other.terms]2705return new.consolidate()27062707def __radd__(self, other):2708if other == 0:2709return self2710else:2711return self + other27122713def __rmul__(self, other):2714new = self.copy()2715for i in range(len(new.terms)):2716new.terms[i] = other * new.terms[i]2717return new27182719def __mul__(self, other):2720from .tautological_ring import TautologicalClass2721if isinstance(other, TautologicalClass):2722return sum([a * b for a in self.terms for b in other._terms.values()])27232724def __repr__(self):2725return '\n\n'.join(repr(self.terms[i])2726for i in range(len(self.terms)))27272728def consolidate(self): # TODO: Check for isomorphisms of Gstgraphs for terms, add corresponding kppolys2729for i in range(len(self.terms)):2730self.terms[i].consolidate()2731return self27322733# returns self as a prodHclass on the correct trivial graph2734# TODO: if self.terms==[], this does not produce a meaningful result2735def to_prodHclass(self):2736if not self.terms:2737raise ValueError('Htautclass does not know its graph, failed to convert to prodHclass')2738else:2739gamma0 = stgraph([self.terms[0].Gr.gamma.g()], [self.terms[0].Gr.gamma.list_markings()], [])2740terms = [t.to_decHstratum() for t in self.terms]2741return prodHclass(gamma0, terms)27422743# computes the pushforward under the map \bar H -> M_{g',b}, sending C to C/G2744# the result is a tautclass2745def quotient_pushforward(self):2746if not self.terms:2747# TODO: this raises a warning2748return tautclass([])2749return sum(c.quotient_pushforward() for c in self.terms)27502751# pull back tautclass other on \bar M_{g',b} under the quotient map delta and return multiplication with self2752# effectively: pushforward self (divide by deg(delta)), multiply with other, then pull back entire thing2753# TODO: verify that this is allowed2754def quotient_pullback(self, other):2755if not self.terms:2756return deepcopy(self) # self=0, so pullback still zero2757g = self.terms[0].Gr.gamma.g()2758hdata = self.terms[0].Gr.hdata2759trivGg = trivGgraph(g, hdata)2760deltadeg = trivGg.delta_degree(0)27612762push = (QQ(1) / deltadeg) * self.quotient_pushforward()2763pro = push * other2764pro.dimension_filter()27652766# result = quotient pullback of pro to trivGg2767result = Htautclass([])2768edgenums = set(t.gamma.num_edges() for t in pro._terms.values())2769Hstrata = {i: list_Hstrata(g, hdata, i) for i in edgenums}2770Hquots = {i: list_quotgraphs(g, hdata, i, True) for i in edgenums}27712772for t in pro._terms.values():2773# t is a decstratum on \bar M_{g',b} living on a graph t.gamma2774# list all Hstrata having quotient graph t.gamma, then pull back t.poly and scale by right multiplicity2775(r, ti, tdicv, tdicl) = deggrfind(t.gamma)2776# tdicv and tdicl send vertex/leg names in t.gamma to the names of the vertices/legs in the standard graph27772778preims = [j for j in range(len(Hstrata[r])) if Hquots[r][j][1] == ti]27792780for j in preims:2781# this corresponds to a Gstgraph, which needs to be decorated by pullbacks of t.poly and added to result2782newGr = Hstrata[r][j].copy()2783numvert = newGr.gamma.num_verts()2784multiplicity = prod([newGr.lstabilizer(e0).order() for (e0, e1) in Hquots[r][j][0].edges(2785copy=False)]) * t.gamma.automorphism_number() / QQ(len(equiGraphIsom(newGr, newGr)))2786(quotientgraph, inde, dicv, dicvinv, dicl, diclinv) = Hquots[r][j]27872788newpoly = kppoly([], [])2789for (kappa, psi, coeff) in t.poly:2790newcoeff = coeff2791newkappa = [[] for u in range(numvert)]2792for v in range(len(kappa)):2793newkappa[dicvinv[tdicv[v]]] = kappa[v]2794newcoeff *= (QQ(1) / newGr.vstabilizer(dicvinv[tdicv[v]]).order())**(sum(kappa[v]))2795newpsi = {diclinv[tdicl[l]]: psi[l] for l in psi}2796newcoeff *= prod([(newGr.lstabilizer(diclinv[tdicl[l]]).order())**(psi[l]) for l in psi])27972798newpoly += kppoly([(newkappa, newpsi)], [newcoeff])27992800newpoly *= multiplicity28012802result.terms.append(Hdecstratum(newGr, poly=newpoly))2803return result280428052806# 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)2807# self.Gr = underlying Gstgraph for the boundary stratum2808# 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^22809# self.psi = dictionary, associating nonnegative integers to some legs, where psi[l]=3 means that there is a psi^3 at this leg2810# 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 points2811class Hdecstratum:2812def __init__(self, Gr, kappa=None, psi=None, poly=None):2813self.Gr = Gr2814if kappa is None:2815kappa = [[] for i in range(Gr.gamma.num_verts())]2816if psi is None:2817psi = {}2818if poly is None:2819self.poly = kppoly([(kappa, psi)], [1])2820else:2821self.poly = poly2822# def deg(self):2823# return len(self.gamma.edges)+sum([sum([k[i]*(i+1) for i in range(len(k))]) for k in kappa])+sum(psi.values())28242825def copy(self, mutable=True):2826ans = Hdecstratum.__new__(Hdecstratum)2827ans.Gr = self.Gr.copy(mutable=mutable)2828ans.poly = self.poly.copy()2829return ans28302831def __neg__(self):2832return (-1) * self28332834def __rmul__(self, other):2835if isinstance(other, decstratum):2836return self.__mul__(other)2837# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational):2838else:2839new = self.copy()2840new.poly *= other2841return new28422843def to_decHstratum(self):2844result = self.Gr.to_decHstratum()2845result.poly *= self.poly2846return result28472848def __mul__(self, other):2849if isinstance(other, decstratum):2850# Step 1: find a list commdeg of generic (self,other)-Gstgraphs Gamma32851# Elements of this list will have the form (Gamma3,currentdeg) with currentdeg a list of elements (vdict1,ldict1,vdict2,ldict2), where2852# * Gamma3 is a Gstgraph with same group G as self.Gr2853# * vdict1, vdict2 are the vertex-maps from vertices of Gamma3 to self.Gr.gamma and other.gamma2854# * ldict1, ldict2 are the leg-maps from legs of self.Gr.gamma, other.gamma to Gamma32855if self.Gr.gamma.num_edges() == 0: # TODO: implement intersection with non-trivial Gstgraph2856# minimal number of edge-orbits to hope for generic graph2857maxdeg = other.gamma.num_edges()2858mindeg = (QQ(maxdeg) / self.Gr.G.order()).ceil()28592860Ggrdegenerations = [list_Hstrata(self.Gr.gamma.g(), self.Gr.hdata, r) for r in range(maxdeg + 1)]28612862commdeg = []2863# gives for every entry in commdeg the number of degenerations isomorphic to this entry via Aut_G(Gamma3)2864multiplicityvector = []2865for deg in range(mindeg, maxdeg + 1):2866for Gamma3 in Ggrdegenerations[deg]:2867currentdeg = []2868currentmultiplicity = []2869otstructures = Astructures(Gamma3.gamma, other.gamma)2870if not otstructures:2871continue2872AutGr = equiGraphIsom(Gamma3, Gamma3)28732874# Now single out representatives of the AutGr-action on otstructures2875while otstructures:2876(dicvcurr, diclcurr) = otstructures[0]2877multiplicity = 02878for (dicv, dicl) in AutGr:2879dicvnew = {v: dicvcurr[dicv[v]] for v in dicv}2880diclnew = {l: dicl[diclcurr[l]] for l in diclcurr}2881try:2882otstructures.remove((dicvnew, diclnew))2883multiplicity += 12884except ValueError:2885pass2886# now must test if we are dealing with a generic structure28872888coveredlegs = set()2889for g in Gamma3.G:2890for l in diclcurr.values():2891coveredlegs.add(Gamma3.legact[(g, l)])2892# in general case would need to include half-edges from self.Gr.gamma2893if set(Gamma3.gamma.halfedges()).issubset(coveredlegs):2894currentdeg += [({v: 0 for v in range(Gamma3.gamma.num_verts())},2895{l: l for l in self.Gr.gamma.leglist()}, dicvcurr, diclcurr)]2896multiplicity *= QQ(1) / len(AutGr) # 1*numotheraut/len(AutGr)2897currentmultiplicity += [multiplicity]2898commdeg += [(Gamma3, currentdeg)]2899multiplicityvector += [currentmultiplicity]29002901# Step 1 finished29022903# Step2: go through commdeg, use vdicts to transfer kappa-classes and ldicts to transfer psi-classes2904# from self and other to Gamma3; also look for multiple edges in G-orbits on Gamma3 and put2905# the contribution (-psi-psi') coming from excess intersection there2906# TODO: figure out multiplicities from orders of orbits, automorphisms, etc.2907# TODO: eliminate early any classes that must vanish for dimension reasons2908# sum everything up and return it2909result = Htautclass([])2910for count in range(len(commdeg)):2911if not commdeg[count][1]:2912continue2913Gamma3 = commdeg[count][0]2914gammaresultpoly = kppoly([], [])2915for count2 in range(len(commdeg[count][1])):2916(vdict1, ldict1, vdict2, ldict2) = commdeg[count][1][count2]2917# compute preimages of vertices in self, other under the chosen morphisms to assign kappa-classes2918preim1 = [[] for i in range(self.Gr.gamma.num_verts())]2919for v in vdict1:2920preim1[vdict1[v]] += [v]2921preim2 = [[] for i in range(other.gamma.num_verts())]2922for v in vdict2:2923preim2[vdict2[v]] += [v]2924numvert = Gamma3.gamma.num_verts()29252926# excess intersection classes2927quotdicl = {}2928quotgraph = Gamma3.quotient_graph(dicl=quotdicl)2929numpreim = {l: 0 for l in quotgraph.halfedges()}29302931for l in self.Gr.gamma.halfedges():2932numpreim[quotdicl[ldict1[l]]] += 12933for l in other.gamma.halfedges():2934numpreim[quotdicl[ldict2[l]]] += 129352936excesspoly = onekppoly(numvert)2937for (e0, e1) in quotgraph.edges(copy=False):2938if numpreim[e0] > 1:2939excesspoly *= ((-1) * (psicl(e0, numvert) + psicl(e1, numvert)))**(numpreim[e0] - 1)29402941resultpoly = kppoly([], [])2942for (kappa1, psi1, coeff1) in self.poly:2943for (kappa2, psi2, coeff2) in other.poly:2944# pullback of psi-classes2945psipolydict = {ldict1[l]: psi1[l] for l in psi1}2946for l in psi2:2947psipolydict[ldict2[l]] = psipolydict.get(ldict2[l], 0) + psi2[l]2948psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])29492950# pullback of kappa-classes2951kappapoly1 = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim1[v]])**kappa1[v][k]2952for k in range(len(kappa1[v]))]) for v in range(self.Gr.gamma.num_verts())])2953kappapoly2 = prod([prod([sum([kappacl(w, k + 1, numvert) for w in preim2[v]])**kappa2[v][k]2954for k in range(len(kappa2[v]))]) for v in range(other.gamma.num_verts())])2955resultpoly += coeff1 * coeff2 * psipoly * kappapoly1 * kappapoly229562957# TODO: divide by |Aut_G(Gamma3)| ??? if so, do in definition of multiplicityvector2958resultpoly *= multiplicityvector[count][count2] * excesspoly2959gammaresultpoly += resultpoly29602961result.terms += [Htautclass([Hdecstratum(Gamma3, poly=gammaresultpoly)])]2962return result29632964def __repr__(self):2965return 'Graph : ' + repr(self.Gr) + '\n' + 'Polynomial : ' + repr(self.poly)29662967def consolidate(self):2968self.poly.consolidate()2969return self29702971# computes the pushforward under the map \bar H -> M_{g',b}, sending C to C/G2972# the result is a tautclass2973def quotient_pushforward(self):2974Gord = self.Gr.G.order()29752976qdicv = {}2977qdicvinv = {}2978qdicl = {}2979quotgr = self.Gr.quotient_graph(dicv=qdicv, dicvinv=qdicvinv, dicl=qdicl)29802981preimlist = [[] for i in range(quotgr.num_verts())]2982for v in range(self.Gr.gamma.num_verts()):2983preimlist[qdicv[v]] += [v]29842985# first, for each vertex w in quotgr compute the degree of the delta-map \bar H(preim(v)) -> \bar M_{g(v),n(v)}2986deltadeg = {w: self.Gr.delta_degree(qdicvinv[w]) for w in qdicvinv}29872988# result will be decstratum on the graph quotgr2989# go through all terms of self.poly and add their pushforwards2990# for each term, go through vertices of quotient graph and collect all classes above it,2991# i.e. kappa-classes from vertex-preimages and psi-classes from leg-preimages29922993resultpoly = kppoly([], [])2994for (kappa, psi, coeff) in self.poly:2995kappanew = []2996coeffnew = coeff2997psinew = {}2998for w in range(quotgr.num_verts()):2999kappatemp = []3000for v in preimlist[w]:3001kappatemp = kappaadd(kappatemp, kappa[v])3002kappanew += [kappatemp]3003# Gord/len(preimlist[w]) is the order of the stabilizer of any preimage v of w3004coeffnew *= (QQ(Gord) / len(preimlist[w]))**sum(kappatemp)3005for l in psi:3006psinew[qdicl[l]] = psinew.get(qdicl[l], 0) + psi[l]3007coeffnew *= (QQ(1) / self.Gr.character[l][1])**psi[l]3008coeffnew *= prod(deltadeg.values())3009resultpoly += kppoly([(kappanew, psinew)], [coeffnew])3010return tautclass([decstratum(quotgr, poly=resultpoly)])301130123013def remove_trailing_zeros(l):3014r"""3015Remove the trailing zeroes t the end of l.30163017EXAMPLES::30183019sage: from admcycles.admcycles import remove_trailing_zeros3020sage: l = [0, 1, 0, 0]3021sage: remove_trailing_zeros(l)3022sage: l3023[0, 1]3024sage: remove_trailing_zeros(l)3025sage: l3026[0, 1]30273028sage: l = [0, 0]3029sage: remove_trailing_zeros(l)3030sage: l3031[]3032sage: remove_trailing_zeros(l)3033sage: l3034[]3035"""3036while l and l[-1] == 0:3037l.pop()30383039# returns sum of kappa-vectors for same vertex, so [0,2]+[1,2,3]=[1,4,3]304030413042def kappaadd(a, b):3043if len(a) < len(b):3044aprime = a + (len(b) - len(a)) * [0]3045else:3046aprime = a3047if len(b) < len(a):3048bprime = b + (len(a) - len(b)) * [0]3049else:3050bprime = b3051return [aprime[i] + bprime[i] for i in range(len(aprime))]305230533054def Graphtodecstratum(G):3055r"""3056Converts a Pixton-style Graph (matrix with univariate-Polynomial entries) into a decstratum.30573058Assume that markings on Graph are called 1, 2, 3, ..., n.30593060The function :func:`Pixtongraph` provides the conversion in the other direction.30613062EXAMPLES::30633064sage: from admcycles.DR.graph import Graph, R, X3065sage: from admcycles.admcycles import Graphtodecstratum3066sage: G = Graph(matrix(R, 3, 2, [-1, 0, 1, 1, X + 2, 1]))3067sage: Graphtodecstratum(G)3068Graph : [1, 2] [[2], [3]] [(2, 3)]3069Polynomial : (kappa_1)_13070"""3071# first care about vertices, i.e. genera and kappa-classes3072genera = []3073kappa = []3074for i in range(1, G.M.nrows()):3075genera.append(G.M[i, 0][0])3076kappa.append([G.M[i, 0][j] for j in range(1, G.M[i, 0].degree() + 1)])3077# now care about markings as well as edges together with the corresponding psi-classes3078legs = [[] for i in genera]3079edges = []3080psi = {}3081legname = G.M.ncols() # legs corresponding to edges are named legname, legname+1, ...; cannot collide with marking-names3082for j in range(1, G.M.ncols()):3083for i in range(1, G.M.nrows()):3084if G.M[i, j] != 0:3085break3086# now i is the index of the first nonzero entry in column j3087if G.M[0, j] != 0: # this is a marking3088legs[i - 1].append(ZZ(G.M[0, j]))3089if G.M[i, j].degree() >= 1:3090psi[G.M[0, j]] = G.M[i, j][1]30913092if G.M[0, j] == 0: # this is a leg, possibly self-node3093if G.M[i, j][0] == 2: # self-node3094legs[i - 1] += [legname, legname + 1]3095edges.append((legname, legname + 1))3096if G.M[i, j].degree() >= 1:3097psi[legname] = G.M[i, j][1]3098if G.M[i, j].degree() >= 2:3099psi[legname + 1] = G.M[i, j][2]3100legname += 23101if G.M[i, j][0] == 1: # edge to different vertex3102if G.M.nrows() <= i + 1:3103print('Attention')3104print(G.M)3105for k in range(i + 1, G.M.nrows()):3106if G.M[k, j] != 0:3107break3108# now k is the index of the second nonzero entry in column j3109legs[i - 1] += [legname]3110legs[k - 1] += [legname + 1]3111edges.append((legname, legname + 1))3112if G.M[i, j].degree() >= 1:3113psi[legname] = G.M[i, j][1]3114if G.M[k, j].degree() >= 1:3115psi[legname + 1] = G.M[k, j][1]3116legname += 23117return decstratum(stgraph(genera, legs, edges), kappa=kappa, psi=psi)311831193120@file_cache.file_cached_function(3121directory=os.path.join(DOT_SAGE, "admcycles"),3122url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",3123remote_database_list="https://modulispaces.gitlab.io/relations-database/generating_indices_files",3124env_var="ADMCYCLES_CACHE_DIR",3125key=file_cache.ignore_args_key([3, 4]),3126filename=file_cache.ignore_args_filename())3127def generating_indices(g, n, r, FZ=True, FZmethod=None, moduli='st'):3128r"""3129Return the indices of a basis of `R^r(\bar M_{g,n})` in terms of the standard list of generators.31303131This function assumes that the generalized Faber-Zagier relations [PPZ15]_3132are a complete set of relations. For most parameters this is computed by3133using the FZ-relations. However, if r is greater than half the dimension of3134\bar M_{g,n} and all cohomology is tautological, we rather compute a3135generating set using Poincare duality and the intersection pairing313631373138INPUT:31393140FZ : bool (default: True)3141Whether FZ relations are used to compute the basis. If False (only for moduli='st'),3142instead compute basis by pairing matrix from opposite degree.31433144FZmethod : string (default: None)3145Which method is to be used for computing FZ relations, the options are '3spin' and 'newrels'.3146If no method is supplied we use the 3-spin formula when ``r`` < 3 and3147the new relation method otherwise.31483149moduli : string (default: 'st')3150If instead of 'st' one of 'ct', 'rt' or 'sm' is given, compute a basis for the tautological3151ring of the corresponding open subset of `\bar M_{g,n}`.31523153EXAMPLES::31543155sage: from admcycles import generating_indices3156sage: generating_indices(1, 2, 1)3157[0, 1]3158sage: generating_indices(1, 2, 1, FZ=False)3159[0, 1]3160sage: generating_indices(1, 2, 1, moduli='ct')3161[0]3162sage: generating_indices(1, 1, 1, moduli='ct')3163[]31643165TESTS::31663167sage: from admcycles import generating_indices3168sage: generating_indices(9, 0, 3, FZmethod='3spin', moduli='sm')3169[0, 1, 2]3170sage: generating_indices(9, 0, 3, FZmethod='newrels', moduli='sm')3171[0, 1, 2]31723173We do the same as above, but we avoid the use of any chached results.31743175sage: from admcycles import generating_indices3176sage: generating_indices.f(1, 2, 1)3177[0, 1]3178sage: generating_indices.f(1, 2, 1, FZ=False)3179[0, 1]3180sage: generating_indices.f(1, 2, 1, moduli='ct')3181[0]3182sage: generating_indices.f(1, 1, 1, moduli='ct')3183[]31843185Test the oneline database::31863187sage: from admcycles import generating_indices3188sage: from tempfile import mkdtemp3189sage: from shutil import rmtree3190sage: import os3191sage: import pickle3192sage: generating_indices.set_online_lookup(True)3193sage: filename = "generating_indices_0_3_0_st.pkl"3194sage: tmpdir = mkdtemp()3195sage: filename_with_path = os.path.join(tmpdir, filename)3196sage: generating_indices._FileCachedFunction__download(filename, filename_with_path) # optional - internet3197sage: f = open(filename_with_path, "rb") # optional - internet3198sage: pickle.load(f) # optional - internet3199[0]3200sage: rmtree(tmpdir)3201"""3202if moduli == 'tl':3203raise NotImplementedError('generating_indices not implemented for treelike curves')3204if FZ or r <= (3 * g - 3 + n) / QQ(2) or not (g <= 1 or (g == 2 and n <= 19)):3205if FZmethod is None:3206spinmethod = r < 33207if FZmethod == '3spin':3208spinmethod = True3209if FZmethod == 'newrels':3210spinmethod = False3211# r < 3 is an extremely rough first approximation of when it is better to use the FZ approach3212rel = DR.rels_matrix(g, r, n, symm=0, moduli_type=get_moduli(moduli, DRpy=True), usespin=spinmethod)3213if rel.nrows() == 0: # no relation3214genstobasis.set_cache(list((QQ**rel.ncols()).basis()), *(g, n, r, moduli))3215return list(range(rel.ncols()))3216relconv = matrix([DR.convert_vector_to_monomial_basis(rel.row(i), g, r, tuple(range(1, n + 1)),3217moduli_type=get_moduli(moduli, DRpy=True)) for i in range(rel.nrows())])3218del rel3219reverseindices = list(range(relconv.ncols()))3220reverseindices.reverse()3221reordrelconv = relconv.matrix_from_columns(reverseindices)3222del relconv3223reordrelconv.echelonize() # (algorithm='classical')32243225reordnongens = reordrelconv.pivots()3226nongens = [reordrelconv.ncols() - i - 1 for i in reordnongens]3227gens = [i for i in range(reordrelconv.ncols()) if i not in nongens]32283229# compute result of genstobasis here, for efficiency reasons3230gtob = {gens[j]: vector(QQ, len(gens), {j: 1}) for j in range(len(gens))}3231for i in range(len(reordnongens)):3232gtob[nongens[i]] = vector([-reordrelconv[i, reordrelconv.ncols() - j - 1] for j in gens])3233genstobasis.set_cache([gtob[i] for i in range(reordrelconv.ncols())], g, n, r, moduli)3234return gens3235else:3236if moduli != 'st':3237raise NotImplementedError(3238'generating indices via pairing only implemented for whole moduli of stable curves')3239gencompdeg = generating_indices(g, n, 3 * g - 3 + n - r, True)3240maxrank = len(gencompdeg)3241currrank = 03242M = matrix(maxrank, 0)3243gens = []3244count = 03245while currrank < maxrank:3246Mnew = block_matrix([[M, matrix(DR.pairing_submatrix(tuple(gencompdeg), (count,), g,32473 * g - 3 + n - r, tuple(range(1, n + 1))))]], subdivide=False)3248if Mnew.rank() > currrank:3249gens.append(count)3250M = Mnew3251currrank = Mnew.rank()3252count += 13253return gens325432553256def Hintnumbers(g, dat, indices=None, redundancy=False):3257r"""3258TESTS::32593260We test that the function works correctly if the product Hbar * tautclass(...) is empty for some indices.32613262sage: from admcycles.admcycles import HurData, Hintnumbers3263sage: G = CyclicPermutationGroup(2)3264sage: g = G.gen()3265sage: e = G.one()3266sage: dat = HurData(G, [g, e, g, e])3267sage: Hintnumbers(0, dat)3268[4, 1, 2, 2, 1, 2, 2, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]3269"""3270Hbar = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])3271dimens = Hbar.terms[0].Gr.dim()3272markins = tuple(range(1, 1 + len(Hbar.terms[0].Gr.gamma.list_markings())))32733274strata = DR.all_strata(g, dimens, markins)3275intnumbers = []32763277if indices is None or redundancy:3278effindices = list(range(len(strata)))3279else:3280effindices = indices32813282for i in effindices:3283# If the below product does not have any terms, quotient_pushforward3284# returns ZZ(0) and evaluate raises an exception.3285# TODO: A better way to fix this may be to add arguments g, n to3286# Htautclass, then quotient_pushforward can always return a3287# tautological class.3288prod = Hbar * tautclass([Graphtodecstratum(strata[i])])3289if prod.terms:3290intnumbers += [prod.quotient_pushforward().evaluate()]3291else:3292intnumbers += [ZZ(0)]32933294if not redundancy:3295return intnumbers32963297M = DR.FZ_matrix(g, dimens, markins)3298Mconv = matrix([DR.convert_vector_to_monomial_basis(M.row(i), g, dimens, markins) for i in range(M.nrows())])3299relcheck = Mconv * vector(intnumbers)3300if relcheck == 2 * relcheck: # lazy way to check relcheck==(0,0,...,0)3301print('Intersection numbers are consistent, number of checks: ' + repr(len(relcheck)))3302else:3303print('Intersection numbers not consistent for ' + repr((g, dat.G, dat.l)))3304print('relcheck = ' + repr(relcheck))33053306if indices is None:3307return intnumbers3308else:3309return [intnumbers[j] for j in indices]331033113312def Hidentify(g, dat, method='pull', vecout=False, redundancy=False, markings=None):3313r"""3314Identifies the (pushforward of the) fundamental class of \bar H_{g,dat} in3315terms of tautological classes.33163317INPUT:33183319- ``g`` -- integer; genus of the curve C of the cover C -> D33203321- ``dat`` -- HurwitzData; ramification data of the cover C -> D33223323- ``method`` -- string (default: `'pull'`); method of computation33243325method='pull' means we pull back to the boundary strata and recursively3326identify the result, then use linear algebra to restrict the class of \bar H3327to an affine subvectorspace (the corresponding vector space is the3328intersection of the kernels of the above pullback maps), then we use3329intersections with kappa,psi-classes to get additional information.3330If this is not sufficient, return instead information about this affine3331subvectorspace.33323333method='pair' means we compute all intersection pairings of \bar H with a3334generating set of the complementary degree and use this to reconstruct the3335class.33363337- ``vecout`` -- bool (default: `False`); return a coefficient-list with respect3338to the corresponding list of generators tautgens(g,n,r).3339NOTE: if vecout=True and markings is not the full list of markings 1,..,n, it3340will return a vector for the space with smaller number of markings, where the3341markings are renamed in an order-preserving way.33423343- ``redundancy`` -- bool (default: `False`); compute pairings with all possible3344generators of complementary dimension (not only a generating set) and use the3345redundant intersections to check consistency of the result; only valid in case3346method='pair'.33473348- ``markings`` -- list (default: `None`); return the class obtained by the3349fundamental class of the Hurwitz space by forgetting all points not in3350markings; for markings = None, return class without forgetting any points.33513352EXAMPLES::33533354sage: from admcycles.admcycles import trivGgraph, HurData, Hidentify3355sage: G = PermutationGroup([(1, 2)])3356sage: H = HurData(G, [G[1], G[1]])3357sage: t = Hidentify(2, H, markings=[]); t # Bielliptic locus in \Mbar_23358Graph : [2] [[]] []3359Polynomial : 30*(kappa_1)_03360<BLANKLINE>3361Graph : [1, 1] [[2], [3]] [(2, 3)]3362Polynomial : -933633364Check that pair and pull methods are compatible::33653366sage: from admcycles.admcycles import Hdatabase3367sage: Hdatabase.clear() # remove the cached results from the previous computation3368sage: t2 = Hidentify(2, H, markings=[], method='pair')3369sage: t == t23370True33713372sage: G = PermutationGroup([(1, 2, 3)])3373sage: H = HurData(G, [G[1]])3374sage: t = Hidentify(2, H, markings=[]); t.is_zero() # corresponding space is empty3375True33763377TESTS::33783379sage: G = PermutationGroup([(1, 2)])3380sage: H = HurData(G, [G[1], G[1]])3381sage: Hidentify(2, H, markings=[2], vecout=True)3382(60, -21, 66, -123, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)3383sage: Hdatabase.clear() # remove the cached results from the previous computation3384sage: Hidentify(2, H, markings=[2], vecout=True, method='pair')3385(60, -21, 66, -123, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)3386"""3387Hbar = trivGgraph(g, dat)3388r = Hbar.dim()3389n = len(Hbar.gamma.list_markings())33903391# global Hexam3392# Hexam=(g,dat,method,vecout,redundancy,markings)33933394if markings is None:3395markings = list(range(1, n + 1))33963397N = len(markings)3398msorted = sorted(markings)3399markingdictionary = {i + 1: msorted[i] for i in range(N)}3400invmarkingdictionary = {markingdictionary[j]: j for j in markingdictionary}34013402# Section to deal with the case that the group G is trivial quickly3403if dat.G.order() == 1:3404if len(markings) < n:3405return tautclass([])3406else:3407return tautclass([decstratum(stgraph([g], [list(range(1, n + 1))], []))])34083409if not cohom_is_taut(g, len(markings), 2 * (3 * g - 3 + len(markings) - r)):3410print('Careful, Hurwitz cycle ' + repr((g, dat)) + ' might not be tautological!\n')34113412# first: try to look up if we already computed this class (or a class with even more markings) before3413found = False3414try:3415Hdb = Hdatabase[(g, dat)]3416for marks in Hdb:3417if set(markings).issubset(set(marks)):3418forgottenmarks = set(marks) - set(markings)3419result = deepcopy(Hdb[marks])3420result = result.forgetful_pushforward(list(forgottenmarks))3421found = True3422# TODO: potentially include this result in Hdb34233424except KeyError:3425pass34263427# if not, proceed according to method given3428if method == 'pair' and not found:3429Hbar = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])34303431def pairf(a):3432prod = Hbar * a3433if not prod.terms:3434return QQ(0)3435return prod.quotient_pushforward().evaluate()3436from .tautological_ring import TautologicalRing3437R = TautologicalRing(g, n)3438result = R.identify_class(3 * g - 3 + n - r, pairfunction=pairf, check=redundancy)34393440if (g, dat) not in Hdatabase:3441Hdatabase[(g, dat)] = {}3442Hdatabase[(g, dat)][tuple(range(1, n + 1))] = result34433444if len(markings) < n:3445result = Hidentify(g, dat, method, False, redundancy, markings)3446if method == 'pull' and not found:3447from .tautological_ring import TautologicalRing3448R = TautologicalRing(g, N)3449bar_H = prodHclass(stgraph([g], [list(range(1, N + 1))], []), [decHstratum(stgraph([g],3450[list(range(1, N + 1))], []), {0: (g, dat)}, [[0, markingdictionary]])])34513452def pullfun(gamma):3453return bar_H.gamma0_pullback(gamma).toprodtautclass()34543455def kpfun(kappa, psi, cl):3456return (bar_H * cl).evaluate()34573458result = R.identify_class(3 * g - 3 + N - r, pullfunction=pullfun, kappapsifunction=kpfun, check=redundancy)3459result = result.rename_legs(markingdictionary, inplace=False)34603461# return section3462if not found:3463if (g, dat) not in Hdatabase:3464Hdatabase[(g, dat)] = {}3465Hdatabase[(g, dat)][tuple(markings)] = deepcopy(result)34663467if vecout:3468rnresult = result.rename_legs(invmarkingdictionary, inplace=False)3469return rnresult.vector(3 * g - 3 + len(markings) - r)3470else:3471return result347234733474@cached_function3475def pullback_matrix(g, n, d, bdry=None, irrbdry=True):3476r"""3477Compute matrix representing pullback of basis of the tautological ring3478of Mbar_g,n in degree d under boundary pullbacks.34793480If bdry is given (as a StableGraph with exactly one edge) it computes3481the matrix whose columns are the pullback of our preferred basis to the3482divisor bdry, identified in the (tensor product) Tautbasis.3483If bdry=None, concatenate all matrices for all possible boundary3484divisors (in their order inside list_strata(g,n,1)).34853486If additionally irrbdry=False, omit the pullback to the irreducible3487boundary (since there computations get more complicated).34883489EXAMPLES::34903491sage: from admcycles.admcycles import StableGraph, pullback_matrix, list_strata3492sage: pullback_matrix(2, 0, 1)3493[ 1 -2]3494[ 0 4]3495[ 1 -2]3496[ 1 -2]3497sage: L = list_strata(2,0,1); L3498[[1] [[1, 2]] [(1, 2)], [1, 1] [[1], [2]] [(1, 2)]]3499sage: pullback_matrix(2, 0, 1, L[0])3500[ 1 -2]3501[ 0 4]3502sage: pullback_matrix(2, 0, 1, L[1])3503[ 1 -2]3504[ 1 -2]3505"""3506genindices = generating_indices(g, n, d) # indices of generators corresponding to columns of A3507strata = DR.all_strata(g, d, tuple(range(1, n + 1)))3508gens = [Graphtodecstratum(strata[i]) for i in genindices]3509ngens = len(genindices)35103511if bdry is None:3512A = matrix(0, ngens)3513bdrydiv = list_strata(g, n, 1)35143515for bdry in bdrydiv:3516if irrbdry or bdry.num_verts() != 1:3517A = block_matrix([[A], [pullback_matrix(g, n, d, bdry)]], subdivide=False)3518A.set_immutable()3519return A3520else:3521pullbacks = [bdry.boundary_pullback(cl).totensorTautbasis(d, True) for cl in gens]3522A = matrix(pullbacks).transpose()3523A.set_immutable()3524return A352535263527def kpintersection_matrix(g, n, d):3528r"""3529Computes the matrix B whose columns are the intersection numbers of our preferred3530generating_indices(g,n,d) with the list kppolynomials of kappa-psi-polynomials of3531opposite degree. Returns the tuple (B,kppolynomials), where the latter are3532class:`TautologicalClass`.35333534TESTS::35353536sage: from admcycles.admcycles import kpintersection_matrix3537sage: kpintersection_matrix(0,5,1)[0]3538[5 3 3 3 3]3539[3 1 2 2 2]3540[3 2 1 2 2]3541[3 2 2 1 2]3542[3 2 2 2 1]3543[3 2 2 2 2]3544sage: kpintersection_matrix(0,5,2)[0]3545[1]3546"""3547from .tautological_ring import TautologicalRing3548genindices = generating_indices(g, n, d) # indices of generators corresponding to columns of A3549tg = TautologicalRing(g, n).generators(d)3550gens = [tg[i] for i in genindices]35513552kppolynomials = list(kappapsipolys(g, n, 3 * g - 3 + n - d))35533554B = [[(gen * s).evaluate() for gen in gens] for s in kppolynomials]3555return (matrix(B), kppolynomials)355635573558class prodtautclass:3559r"""3560A tautological class on the product of several spaces \bar M_{g_i, n_i},3561which correspond to the vertices of a stable graph.35623563One way to construct such a tautological class is to pull back an other3564tautological class under a boundary gluing map.35653566Internally, the product class is stored as a list ``self.terms``, where3567each entry is of the form ``[ds(0),ds(1), ..., ds(m)]``, where ``ds(j)``3568are decstratums.35693570Be careful that the marking-names on the ``ds(j)`` are 1,2,3, ...3571corresponding to legs 0,1,2, ... in gamma.legs(j).3572If argument prodtaut is given, it is a list (of length gamma.num_verts())3573of tautclasses with correct leg names and this should produce the3574prodtautclass given as pushforward of the product of these classes that is,3575``self.terms`` is the list of all possible choices of a decstratum from the3576various factors.3577"""35783579def __init__(self, gamma, terms=None, protaut=None):3580self.gamma = gamma.copy(mutable=False)3581if terms is not None:3582self.terms = terms3583elif protaut is not None:3584declists = []3585for t in protaut:3586try:3587declists.append(t.terms)3588except AttributeError:3589declists.append(t._terms.values())3590self.terms = [[ds.copy() for ds in c] for c in itertools.product(*declists)]3591else:3592self.terms = [[decstratum(StableGraph([self.gamma.genera(i)], [list(3593range(1, self.gamma.num_legs(i) + 1))], [])) for i in range(self.gamma.num_verts())]]35943595def copy(self):3596ans = prodtautclass.__new__(prodtautclass)3597ans.gamma = self.gamma3598ans.terms = [[ds.copy() for ds in t] for t in self.terms]3599return ans36003601def _tautological_ring(self):3602r"""3603Return the target tautological ring of the gluing map pushforward.36043605EXAMPLES::36063607sage: from admcycles.admcycles import prodtautclass, StableGraph3608sage: H = StableGraph([2],[[1,2,4]],[])3609sage: b = prodtautclass(H)3610sage: b._tautological_ring()3611TautologicalRing(g=2, n=(1, 2, 4), moduli='st') over Rational Field3612"""3613from .tautological_ring import TautologicalRing3614return TautologicalRing(self.gamma.g(), self.gamma.list_markings())36153616def factors(self):3617r"""3618Return the list of factors of the tensor product.3619"""3620from .tautological_ring import TautologicalRing3621return [TautologicalRing(self.gamma.genera(v), self.gamma.num_legs(v)) for v in range(self.gamma.num_verts())]36223623# returns the pullback of self under the forgetful map which forgot the markings in the list newmark3624# rename: if True, checks if newmark contains leg names already taken for edges3625# def forgetful_pullback(self,newmark,rename=True):3626# return sum([c.forgetful_pullback(newmark,rename) for c in self.terms])36273628def pushforward(self, R=None):3629r"""3630Return the tautological class obtained by pushing forward under the gluing3631map associated to gamma.36323633EXAMPLES::36343635sage: from admcycles.admcycles import prodtautclass, StableGraph, psiclass, fundclass3636sage: G = StableGraph([0, 2], [[1, 2, 4, 3], [5]], [(3, 5)])3637sage: b = prodtautclass(G, protaut = [psiclass(4,0,4), fundclass(2,1)])3638sage: b.pushforward()3639Graph : [0, 2] [[1, 2, 4, 3], [5]] [(3, 5)]3640Polynomial : psi_33641"""3642if R is None:3643R = self._tautological_ring()3644result = R.zero()3645for t in self.terms:3646# we have to combine the graphs from all terms such that there are no collisions of half-edge names3647# * genera will be the concatenation of all t[i].gamma.genera3648# * legs will be the concatenation of renamed versions of the t[i].gamma.legs3649# * edges will be the union of the renamed edge-lists from the elements t[i] and the edge-list of self.gamma3650maxleg = max(self.gamma.leglist() + [0]) + 13651genera = []3652legs = []3653# TODO: to be changed (direct access to "hidden" attributes)3654edges = self.gamma.edges()3655numvert = sum(s.gamma.num_verts() for s in t) # total number of vertices in new graph3656poly = onekppoly(numvert)36573658for i in range(len(t)):3659# s is a decstratum3660s = t[i]3661gr = s.gamma3662start = len(genera)3663genera.extend(gr.genera())36643665# create the renaming-dictionary3666# TODO: to be changed (direct access to _legs and _edges attribute)3667renamedic = {j + 1: self.gamma._legs[i][j] for j in range(len(self.gamma._legs[i]))}3668for (e0, e1) in gr._edges:3669renamedic[e0] = maxleg3670renamedic[e1] = maxleg + 13671maxleg += 236723673legs += [[renamedic[l] for l in ll] for ll in gr._legs]3674edges += [(renamedic[e0], renamedic[e1]) for (e0, e1) in gr._edges]36753676grpoly = s.poly.copy()3677grpoly.rename_legs(renamedic)3678grpoly.expand_vertices(start, numvert)3679poly *= grpoly3680result += R(StableGraph(genera, legs, edges), poly=poly)3681return result36823683def partial_pushforward(self, gamma0, dicv, dicl):3684r"""3685Given a stgraph gamma0 and (dicv,dicl) a morphism from self.gamma to gamma0 (i.e.3686self.gamma is a specialization of gamma0), it returns the prodtautclass on gamma03687obtained by the partial pushforward along corresponding gluing maps.36883689Notice that in this version, one needs to 'unedge' the edges of gamma0 and also3690the corresponding edges in self.gamma; otherwise the method rename_legs will not3691function.36923693INPUT:36943695- gamma0 (StableGraph): the graph obtained by some contraction of edges3696of self.gamma3697- dicv (dict): a dictionary describing the map of vertice of self.gamma3698to vertices gamma03699- dicl (dict): a dictionary describing the map of legs of gamma0 to3700self.gamma37013702EXAMPLES::37033704sage: from admcycles.admcycles import StableGraph, prodtautclass, decstratum3705sage: G = StableGraph([2,0],[[2,3,4],[1,5,6]],[(3,5)])3706sage: G1 = StableGraph([1,1],[[2,10],[1,3,11]],[(10,11)])3707sage: G2 = StableGraph([0],[[1,2,3]],[])3708sage: d1 = decstratum(G1, [[], [0,1]], {1: 1})3709sage: d2 = decstratum(G2)3710sage: E = prodtautclass(G, [[d1, d2]])3711sage: G0 = StableGraph([2],[[1,2,3,4]],[])3712sage: E.partial_pushforward(G0, {0:0,1:0}, {3: 6, 4: 4, 2: 2, 1: 1})3713Outer graph : [2] [[1, 2, 3, 4]] []3714Vertex 0 :3715Graph : [1, 1, 0] [[9, 12], [2, 4, 13], [1, 11, 3]] [(9, 11), (12, 13)]3716Polynomial : (kappa_2)_1*psi_23717371837193720"""3721gamma = self.gamma3722preimv = [[] for v in gamma0.genera(copy=False)]3723for w in dicv:3724preimv[dicv[w]].append(w)3725usedlegs = dicl.values()37263727# now create list of preimage-graphs, where leg-names and orders are preserved so that decstratums can be used unchanged3728preimgr = []3729for v in range(gamma0.num_verts()):3730preimgr.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 (3731gamma.vertex(e[0]) in preimv[v] and gamma.vertex(e[1]) in preimv[v]) and not (e[0] in usedlegs or e[1] in usedlegs)], mutable=True))37323733# now go through self.terms, split decstrata onto preimgr (creating temporary prodtautclasses)3734# push those forward obtaining tautclasses on the moduli spaces corresponding to the vertices of gamma0 (but wrong leg labels)3735# rename them according to the inverse of dicl, then multiply all factors together using distributivity3736result = prodtautclass(gamma0, [])3737rndics = [{dicl[gamma0.legs(v, copy=False)[j]]:j + 1 for j in range(gamma0.num_legs(v))}3738for v in range(gamma0.num_verts())]37393740for v in range(gamma0.num_verts()):3741preimgr[v].rename_legs(rndics[v], shift=preimgr[v]._maxleg, tidyup=False)37423743for t in self.terms:3744tempclasses = [prodtautclass(preimgr[v], [[t[i] for i in preimv[v]]]) for v in range(gamma0.num_verts())]3745temppush = [s.pushforward() for s in tempclasses]3746# for v in range(gamma0.num_verts()):3747# temppush[v].rename_legs(rndics[v])3748for comb in itertools.product(*[tp._terms.values() for tp in temppush]):3749result.terms.append(list(comb))3750return result37513752# 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 themselves3753def toprodHclass(self):3754gamma0 = self.gamma.copy(mutable=False)3755trivgp = PermutationGroup([()])3756trivgpel = trivgp[0]3757result = prodHclass(gamma0, [])37583759for t in self.terms:3760# we have to combine the graphs from all terms such that there are no collisions of half-edge names3761# * genera will be the concatenation of all t[i].gamma.genera3762# * legs will be the concatenation of renamed versions of the t[i].gamma.legs3763# * edges will be the union of the renamed edge-lists from the elements t[i] and the edge-list of self.gamma3764dicv0 = {}3765dicl0 = {l: l for l in self.gamma.leglist()}3766spaces = {}3767vertdata = []37683769maxleg = max(self.gamma.leglist() + [0]) + 13770genera = []3771legs = []3772edges = self.gamma.edges()3773numvert = sum(s.gamma.num_verts() for s in t) # total number of vertices in new graph3774poly = onekppoly(numvert)37753776for i in range(len(t)):3777# s is a decstratum3778s = t[i]3779gr = s.gamma3780start = len(genera)3781genera += gr.genera(copy=False)3782dicv0.update({j: i for j in range(start, len(genera))})37833784# create the renaming-dictionary3785renamedic = {j + 1: self.gamma.legs(i, copy=False)[j] for j in range(self.gamma.num_legs(i))}3786for (e0, e1) in gr.edges():3787renamedic[e0] = maxleg3788renamedic[e1] = maxleg + 13789maxleg += 237903791legs += [[renamedic[l] for l in ll] for ll in gr._legs]3792edges += [(renamedic[e0], renamedic[e1]) for (e0, e1) in gr._edges]3793spaces.update({j: [genera[j], HurwitzData(trivgp, [(trivgpel, 1, 0)3794for counter in range(len(legs[j]))])] for j in range(start, len(genera))})3795vertdata += [[j, {legs[j][lcount]:lcount +37961 for lcount in range(len(legs[j]))}] for j in range(start, len(genera))]37973798grpoly = s.poly.copy()3799grpoly.rename_legs(renamedic)3800grpoly.expand_vertices(start, numvert)3801poly *= grpoly38023803gamma = stgraph(genera, legs, edges)38043805result.terms.append(decHstratum(gamma, spaces, vertdata, dicv0, dicl0, poly))3806return result38073808# 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 prodcl3809# 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.gamma3810# multiply the pullback of these classes accordingly3811def factor_pullback(self, vertices, prodcl):3812other = prodtautclass(self.gamma) # initialize with 1-class3813defaultterms = [other.terms[0][i]3814for i in range(self.gamma.num_verts())] # collect 1-terms on various factors here3815other.terms = []3816for t in prodcl.terms:3817newterm = [ds.copy() for ds in defaultterms]3818for j in range(len(vertices)):3819newterm[vertices[j]] = t[j]3820other.terms.append(newterm)3821return self * other38223823def __neg__(self):3824return (-1) * self38253826def __add__(self, other):3827if other == 0:3828return self.copy()3829new = self.copy()3830new.terms += [[ds.copy() for ds in t] for t in other.terms]3831return new.consolidate()38323833def __radd__(self, other):3834if other == 0:3835return self3836else:3837return self + other38383839def __iadd__(self, other):3840if other == 0:3841return self3842if not isinstance(other, prodtautclass) or other.gamma != self.gamma:3843raise ValueError("summing two prodtautclasses on different graphs!")3844self.terms += other.terms3845return self38463847def __mul__(self, other):3848return self.__rmul__(other)38493850def __imul__(self, other):3851if isinstance(other, prodtautclass):3852self.terms = (self.__rmul__(other)).terms3853return self3854# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):3855else:3856for t in self.terms:3857t[0] *= other3858return self38593860def __rmul__(self, other):3861if isinstance(other, prodtautclass):3862# multiply the decstratums on each vertex together separately3863if other.gamma != self.gamma:3864raise ValueError("product of two prodtautclass on different graphs!")38653866result = prodtautclass(self.gamma, [])3867for T1 in self.terms:3868for T2 in other.terms:3869# go through vertices, multiply decstratums -> obtain tautclasses3870# in the end: must output all possible combinations of those tautclasses3871vertextautcl = []3872for v, Rfac in enumerate(self.factors()):3873vertextautcl.append(list(T1[v].multiply(T2[v], Rfac)._terms.values()))3874for choice in itertools.product(*vertextautcl):3875result += prodtautclass(self.gamma, [list(choice)])3876return result3877# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):3878else:3879new = self.copy()3880for t in new.terms:3881t[0] = other * t[0]3882return new38833884def dimension_filter(self):3885for t in self.terms:3886for s in t:3887s.dimension_filter()3888return self.consolidate()38893890def consolidate(self):3891revrange = list(range(len(self.terms)))3892revrange.reverse()3893for i in revrange:3894for s in self.terms[i]:3895if len(s.poly.monom) == 0:3896self.terms.pop(i)3897break3898return self38993900def __repr__(self):3901s = 'Outer graph : ' + repr(self.gamma) + '\n'3902terms = sorted(self.terms, key=lambda l: [x.gamma for x in l])3903for i in range(len(terms)):3904for j in range(len(terms[i])):3905s += 'Vertex ' + repr(j) + ' :\n' + repr(terms[i][j]) + '\n'3906s += '\n\n'3907return s.rstrip('\n\n')39083909def factor_reconstruct(self, m, otherfactors):3910r"""3911Assuming that the prodtautclass is a product v_0 x ... x v_(s-1) of tautclasses v_i on the3912vertices, this returns the mth factor v_m assuming that otherfactors=[v_1, ..., v_(m-1), v_(m+1), ... v_(s-1)]3913is the list of factors on the vertices not equal to m.39143915EXAMPLES::39163917sage: from admcycles.admcycles import StableGraph, prodtautclass, psiclass, kappaclass, fundclass3918sage: Gamma = StableGraph([1,2,1],[[1,2],[3,4],[5]],[(2,3),(4,5)])3919sage: pt = prodtautclass(Gamma,protaut=[psiclass(1,1,2),kappaclass(2,2,2),fundclass(1,1)])3920sage: res = pt.factor_reconstruct(1,[psiclass(1,1,2),fundclass(1,1)])3921sage: res3922Graph : [2] [[1, 2]] []3923Polynomial : (kappa_2)_039243925NOTE::39263927This method works by computing intersection numbers in the other factors. This could be replaced3928with computing a basis in the tautological ring of these factors.3929In principle, it is also possible to reconstruct all factors v_i (up to scaling) just assuming3930that the prodtautclass is a pure tensor (product of pullbacks from factors).3931"""39323933s = self.gamma.num_verts()3934if not len(otherfactors) == s - 1:3935raise ValueError('otherfactors must have exactly one entry for each vertex not equal to m')3936otherindices = list(range(s))3937otherindices.remove(m)39383939othertestclass = {}3940otherintersection = []3941for i in range(s - 1):3942rv = otherfactors[i].degree_list()[0]3943rvcomplement = otherfactors[i].parent().socle_degree() - rv3944for tc in otherfactors[i].parent().generators(rvcomplement):3945intnum = (tc * otherfactors[i]).evaluate()3946if intnum != 0:3947othertestclass[otherindices[i]] = tc3948otherintersection.append(intnum)3949break3950# R = self._tautological_ring()3951from .tautological_ring import TautologicalRing3952R = TautologicalRing(self.gamma.genera(m), self.gamma.num_legs(m))3953result = R.sum([prod([(t[oi] * othertestclass[oi]).evaluate() for oi in otherindices]) * R([t[m]]) for t in self.terms])3954result.simplify()3955return 1 / prod(otherintersection, QQ(1)) * result39563957# returns the cohomological degree 2r part of self, where we see self as a cohomology vector in3958# H*(\bar M_{g1,n1} x \bar M_{g2,n2} x ... x \bar M_{gm,nm}) with m the number of vertices of self.gamma3959# currently only implemented for m=1 (returns vector) or m=2 (returns list of matrices of length r+1)3960# if vecout=True, in the case m=2 a vector is returned that is the concatenation of all row vectors of the matrices3961# TODO: make r optional argument?3962def totensorTautbasis(self, r, vecout=False):3963if self.gamma.num_verts() == 1:3964g = self.gamma.genera(0)3965n = self.gamma.num_legs(0)3966result = vector(QQ, len(generating_indices(g, n, r)))3967for t in self.terms:3968# t is a one-element list containing a decstratum3969result += t[0].toTautbasis(g, n, r)3970return result3971if self.gamma.num_verts() == 2:3972g1 = self.gamma.genera(0)3973n1 = self.gamma.num_legs(0)3974g2 = self.gamma.genera(1)3975n2 = self.gamma.num_legs(1)3976rmax1 = 3 * g1 - 3 + n13977rmax2 = 3 * g2 - 3 + n239783979rmin = max(0, r - rmax2) # the degree r1 in the first factor can only vary between rmin and rmax3980rmax = min(r, rmax1)39813982result = [matrix(QQ, 0, 0) for i in range(rmin)]39833984for r1 in range(rmin, rmax + 1):3985M = matrix(QQ, len(generating_indices(g1, n1, r1)), len(generating_indices(g2, n2, r - r1)))3986for t in self.terms:3987# t is a list [d1,d2] of two decstratums3988vec1 = t[0].toTautbasis(g1, n1, r1)3989vec2 = t[1].toTautbasis(g2, n2, r - r1)3990M += matrix(vec1).transpose() * matrix(vec2)3991result.append(M)39923993result += [matrix(QQ, 0, 0) for i in range(rmax + 1, r + 1)]39943995if vecout:3996return vector([M[i, j] for M in result for i in range(M.nrows()) for j in range(M.ncols())])3997else:3998return result39994000if self.gamma.num_verts() > 2:4001print('totensorTautbasis not yet implemented on graphs with more than two vertices')4002return 040034004# converts a decstratum or tautclass to a vector in Pixton's basis4005# tries to reconstruct g,n,r from first nonzero term on the first decstratum4006# if they are explicitly given, only extract the part of D that lies in right degree, ignore others400740084009def converttoTautvect(D, g=None, n=None, r=None, moduli='st'):4010# global copyD4011# copyD=(D,g,n,r)4012if isinstance(D, decstratum):4013D.dimension_filter()4014if g is None:4015g = D.gamma.g()4016if n is None:4017n = len(D.gamma.list_markings())4018if len(D.poly.monom) == 0:4019if (r is None):4020print('Unable to identify r for empty decstratum')4021return 04022else:4023return vector(QQ, DR.num_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True)))4024if r is None:4025polydeg = D.poly.deg()4026if polydeg is not None:4027r = D.gamma.num_edges() + polydeg4028# just assume now that g,n,r are given4029length = DR.num_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True))4030markings = tuple(range(1, n + 1))4031result = vector(QQ, length)4032graphdegree = D.gamma.num_edges()4033for (kappa, psi, coeff) in D.poly:4034if graphdegree + sum([sum((j + 1) * kvec[j] for j in range(len(kvec))) for kvec in kappa]) + sum(psi.values()) == r:4035try:4036pare = coeff.parent()4037except AttributeError:4038pare = QQ4039result += vector(pare, length, {DR.num_of_stratum(Pixtongraph(D.gamma, kappa, psi),4040g, r, markings, moduli_type=get_moduli(moduli, DRpy=True)): coeff})4041return result404240434044def Pixtongraph(G, kappa, psi):4045r"""4046Return a Pixton-style graph given a stgraph ``G`` and data ``kappa`` and4047``psi`` as in a :class:`KappaPsiPolynomial`.40484049The function :func:`Graphtodecstratum` provides the conversion in the other direction.40504051EXAMPLES::40524053sage: from admcycles import StableGraph4054sage: from admcycles.admcycles import Pixtongraph, Graphtodecstratum4055sage: st = StableGraph([0, 2], [[1, 2, 4], [3, 5]], [(4, 5)])4056sage: G = Pixtongraph(st, [[], [0, 1]], {1: 1})4057sage: G4058[ -1 1 2 3 0]4059[ 0 X + 1 1 0 1]4060[X^2 + 2 0 0 1 1]4061sage: ds = Graphtodecstratum(G)4062sage: assert ds.gamma.is_isomorphic(st)4063"""4064firstcol = [-1]4065for v in range(G.num_verts()):4066entry = 0 * DR.X + G.genera(v)4067for k in range(len(kappa[v])):4068entry += kappa[v][k] * (DR.X)**(k + 1)4069firstcol.append(entry)4070columns = [firstcol]40714072for l in G.list_markings():4073vert = G.vertex(l)4074entry = 0 * DR.X + 14075if l in psi:4076entry += psi[l] * DR.X4077columns.append([l] + [0 for i in range(vert)] + [entry] + [0 for i in range(G.num_verts() - vert - 1)])4078for (e0, e1) in G.edges(copy=False):4079v0 = G.vertex(e0)4080v1 = G.vertex(e1)4081if v0 == v1:4082entry = 0 * DR.X + 24083if (psi.get(e0, 0) != 0) and (psi.get(e1, 0) == 0):4084entry += psi[e0] * DR.X4085if (psi.get(e1, 0) != 0) and (psi.get(e0, 0) == 0):4086entry += psi[e1] * DR.X4087if (psi.get(e0, 0) != 0) and (psi.get(e1, 0) != 0):4088if psi[e0] >= psi[e1]:4089entry += psi[e0] * DR.X + psi[e1] * (DR.X)**24090else:4091entry += psi[e1] * DR.X + psi[e0] * (DR.X)**24092columns.append([0] + [0 for i in range(v0)] + [entry] + [0 for i in range(G.num_verts() - v0 - 1)])4093else:4094col = [0 for i in range(G.num_verts() + 1)]4095entry = 0 * DR.X + 14096if e0 in psi:4097entry += psi[e0] * DR.X4098col[v0 + 1] = entry40994100entry = 0 * DR.X + 14101if e1 in psi:4102entry += psi[e1] * DR.X4103col[v1 + 1] = entry4104columns.append(col)41054106M = matrix(columns).transpose()4107return DR.Graph(M)41084109# converts vector v in generating set all_strata(g,r,(1,..,n)) to preferred basis computed by generating_indices(g,n,r)411041114112def Tautvecttobasis(v, g, n, r, moduli='st'):4113if (2 * g - 2 + n <= 0) or (r < 0) or (r > socle_degree(g, n, get_moduli(moduli))):4114return vector([])4115if moduli == 'tl':4116res = Tautvecttobasis(v, g, n, r, 'st') # create basis vector on stable locus4117W = op_subset_space(g, n, r, moduli)4118if not res:4119return W.zero()4120reslen = len(res)4121return sum(res[i] * W(vector(QQ, reslen, {i: 1})) for i in range(reslen) if not res[i].is_zero())4122vecs = genstobasis(g, n, r, moduli=moduli)4123assert len(vecs) == len(v), (vecs, v)4124res = vector(QQ, len(vecs[0]))4125for i in range(len(v)):4126if v[i] != 0:4127res += v[i] * vecs[i]4128return res412941304131# 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)41324133@file_cache.file_cached_function(4134directory=os.path.join(DOT_SAGE, "admcycles"),4135url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",4136remote_database_list="https://modulispaces.gitlab.io/relations-database/genstobasis_files",4137env_var="ADMCYCLES_CACHE_DIR",4138pickle_wrappers=(file_cache.rational_vectors_to_py, file_cache.py_to_rational_vectors))4139def genstobasis(g, n, r, moduli='st'):4140r"""4141Returns the list of vectors expressing the generators of `R^r(\bar M_{g,n})` in terms of the4142standard basis.41434144INPUT:41454146moduli : string (default: 'st')4147If instead of 'st' one of 'ct', 'rt' or 'sm' is given, compute a basis expression for the4148tautological ring of the corresponding open subset of `\bar M_{g,n}`.41494150EXAMPLES::41514152sage: from admcycles import TautologicalRing4153sage: from admcycles.admcycles import genstobasis4154sage: genstobasis(1, 1, 1)4155[(1), (1), (24)]4156sage: [t.evaluate() for t in TautologicalRing(1,1).generators(1)]4157[1/24, 1/24, 1]4158sage: genstobasis(2, 1, 1, moduli='ct')4159[(1, 0), (0, 1), (5/7, -5/7)]4160sage: genstobasis(9, 0, 3, moduli='sm')4161[(1, 0, 0), (0, 1, 0), (0, 0, 1)]41624163TESTS:41644165Test the oneline database::41664167sage: from admcycles import generating_indices4168sage: from tempfile import mkdtemp4169sage: from shutil import rmtree4170sage: import os4171sage: import pickle4172sage: genstobasis.set_online_lookup(True)4173sage: filename = "genstobasis_0_4_0_st.pkl"4174sage: tmpdir = mkdtemp()4175sage: filename_with_path = os.path.join(tmpdir, filename)4176sage: genstobasis._FileCachedFunction__download(filename, filename_with_path) # optional - internet4177sage: f = open(filename_with_path, "rb") # optional - internet4178sage: pickle.load(f) # optional - internet4179[(1)]4180sage: rmtree(tmpdir)4181"""4182# We need to work around a caching issue:4183# If generating_indices is in the cache, the call to generatig_indices4184# will return the cached value without actually running the function,4185# in particular without setting the cache for genstobasis.4186# If at the same time genstobasis is not in the cache4187# (which is the case if this function is actually executed), we have4188# created an infinite loop.4189# In this case we must force generating_indices to be executed.4190if (g, n, r, moduli) in generating_indices.cache:4191generating_indices.f(g, n, r, True, moduli=moduli)4192else:4193generating_indices(g, n, r, True, moduli=moduli)4194return genstobasis(g, n, r, moduli=moduli)419541964197def Tautv_to_tautclass(v, g, n, r, moduli='st'):4198r"""4199Deprecated. Use :func:`admcycles.tautological_ring.TautologicalRing.from_vector` instead.4200"""4201from .superseded import deprecation4202deprecation(109, 'Tautv_to_tautclass is deprecated. Please use the from_vector method from TautologicalRing instead.')4203from .tautological_ring import TautologicalRing4204return TautologicalRing(g, n, moduli).from_vector(v, r)420542064207def Tautvb_to_tautclass(v, g, n, r):4208r"""4209Deprecated. Use :func:`admcycles.tautological_ring.TautologicalRing.from_basis_vector` instead.4210"""4211from .superseded import deprecation4212deprecation(109, 'Tautvb_to_tautclass is deprecated. Please use the from_vector method from TautologicalRing instead.')4213from .tautological_ring import TautologicalRing4214return TautologicalRing(g, n).from_basis_vector(v, r)421542164217class prodHclass:4218r"""4219A sum of gluing pushforwards of pushforwards of fundamental classes of4220Hurwitz spaces under products of forgetful morphisms.42214222This is all relative to a fixed stable graph gamma0.4223"""42244225def __init__(self, gamma0, terms):4226self.gamma0 = gamma0.copy(mutable=False)4227self.terms = terms42284229def __neg__(self):4230return (-1) * self42314232def __add__(self, other):4233if other == 0:4234return deepcopy(self)4235if isinstance(other, prodHclass) and other.gamma0 == self.gamma0:4236new = deepcopy(self)4237new.terms += deepcopy(other.terms)4238return new.consolidate()42394240def __radd__(self, other):4241if other == 0:4242return self4243else:4244return self + other42454246def __iadd__(self, other):4247if other == 0:4248return self4249if isinstance(other, prodHclass) and other.gamma0 == self.gamma0:4250self.terms += other.terms # TODO: should we use a deepcopy here?4251else:4252raise ValueError("sum of prodHclasses on different graphs")4253return self42544255def __mul__(self, other):4256from .tautological_ring import TautologicalClass4257if isinstance(other, (TautologicalClass, decstratum)):4258return self.__rmul__(other)4259# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):4260else:4261new = deepcopy(self)4262for t in new.terms:4263t[0] = other * t[0]4264return new42654266def __rmul__(self, other):4267from .tautological_ring import TautologicalClass4268if isinstance(other, (TautologicalClass, decstratum)):4269pbother = self.gamma0.boundary_pullback(other) # this is a prodtautclass on self.gamma0 now4270# 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!4271pbother = pbother.toprodHclass()4272result = prodHclass(deepcopy(self.gamma0), [])4273for t in pbother.terms:4274# t is a decHstratum and (t.dicv0,t.dicl0) is a morphism from t.gamma to self.gamma04275# we pull back self along this morphism to get a prodHclass on t.gamma4276temppullback = self.gamma0_pullback(t.gamma, t.dicv0, t.dicl0)42774278# now it remains to distribute the kappa and psi-classes from t.poly (living on t.gamma) to the terms of temppullback4279for s in temppullback.terms:4280# s is now a decHstratum and (s.dicv0,s.dicl0) describes a map s.gamma -> t.gamma4281s.poly *= t.poly.graphpullback(s.dicv0, s.dicl0)4282# rearrange s to be again a decHstratum with respect to gamma04283s.dicv0 = {v: t.dicv0[s.dicv0[v]] for v in s.dicv0}4284s.dicl0 = {l: s.dicl0[t.dicl0[l]] for l in t.dicl0}4285result.terms += temppullback.terms4286return result4287# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):4288else:4289new = deepcopy(self)4290for t in new.terms:4291t[0] = other * t[0]4292return new42934294# evaluates self against the fundamental class of the ambient space (of self.gamma0), returns a rational number42954296def evaluate(self):4297return sum([t.evaluate() for t in self.terms])42984299# tries to convert self into a prodtautclass on the graph gamma04300# Note: since not all Hurwitz spaces have tautological fundamental class and since not all diagonals have tautological Kunneth decompositions, this will not always work43014302def toprodtautclass(self):4303result = prodtautclass(self.gamma0, [])4304for t in self.terms:4305# create a prodtautclass on t.gamma, then do partial pushforward under (t.dicv0,t.dicl0)4306tempres = decstratum(deepcopy(t.gamma), poly=deepcopy(t.poly))4307tempres = tempres.convert_to_prodtautclass()43084309# now group vertices of t.gamma according to the spaces they access4310# create diagonals and insert the identification of the Hurwitz stack fundamental class4311# make sure that for this you use the version forgetting the most markings possible4312spacelist = {a: [] for a in t.spaces}4313for v in range(t.gamma.num_verts()):4314spacelist[t.vertdata[v][0]].append(v)4315for a in spacelist:4316if not spacelist[a]:4317spacelist.pop(a)43184319for a in spacelist:4320# find out which of the markings 1, .., N of the space containing the Hurwitz cycle are used4321usedmarks = set()4322for v in spacelist[a]:4323usedmarks.update(set(t.vertdata[v][1].values()))4324usedmarks = sorted(usedmarks)4325rndic = {usedmarks[i]: i + 1 for i in range(len(usedmarks))}43264327Hclass = Hidentify(t.spaces[a][0], t.spaces[a][1], markings=usedmarks)4328Hclass = Hclass.rename_legs(rndic, inplace=False)43294330legdics = [{l: rndic[t.vertdata[v][1][l]] for l in t.gamma.legs(v, copy=False)} for v in spacelist[a]]4331diagclass = forgetful_diagonal(t.spaces[a][0], len(4332usedmarks), [t.gamma.legs(v, copy=False) for v in spacelist[a]], legdics, T=Hclass)43334334tempres = tempres.factor_pullback(spacelist[a], diagclass)43354336tempres = tempres.partial_pushforward(self.gamma0, t.dicv0, t.dicl0)4337result += tempres4338return result43394340# 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 gamma14341def gamma0_pullback(self, gamma1, dicv=None, dicl=None):4342gamma0 = self.gamma043434344if dicv is None:4345if gamma0.num_verts() == 1:4346dicv = {v: 0 for v in range(gamma1.num_verts())}4347else:4348raise RuntimeError('dicv not uniquely determined')4349if dicl is None:4350if gamma0.num_edges() == 0:4351dicl = {l: l for l in gamma0.leglist()}4352else:4353raise RuntimeError('dicl not uniquely determined')43544355# Step 1: factor the map gamma1 -> gamma0 in a series of 1-edge degenerations4356# TODO: choose a smart order to minimize computations later (i.e. start w/ separating edges with roughly equal genus on both sides43574358imdicl = dicl.values()4359extraedges = [e for e in gamma1.edges(copy=False) if e[0] not in imdicl]4360delta_e = gamma1.num_edges() - gamma0.num_edges()4361if delta_e != len(extraedges):4362print('Warning: edge numbers')4363global exampullb4364exampullb = (deepcopy(self), deepcopy(gamma1), deepcopy(dicv), deepcopy(dicl))4365raise ValueError('Edge numbers')4366edgeorder = list(range(delta_e))4367contredges = [extraedges[i] for i in edgeorder]43684369contrgraph = [gamma1]4370actvert = []4371edgegraphs = []4372vnumbers = []4373contrdicts = []4374vimages = [dicv[v] for v in range(gamma1.num_verts())]4375count = 04376for e in contredges:4377gammatild = contrgraph[count].copy()4378(av, edgegraph, vnum, diccv) = gammatild.contract_edge(e, adddata=True)4379gammatild.set_immutable()4380contrgraph.append(gammatild)4381actvert.append(av)4382edgegraphs.append(edgegraph)4383vnumbers.append(vnum)4384contrdicts.append(diccv)4385if len(vnum) == 2:4386vimages.pop(vnum[1])4387count += 14388contrgraph.reverse()4389actvert.reverse()4390edgegraphs.reverse()4391vnumbers.reverse()4392contrdicts.reverse()43934394# Step 2: pull back the decHstratum t one edge-degeneration at a time4395# Note that this will convert the decHstratum into a prodHclass, so we need to take care of all the different terms in each step43964397# First we need to adapt (a copy of) self so that we take into account that gamma0 is isomorphic to the contracted version of gamma14398# The gamma0-structure on all terms of self must be modified to be a contrgraph[0]-structure4399result = deepcopy(self)4400result.gamma0 = contrgraph[0]4401# gives isomorphism V(gamma0) -> V(contrgraph[0])4402dicvisom = {dicv[vimages[v]]: v for v in range(gamma0.num_verts())}4403diclisom = {dicl[l]: l for l in dicl} # gives isomorphism L(contrgraph[0]) -> L(gamma0)4404for t in result.terms:4405t.dicv0 = {v: dicvisom[t.dicv0[v]] for v in t.dicv0}4406t.dicl0 = {l: t.dicl0[diclisom[l]] for l in diclisom}4407# dicv, dicl should have served their purpose and should no longer appear below44084409for edgenumber in range(delta_e):4410newresult = prodHclass(contrgraph[edgenumber + 1], [])4411gam0 = contrgraph[edgenumber] # current gamma0, we now pull back under contrgraph[edgenumber+1] -> gam04412av = actvert[edgenumber] # vertex in gam0 where action happens4413diccv = contrdicts[edgenumber] # vertex dictionary for contrgraph[edgenumber+1] -> gam04414# section of diccv: bijective if loop is added, otherwise assigns one of the preimages to the active vertex4415diccvinv = {diccv[v]: v for v in diccv}4416vextractdic = {v: vnumbers[edgenumber][v] for v in range(len(vnumbers[edgenumber]))}44174418for t in result.terms:4419gamma = t.gamma44204421# Step 2.1: extract the preimage of the active vertex inside t.gamma4422actvertices = [v for v in range(gamma.num_verts()) if t.dicv0[v] == av]4423# global exsubvar4424# exsubvar=(t,gamma,actvertices,gam0, av)4425# 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))]4426# outlegs=set(gam0.legs(av, copy=False))-set([e[0] for e in avloops] + [e[1] for e in avloops])4427(gammaprime, dicvextract, diclextract) = gamma.extract_subgraph(actvertices,4428outgoing_legs=[t.dicl0[l] for l in gam0.legs(av, copy=False)], rename=False)44294430dicvextractinv = {dicvextract[v]: v for v in dicvextract}4431# dicvextract maps vertex numbers in gamma to vertex-numbers in gammaprime4432# diclextract not needed since rename=False44334434# Step 2.2: find the common degenerations of this preimage and the one-edge graph relevant for this step4435# We need to rename edgegraphs[edgenumber] to match the leg-names in gammaprime4436egraph = edgegraphs[edgenumber].copy()4437eshift = max(list(t.dicl0.values()) + [0]) + 14438egraph.rename_legs(t.dicl0, shift=eshift)4439egraph.set_immutable()44404441# (gammaprime,dicvextract,diclextract) =gamma.extract_subgraph(actvertices,outgoing_legs=[l for l in egraph.list_markings()],rename=False)44424443# dicvextractinv={dicvextract[v]:v for v in dicvextract}4444# dicvextract maps vertex numbers in gamma to vertex-numbers in gammaprime4445# diclextract not needed since rename=False44464447commdeg = common_degenerations(gammaprime, egraph, modiso=True, rename=True)44484449(e0, e1) = egraph.edges(copy=False)[0]4450for (gammadeg, dv1, dl1, dv2, dl2) in commdeg:4451if gammadeg.num_edges() == gammaprime.num_edges():4452# Step 2.3a: if gammadeg isomorphic to gammaprime, we have excess intersection, introducing -psi - psi'4453term = deepcopy(t)4454numvert = gamma.num_verts()4455dl1inverse = {dl1[l]: l for l in dl1}4456term.poly *= -(psicl(dl1inverse[dl2[e0]], numvert) + psicl(dl1inverse[dl2[e1]], numvert))4457# adapt term.dicv0 and term.dicl044584459# term.dicv0 must be lifted along the contraction map using diccvinv outside of the active area gammaprime; there we use dv24460dv1inverse = {dv1[v]: v for v in dv1} # TODO: is this what we want?4461term.dicv0 = {v: diccvinv[term.dicv0[v]] for v in term.dicv0}4462term.dicv0.update({v: vextractdic[dv2[dv1inverse[dicvextract[v]]]] for v in actvertices})44634464# term.dicl0 should now assign to the two half-edges that are new the corresponding edge in term.gamma4465term.dicl0[e0 - eshift] = dl1inverse[dl2[e0]]4466term.dicl0[e1 - eshift] = dl1inverse[dl2[e1]]44674468newresult.terms.append(term)4469else:4470# Step 2.3b: else there is a real degeneration going on4471# This means we need to find the vertex of gamma where this new edge would appear,4472# then check if the Hurwitz space glued to this spot admits a compatible degeneration.4473# Note that here we must take note that we possibly forgot some markings of this Hurwitz space.44744475vactiveprime = dv1[gammadeg.vertex(dl2[e0])] # vertex in gammaprime where degeneration occurs4476vactive = dicvextractinv[vactiveprime] # vertex in gamma where degeneration occurs44774478# 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 if4479# compatible with the boundary partition4480(a, spacedicl) = t.vertdata[vactive]4481(gsp, Hsp) = t.spaces[a]4482numHmarks = trivGgraph(gsp, Hsp).gamma.num_legs(0)44834484# Hbdry=list_Hstrata(gsp,Hsp,1) # these are the Gstgraphs which might get glued in at vertex vactive (+ other vertices accessing space a)4485# if Hbdry==[]:4486# continue # we would require a degeneration, but none can occur4487# numHmarks=len(Hbdry[0].gamma.list_markings())4488# presentmarks=spacedicl.values()4489# forgetmarks=[i for i in range(1,numHmarks) if i not in presentmarks]44904491# now create list divlist of divisors D living in the space of Hbdry such that we can start enumerating D-structures on elements of Hbdry4492# first we must extract the original boundary graph, which is a subgraph of gammadeg on the preimages of vactiveprime4493vactiveprimepreim = [gammadeg.vertex(dl2[e0]), gammadeg.vertex(dl2[e1])]4494if vactiveprimepreim[0] == vactiveprimepreim[1]:4495vactiveprimepreim.pop(1)4496# old, possibly faulty: (egr,dvex,dlex) =gammadeg.extract_subgraph(vactiveprimepreim,outgoing_legs=gammaprime.legs(vactiveprime),rename=False)4497(egr, dvex, dlex) = gammadeg.extract_subgraph(vactiveprimepreim, outgoing_legs=[4498dl1[le] for le in gammaprime.legs(vactiveprime, copy=False)], rename=False, mutable=True)44994500# rename egr to fit in the space of Hbdry4501rndic = {dl1[l]: spacedicl[l] for l in gammaprime.legs(vactiveprime, copy=False)}4502egr.rename_legs(rndic, shift=numHmarks + 1)4503egr.set_immutable()45044505degenlist = Hbdrystructures(gsp, Hsp, egr)45064507for (Gr, mult, degendicv, degendicl) in degenlist:4508term = deepcopy(t)4509e = egr.edges(copy=False)[0]4510speciale_im = term.replace_space_by_Gstgraph(4511a, Gr, specialv=vactive, speciale=(degendicl[e[0]], degendicl[e[1]]))4512term.poly *= mult4513# term is still a decHstratum with correct gamma, spaces, vertdata and poly, BUT still referencing to ga04514# now repair dicv0, dicl045154516# term.dicl0 should now assign to the two half-edges that are new the corresponding edge in term.gamma4517dl1inverse = {dl1[l]: l for l in dl1}4518eindex = e.index(dl2[e0] + numHmarks + 1)4519term.dicl0[e0 - eshift] = speciale_im[eindex]4520term.dicl0[e1 - eshift] = speciale_im[1 - eindex]45214522# term.dicv0 is now reconstructed from dicl04523# TODO: add more bookkeeping to do this directly???4524term.dicv0 = dicv_reconstruct(term.gamma, contrgraph[edgenumber + 1], term.dicl0)45254526newresult.terms.append(term)45274528result = newresult45294530return result45314532# def dimension_filter(self):4533# for t in self.terms:4534# for s in t:4535# s.dimension_filter()4536# return self.consolidate()45374538def consolidate(self):4539# revrange=range(len(self.terms))4540# revrange.reverse()4541# for i in revrange:4542# for s in self.terms[i]:4543# if len(s.poly.monom)==0:4544# self.terms.pop(i)4545# break4546return self45474548def __repr__(self):4549s = 'Outer graph : ' + repr(self.gamma0) + '\n'4550for t in self.terms:4551s += repr(t) + '\n\n'4552# for i in range(len(self.terms)):4553# for j in range(len(self.terms[i])):4554# s+='Vertex '+repr(j)+' :\n'+repr(self.terms[i][j])+'\n'4555# s+='\n\n'4556return s.rstrip('\n\n')45574558# 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)4559# 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, where4560# * Gr is a Gstgraph4561# * mult is a rational number giving an appropriate multiplicity for the intersection4562# * dicv is a surjective dictionary of the vertices of Gr.gamma to the vertices of bdry4563# * dicl is an injection from the legs of bdry to the legs of Gr.gamma456445654566def Hbdrystructures(g, H, bdry):4567preHbdry, N = preHbdrystructures(g, H)45684569if bdry.num_verts() == 1:4570try:4571tempresult = preHbdry[(g)]4572except KeyError:4573return []4574(e0, e1) = bdry.edges(copy=False)[0]4575result = []4576for (Gr, mult, dicv, dicl) in tempresult:4577result.append([Gr, mult, dicv, {e0: dicl[N + 1], e1:dicl[N + 2]}])4578return result45794580if bdry.num_verts() == 2:4581presentmarks = bdry.list_markings()4582forgetmarks = [i for i in range(1, N + 1) if i not in presentmarks]4583possi = [[0, 1] for j in forgetmarks]45844585(e0, e1) = bdry.edges(copy=False)[0]4586lgs = bdry.legs(copy=True)4587ve0 = bdry.vertex(e0)4588lgs[ve0].remove(e0) # leglists stripped of the legs forming the edge of bdry4589lgs[1 - ve0].remove(e1)45904591result = []45924593for distri in itertools.product(*possi):4594# 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)4595lgscpy = deepcopy(lgs)4596lgscpy[0] += [forgetmarks[j] for j in range(len(forgetmarks)) if distri[j] == 0]4597lgscpy[1] += [forgetmarks[j] for j in range(len(forgetmarks)) if distri[j] == 1]45984599if bdry.genera(0) > bdry.genera(1) or (bdry.genera(0) == bdry.genera(1) and 1 in lgscpy[1]):4600vert = 14601else:4602vert = 04603ed = (ve0 + vert) % 246044605try:4606tempresult = preHbdry[(bdry.genera(vert), tuple(sorted(lgscpy[vert])))]4607for (Gr, mult, dicv, dicl) in tempresult:4608result.append((Gr, mult, {j: (dicv[j] + vert) % 2 for j in dicv},4609{e0: dicl[N + 1 + ed], e1: dicl[N + 2 - ed]}))4610except KeyError:4611pass4612return result46134614# 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)]]4615# 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 ordered4616# For the unique boundary divisor parametrizing irreducible curves (having a self-node), we reserve the key (g)461746184619@cached_function4620def preHbdrystructures(g, H):4621Hbdry = list_Hstrata(g, H, 1)4622if len(Hbdry) == 0:4623return ({}, len(trivGgraph(g, H).gamma.list_markings()))4624N = len(Hbdry[0].gamma.list_markings())4625result = {}4626for Gr in Hbdry:4627AutGr = equiGraphIsom(Gr, Gr)46284629edgs = Gr.gamma.edges()46304631while edgs:4632econtr = edgs[0] # this edge will remain after contraction46334634# now go through AutGr and eliminate all other edges which are in orbit of econtr, record multiplicity4635multiplicity = 04636for (dicv, dicl) in AutGr:4637try:4638edgs.remove((dicl[econtr[0]], dicl[econtr[1]]))4639multiplicity += 14640except (KeyError, ValueError):4641pass46424643# now contract all except econtr and add the necessary data to the dictionary result4644contrGr = Gr.gamma.copy(mutable=True)4645diccv = {j: j for j in range(contrGr.num_verts())}4646for e in Gr.gamma.edges(copy=False):4647if e != econtr:4648(v0, d1, d2, diccv_new) = contrGr.contract_edge(e, True)4649diccv = {j: diccv_new[diccv[j]] for j in diccv}46504651# OLD: multiplicity*=len(GraphIsom(contrGr,contrGr))4652# NOTE: multiplicity must be multiplied by the number of automorphisms of the boundary stratum, 2 in this case4653# this compensates for the fact that these automorphisms give rise to different bdry-structures on Gr46544655if contrGr.num_verts() == 1:4656# it remains a self-loop4657if g not in result:4658result[(g)] = []4659result[(g)].append([Gr, QQ(multiplicity) / len(AutGr),4660{j: 0 for j in range(Gr.gamma.num_verts())}, {N + 1: econtr[0], N + 2: econtr[1]}])4661result[(g)].append([Gr, QQ(multiplicity) / len(AutGr),4662{j: 0 for j in range(Gr.gamma.num_verts())}, {N + 2: econtr[0], N + 1: econtr[1]}])46634664else:4665# we obtain a separating boundary; now we must distinguish cases to ensure that g' <= g-g' and for equality m_1=14666if contrGr.genera(0) > contrGr.genera(1) or (contrGr.genera(0) == contrGr.genera(1) and 1 in contrGr.legs(1, copy=False)):4667switched = 14668else:4669switched = 046704671# eswitched=0 means econtr[0] is in the vertex with g',[m_1,..]; eswitched=1 means it is at the other vertex4672eswitched = (contrGr.vertex(econtr[0]) + switched) % 246734674gprime = contrGr.genera(switched)4675markns = contrGr.legs(switched, copy=True)4676markns.remove(econtr[eswitched])4677markns.sort()4678markns = tuple(markns)46794680try:4681result[(gprime, markns)].append([Gr, QQ(multiplicity) / len(AutGr),4682{v: (diccv[v] + switched) % 2 for v in diccv}, {N + 1: econtr[eswitched], N + 2: econtr[1 - eswitched]}])4683except KeyError:4684result[(gprime, markns)] = [[Gr, QQ(multiplicity) / len(AutGr),4685{v: (diccv[v] + switched) % 2 for v in diccv}, {N + 1: econtr[eswitched], N + 2: econtr[1 - eswitched]}]]46864687if contrGr.genera(0) == contrGr.genera(1) and N == 0:4688# in this case the resulting boundary divisor has an automorphism, so we need to record also the switched version of the above bdry-structure4689result[(gprime, markns)].append([Gr, QQ(multiplicity) / len(AutGr),4690{v: (diccv[v] + 1 - switched) % 2 for v in diccv}, {N + 2: econtr[eswitched], N + 1: econtr[1 - eswitched]}])46914692return (result, N)469346944695# summands in prodHclass4696# * gamma is a stgraph on which this summand lives4697# * 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 flexibility4698# * 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)4699# * poly is a kppoly on gamma4700# * dicv0 is a surjective dictionary mapping vertices of gamma to vertices of the including prodHclass4701# * dicl0 is an injective dictionary mapping legs of gamma0 to legs of gamma4702class decHstratum:4703def __init__(self, gamma, spaces, vertdata, dicv0=None, dicl0=None, poly=None):4704self.gamma = gamma4705self.spaces = spaces4706self.vertdata = vertdata4707if dicv0 is None:4708self.dicv0 = {v: 0 for v in range(gamma.num_verts())}4709else:4710self.dicv0 = dicv047114712if dicl0 is None:4713self.dicl0 = {l: l for l in gamma.list_markings()}4714else:4715self.dicl0 = dicl047164717if poly is None:4718self.poly = onekppoly(gamma.num_verts())4719else:4720self.poly = poly47214722def __repr__(self):4723return repr((self.gamma, self.spaces, self.vertdata, self.dicv0, self.dicl0, self.poly))47244725def __neg__(self):4726return (-1) * self47274728def __rmul__(self, other):4729# if isinstance(other,sage.rings.integer.Integer) or isinstance(other,sage.rings.rational.Rational) or isinstance(other,int):4730new = deepcopy(self)4731new.poly *= other4732return new47334734# 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.gamma4735# 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])4736# 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())4737# this function is meant as an auxiliary function for self.evaluate()4738def prodforgetpullback(self, spacelist):4739alist = list(spacelist)4740decs = decstratum(self.gamma, poly=self.poly)4741splitdecs = decs.split()47424743# find number of markings of spaces M_i4744N = {a: self.spaces[a][1].nummarks() for a in spacelist}4745forgetlegs = [list(set(range(1, N[self.vertdata[v][0]] + 1)) - set(self.vertdata[v][1].values()))4746for v in range(self.gamma.num_verts())]4747resultgraph = stgraph([self.spaces[a][0] for a in spacelist], [list(range(1, N[a] + 1)) for a in N], [])47484749# rename splitted kppolys to match the standard names of markings on the M_i and wrap them in decstrata4750trivgraphs = [stgraph([self.gamma.genera(i)], [list(self.vertdata[i][1].values())], [])4751for i in range(self.gamma.num_verts())]4752splitdecs = [[s[i].rename_legs(self.vertdata[i][1]) for i in range(len(s))] for s in splitdecs]4753splitdecs = [[decstratum(trivgraphs[i], poly=s[i]) for i in range(len(s))] for s in splitdecs]47544755result = prodtautclass(resultgraph, [])4756for s in splitdecs:4757term = prodtautclass(resultgraph)4758for j in range(len(alist)):4759t = prod([s[i].forgetful_pullback(forgetlegs[i]) for i in spacelist[alist[j]]])4760t.dimension_filter()4761t = t.toprodtautclass()4762term = term.factor_pullback([j], t)4763result += term4764return result47654766# evaluates self against the fundamental class of the ambient space (of self.gamma), returns a rational number47674768def evaluate(self):4769spacelist = {a: [] for a in self.spaces}4770for v in range(self.gamma.num_verts()):4771spacelist[self.vertdata[v][0]].append(v)4772for a in spacelist:4773if not spacelist[a]:4774spacelist.pop(a)4775alist = list(spacelist)47764777pfp = self.prodforgetpullback(spacelist)4778Gr = [trivGgraph(*self.spaces[a]) for a in spacelist]4779prodGr = [prodHclass(gr.gamma, [gr.to_decHstratum()]) for gr in Gr]4780result = 04781for t in pfp.terms:4782tempresult = 14783for i in range(len(alist)):4784if not t[i].gamma.edges(): # decstratum t[i] is a pure kppoly4785tempresult *= (Hdecstratum(Gr[i], poly=t[i].poly)).quotient_pushforward().evaluate()4786else:4787tempresult *= (prodGr[i] * t[i]).evaluate()4788result += tempresult4789return result47904791# replaces the fundamental class of the Hurwitz space with label a with the (fundamental class of the stratum corresponding to) Gstgraph Gr4792# 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 stabilizing4793# 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)4794# IMPORTANT: here we assume that speciale is not contracted in the forgetful-stabilization process!4795def replace_space_by_Gstgraph(self, a, Gr, specialv=None, speciale=None):4796# global examinrep4797# examinrep=(deepcopy(self),a,deepcopy(Gr),specialv,speciale)47984799avertices = [v for v in range(self.gamma.num_verts()) if self.vertdata[v][0] == a]4800avertices.reverse()48014802gluein = Gr.to_decHstratum()48034804maxspacelabel = max(self.spaces)4805self.spaces.pop(a)4806self.spaces.update({l + maxspacelabel + 1: gluein.spaces[l] for l in gluein.spaces})48074808for v in avertices:4809# 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 where48104811gluegraph = gluein.gamma.copy()4812dicl_v = self.vertdata[v][1]48134814usedlegs = dicl_v.values()4815unusedlegs = [l for l in gluegraph.list_markings() if l not in usedlegs]48164817gluegraph.forget_markings(unusedlegs)48184819(forgetdicv, forgetdicl, forgetdich) = gluegraph.stabilize()48204821forgetdicl.update({l: l for l in gluegraph.leglist() if l not in forgetdicl})48224823# now we must rename the legs in gluegraph so they fit with the leg-names in self.gamma, using the inverse of dicl_v4824dicl_v_inverse = {dicl_v[l]: l for l in dicl_v}4825shift = max(list(dicl_v) + [0]) + 14826gluegraph.rename_legs(dicl_v_inverse, shift)48274828divGr = {}4829divs = {}4830dil = {}4831numvert_old = self.gamma.num_verts()4832num_newvert = gluegraph.num_verts()4833dic_voutgoing = {l: l for l in self.gamma.legs(v, copy=False)}4834self.gamma = self.gamma.copy()4835self.gamma.glue_vertex(v, gluegraph, divGr, divs, dil)4836self.gamma.set_immutable()4837dil.update(dic_voutgoing)4838dil_inverse = {dil[l]: l for l in dil}48394840if specialv == v:4841speciale_im = (dil[forgetdich.get(speciale[0], speciale[0]) + shift],4842dil[forgetdich.get(speciale[1], speciale[1]) + shift])48434844# now repair vertdata: note that new vertex list is obtained by deleting v and appending vertex list of gluegraph4845# 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 adapted4846self.vertdata.pop(v)4847for w in range(num_newvert):4848# forgetdicv[w] is the vertex number that w corresponded to before stabilizing, inside gluein4849(pre_b, pre_diclb) = gluein.vertdata[forgetdicv[w]]48504851b = pre_b + maxspacelabel + 14852# pre_diclb is injective dictionary from legs of vertex forgetdicv[w] in gluein.gamma to the standard-leg-numbers in self.spaces[b]4853# thus we need to reverse the shift/rename and the glue_vertex-rename and compose with diclb4854diclb = {l: pre_diclb[forgetdicl[dicl_v.get(dil_inverse[l], dil_inverse[l] - shift)]]4855for l in self.gamma.legs(w + numvert_old - 1, copy=False)}48564857self.vertdata.append([b, diclb])48584859# now repair dicv0, the surjective map from vertices of self.gamma to the gamma0 of the containing prodHclass4860# vertex v went to some vertex w, now all vertices that have replaced v must go to w4861# unfortunately, all vertices v+1, v+2, ... have shifted down by 1, so this must be taken care of4862w = self.dicv0.pop(v)4863for vnumb in range(v, numvert_old - 1):4864self.dicv0[vnumb] = self.dicv0[vnumb + 1]4865for vnumb in range(numvert_old - 1, numvert_old - 1 + num_newvert):4866self.dicv0[vnumb] = w48674868# now repair dicl0, the injective map from leg-names of gamma0 to leg-names of self.gamma4869# ACTUALLY: since gluing does not change any of the old leg-names, dicl0 is already up to date!48704871# now repair self.poly, term by term, collecting the results in newpoly, which replaces self.poly at the end4872newpoly = kppoly([], [])4873for (kappa, psi, coeff) in self.poly:4874trunckappa = [l[:] for l in kappa]4875kappav = trunckappa.pop(v)4876trunckappa += [[] for i in range(num_newvert)]4877trunckppoly = kppoly([(trunckappa, psi)], [coeff])4878trunckppoly *= prod([(sum([kappacl(numvert_old - 1 + k, j + 1, numvert_old + num_newvert - 1)4879for k in range(num_newvert)]))**kappav[j] for j in range(len(kappav))])4880newpoly += trunckppoly48814882self.poly = newpoly48834884spacesused = {a: False for a in self.spaces}4885for a, _ in self.vertdata:4886spacesused[a] = True4887unusedspaces = [a for a in spacesused if not spacesused[a]]48884889degree = 14890for a in unusedspaces:4891trivGraph = trivGgraph(*self.spaces.pop(a))4892if trivGraph.dim() == 0:4893degree *= trivGraph.delta_degree(0)4894else:4895degree = 04896break48974898self.poly *= degree48994900if specialv is not None:4901return speciale_im49024903# takes genus g, number n of markings and lists leglists, legdics of length r (and possibly a tautclass T on \bar M_{g,n})4904# for i=0,..,r-1 the dictionary legdics[i] is an injection of the list leglists[i] in {1,..,n}4905# 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 map4906# this map exactly forgets in the ith factor all markings not in the image of legdics[i]4907# afterwards it renames the markings on the ith factor such that marking legdics[i][j] is called j4908# 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)490949104911def forgetful_diagonal(g, n, leglists, legdics, T=None):4912# global inpu4913# inpu = (g,n,leglists,legdics,T)4914r = len(leglists)4915if T is None:4916T = tautclass([decstratum(stgraph([g], [list(range(1, n + 1))], []))])49174918gamma = stgraph([g for i in range(r)], [list(range(1, n + 1)) for i in range(r)], [])4919result = prodtautclass(gamma, [[decstratum(stgraph([g], [list(range(1, n + 1))], [])) for i in range(r)]])49204921# We want to obtain small diagonal by intersecting Delta_12, Delta_13, ..., Delta_1r4922# 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)4923forgetlegs = [[j for j in range(1, n + 1) if j not in legdics[i].values()] for i in range(r)]4924rndics = [{legdics[i][leglists[i][j]]: j + 1 for j in range(len(leglists[i]))} for i in range(r)]49254926if r > 1:4927# TODO: check that everything is tautological more carefully, only checking those degrees that are really crucial4928for rdeg in range(0, 2 * (3 * g - 3 + n) + 1):4929if not cohom_is_taut(g, n, rdeg):4930print("In computation of diagonal : H^%s(bar M_%s,%s) not necessarily tautological" % (rdeg, g, n))49314932# now compute diagonal4933Dgamma = stgraph([g, g], [list(range(1, n + 1)), list(range(1, n + 1))], [])4934Delta = prodtautclass(Dgamma, [])4935for deg in range(0, (3 * g - 3 + n) + 1):4936gi1 = generating_indices(g, n, deg)4937gi2 = generating_indices(g, n, 3 * g - 3 + n - deg)4938strata1 = DR.all_strata(g, deg, tuple(range(1, n + 1)))4939gens1 = [Graphtodecstratum(strata1[j]) for j in gi1]4940strata2 = DR.all_strata(g, 3 * g - 3 + n - deg, tuple(range(1, n + 1)))4941gens2 = [Graphtodecstratum(strata2[j]) for j in gi2]49424943# compute inverse of intersection matrix4944invintmat = inverseintmat(g, n, tuple(gi1), tuple(gi2), deg)4945for a in range(len(gi1)):4946for b in range(len(gi2)):4947if invintmat[a, b] != 0:4948Delta.terms.append([invintmat[a, b] * gens1[a], gens2[b]])49494950for i in range(1, r):4951result = result.factor_pullback([0, i], Delta)4952result = result.factor_pullback([0], T.toprodtautclass())49534954# now we take care of forgetful pushforwards and renaming4955for t in result.terms:4956for i in range(r):4957t[i] = t[i].forgetful_pushforward(forgetlegs[i])4958# note: there can be no half-edge names in the range 1,..,n so simply renaming legs is possible4959t[i].rename_legs(rndics[i])49604961result.gamma = stgraph([g for i in range(r)], [l[:] for l in leglists], [])49624963return result.consolidate()496449654966@cached_function4967def inverseintmat(g, n, gi1, gi2, deg):4968if deg <= QQ(3 * g - 3 + n) / 2:4969intmat = matrix(DR.pairing_submatrix(gi1, gi2, g, deg, tuple(range(1, n + 1))))4970else:4971intmat = matrix(DR.pairing_submatrix(gi2, gi1, g, 3 * g - 3 + n - deg, tuple(range(1, n + 1)))).transpose()4972return intmat.transpose().inverse()497349744975def inverseintmat2(g, n, gi1, gi2, deg):4976tg1 = tautgens(g, n, deg)4977tg2 = tautgens(g, n, 3 * g - 3 + n - deg)4978intmat = matrix([[(tg1[i] * tg2[j]).evaluate() for i in gi1] for j in gi2])4979return intmat.transpose().inverse()498049814982""" # remove all terms that must vanish for dimension reasons4983def dimension_filter(self):4984for i in range(len(self.poly.monom)):4985(kappa,psi)=self.poly.monom[i]4986for v in range(len(self.gamma.genera)):4987# local check: we are not exceeding dimension at any of the vertices4988if 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)):4989self.poly.coeff[i]=04990break4991# global check: the total codimension of the term of the decstratum is not higher than the total dimension of the ambient space4992#if self.poly.deg(i)+len(self.gamma.edges)>3*self.gamma.g()-3+len(self.gamma.list_markings()):4993# self.poly.coeff[i]=04994#break4995return self.consolidate()49964997def consolidate(self):4998self.poly.consolidate()4999return self """50005001""" # computes integral against the fundamental class of the corresponding moduli space5002# will not complain if terms are mixed degree or if some of them do not have the right codimension5003def evaluate(self):5004answer=05005for (kappa,psi,coeff) in self.poly:5006temp=15007for v in range(len(self.gamma.genera)):5008psilist=[psi.get(l,0) for l in self.gamma.legs(v)]5009kappalist=[]5010for j in range(len(kappa[v])):5011kappalist+=[j+1 for k in range(kappa[v][j])]5012if sum(psilist+kappalist) != 3*self.gamma.genera(v)-3+len(psilist):5013temp = 05014break5015temp*=DR.socle_formula(self.gamma.genera(v),psilist,kappalist)5016answer+=coeff*temp5017return answer """5018""" def rename_legs(self,dic):5019self.gamma.rename_legs(dic)5020self.poly.rename_legs(dic)5021return self5022def __repr__(self):5023return 'Graph : ' + repr(self.gamma) +'\n'+ 'Polynomial : ' + repr(self.poly) """502450255026# old code snippet for boundary pullback of decHstrata5027"""5028if len(erg.genera)==1:5029# loop graph, add all missing markings to it5030egr.legs(0)+=forgetmarks5031divlist=[egr]5032if len(erg.genera)==2:5033possi=[[0,1] for j in forgetmarks]5034divlist=[]5035for distri in itertools.product(*possi):5036# 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)5037ergcpy=deepcopy(erg)5038ergcpy.legs(0)+=[forgetmarks[j] for j in range(len(forgetmarks)) if distri[j]==0]5039ergcpy.legs(1)+=[forgetmarks[j] for j in range(len(forgetmarks)) if distri[j]==1]5040divlist.append(ergcpy)5041"""50425043# 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 dicl5044# TODO: we could give dicv as additional argument if we already know part of the final dicv504550465047def dicv_reconstruct(Gamma, A, dicl):5048if A.num_verts() == 1:5049return {ver: 0 for ver in range(Gamma.num_verts())}5050else:5051dicv = {Gamma.vertex(dicl[h]): A.vertex(h) for h in dicl}5052remaining_vertices = set(range(Gamma.num_verts())) - set(dicv)5053remaining_edges = set(e for e in Gamma.edges(copy=False)5054if e[0] not in dicl.values())5055current_vertices = set(dicv)50565057while remaining_vertices:5058newcurrent_vertices = set()5059for e in remaining_edges.copy():5060if Gamma.vertex(e[0]) in current_vertices:5061vnew = Gamma.vertex(e[1])5062remaining_edges.remove(e)5063# if vnew is in current vertices, we don't have to do anything (already know it)5064# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)5065if vnew not in current_vertices:5066dicv[vnew] = dicv[Gamma.vertex(e[0])]5067remaining_vertices.discard(vnew)5068newcurrent_vertices.add(vnew)5069continue5070if Gamma.vertex(e[1]) in current_vertices:5071vnew = Gamma.vertex(e[0])5072remaining_edges.remove(e)5073# if vnew is in current vertices, we don't have to do anything (already know it)5074# otherwise it must be a new vertex (cannot reach old vertex past the front of current vertices)5075if vnew not in current_vertices:5076dicv[vnew] = dicv[Gamma.vertex(e[1])]5077remaining_vertices.discard(vnew)5078newcurrent_vertices.add(vnew)5079current_vertices = newcurrent_vertices5080return dicv508150825083# approximation to the question if H^{r}(\overline M_{g,n},QQ) is generated by tautological classes5084# if True is returned, the answer is yes, if False is returned, the answer is no; if None is returned, we make no claim5085# we give a reference for each of the claims, though we do not claim that this is the historically first such reference5086def cohom_is_taut(g, n, r):5087if g == 0:5088return True # [Keel - Intersection theory of moduli space of stable N-pointed curves of genus zero]5089if g == 1 and r % 2 == 0:5090return True # [Petersen - The structure of the tautological ring in genus one]5091if g == 1 and n < 10:5092# [Graber, Pandharipande - Constructions of nontautological classes on moduli spaces of curves] TODO: also for n=10 probably, typo in GP ?5093return True5094if g == 1 and n >= 11 and r == 11:5095return False # [Graber, Pandharipande]5096if g == 2 and r % 2 == 0 and n < 20:5097return True # [Petersen - Tautological rings of spaces of pointed genus two curves of compact type]5098if g == 2 and n <= 3:5099# [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]5100return True5101if g == 2 and n == 4:5102# [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 characteristic5103return True5104# WARNING: [BGP] contains error, later found in [Bini,Harer - Euler characteristics of moduli spaces of curves], does not affect the numbers above5105if g == 3 and n == 0:5106return True # [Getzler - Topological recursion relations in genus 2 (Proof of Prop. 16)] + [Yang]5107if g == 3 and n == 1:5108return True # [Getzler,Looijenga - The hodge polynomial of \bar M_3,1] + [Yang]5109if g == 3 and n == 2:5110return True # [Bergström - Cohomology of moduli spaces of curves of genus three via point counts]5111if g == 4 and n == 0:5112return True # [Bergström, Tommasi - The rational cohomology of \bar M_4] + [Yang]51135114# TODO: Getzler computes Serre characteristic in genus 1 ('96)5115# TODO: [Bini,Harer - Euler characteristics of moduli spaces of curves]: recursive computation of chi(\bar M_{g,n})5116# This suggests that g=3,n=2 should also work (Yang's numbers add up to Euler char.)51175118if r in [0, 2]:5119# r=0: fundamental class is tautological, r=1: [Arbarello, Cornalba - The Picard groups of the moduli spaces of curves]5120return True5121if r in [1, 3, 5]:5122return True # [Arbarello, Cornalba - Calculating cohomology groups of moduli spaces of curves via algebraic geometry]5123if 2 * (3 * g - 3 + n) - r in [0, 1, 2, 3, 5]:5124return True5125# dim=(3*g-3+n), then by Hard Lefschetz, there is isomorphism H^(dim-k) -> H^(dim+k) by multiplication with ample divisor5126# this divisor is tautological, so if all of H^(dim-k) is tautological, also H^(dim+k) is tautological51275128return None51295130# approximation to the question if the Faber-Zagier relations generate all tautological relations in A^d(\bar M_{g,n})5131# if True is returned, the answer is yes, if False is returned, it means that no proof has been registered yet5132# we give a reference for each of the claims, though we do not claim that this is the historically first such reference513351345135@file_cache.file_cached_function(5136directory=os.path.join(DOT_SAGE, "admcycles"),5137url="https://gitlab.com/modulispaces/relations-database/-/raw/master/data/",5138remote_database_list="https://modulispaces.gitlab.io/relations-database/FZ_conjecture_holds_files",5139env_var="ADMCYCLES_CACHE_DIR",5140key=file_cache.ignore_args_key([4]),5141filename=file_cache.ignore_args_filename())5142def FZ_conjecture_holds(g, n, d, method='pair', quiet=True):5143r"""5144Checks if the set of generalized Faber-Zagier relations [PPZ15]_ in R^d(Mbar_{g,n}) is complete.51455146It does so by comparing the rank of the quotient of the strata algebra by the FZ-relations5147to a lower bound for the rank of the cohomology group R^d(Mbar_{g,n}).51485149For method = 'pair', this lower bound is obtained as the rank of a matrix of intersection5150numbers of generators of R^d(Mbar_{g,n}) with generators in opposite degree.51515152For method = 'pull', the lower bound arises from the rank of a matrix obtained from5153intersection numbers with kappa-psi-monomials and boundary pullbacks to products of spaces5154Mbar_{gi, ni} for which the FZ-conjecture is checked recursively.51555156For quiet = False, the program gives status reports about the progress of the computation.51575158EXAMPLES::51595160sage: from admcycles.admcycles import FZ_conjecture_holds5161sage: FZ_conjecture_holds(1,4,1)5162True5163sage: FZ_conjecture_holds(2,3,1,method='pull')5164True5165"""5166if g < 0 or n < 0 or d < 0 or d > 3 * g - 3 + n:5167return True # nonsensical cases5168gi = generating_indices(g, n, d)5169quotdim = len(gi)51705171if method == 'pair':5172pgens = tautgens(g, n, d)5173gens = [pgens[i] for i in gi]5174cogens = tautgens(g, n, 3 * g - 3 + n - d)5175per = [i - 1 for i in Permutations(len(cogens)).random_element()]51765177space = span(QQ, [[0 for i in range(quotdim)]])51785179for i, p in enumerate(per):5180space += span(QQ, [[(a * cogens[p]).evaluate() for a in gens]])5181if not quiet:5182print((i + 1, space.rank()))5183if space.rank() == quotdim:5184return True5185return False51865187# Below is the code in case method == 'pull'51885189# Step 1 : pairings with kappa-psi monomials5190A, _ = kpintersection_matrix(g, n, d)5191A.echelonize()5192if A.rank() == quotdim:5193if not quiet:5194print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))5195return True51965197# Step 2 : pullbacks by separating boundary morphisms5198if all(d - di > 3 * (g - gi) - 3 + (n - ni + 1) or FZ_conjecture_holds(gi, ni, di) for gi in range(1, g) for ni in range(1, n + 2) for di in range(d + 1))\5199and FZ_conjecture_holds(g, n - 1, d):5200A = block_matrix([[A], [pullback_matrix(g, n, d, irrbdry=False)]], subdivide=False)5201A.echelonize()5202if A.rank() == quotdim:5203if not quiet:5204print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))5205return True52065207# Step 3 : pullback by non-separating boundary morphism5208if FZ_conjecture_holds(g - 1, n + 2, d):5209ibd = StableGraph([g - 1], [list(range(1, n + 3))], [(n + 1, n + 2)])5210A = block_matrix([[A], [pullback_matrix(g, n, d, bdry=ibd)]], subdivide=False)5211A.echelonize()5212if A.rank() == quotdim:5213if not quiet:5214print('FZ-conjecture holds for (g,n,d) = ' + repr((g, n, d)))5215return True5216# all has failed5217if not quiet:5218print('FZ-conjecture cannot be verified for (g,n,d) = ' + repr((g, n, d)) + '!')5219print('Rank of quotient by FZ-relations : ' + repr(quotdim))5220print('Rank of intersection and pullback : ' + repr(A.rank()))5221return False522252235224def FZresulttable(output='betti'):5225r"""5226Returns string representing ranks of the tautological ring (for output='betti') or5227cases where the Faber-Zagier conjecture has been verified (for output='FZconfirm').52285229EXAMPLES::52305231sage: from admcycles.admcycles import FZresulttable5232sage: # FZresulttable throws an error if there aren't any results stored5233sage: # To make the success of the doctest independend from the order in which5234sage: # the doctests are executed we just store something.5235sage: from admcycles import generating_indices5236sage: g = generating_indices(0,3,0)5237sage: s = FZresulttable(output='betti')5238"""5239result = 'g\tn\tr=0'5240if output == 'betti':5241gicache = generating_indices.cache5242bettidict = {(a[0], a[1], a[2]): len(b) for a, b in gicache.items()}5243if output == 'FZconfirm':5244gicache = FZ_conjecture_holds.cache5245bettidict = {(a[0], a[1], a[2]): b for a, b in gicache.items()}5246gnknowns = sorted(list(set((g, n) for g, n, _ in bettidict)))52475248rmax = max(r for _, _, r in bettidict)5249result = 'g\tn\t'5250for r in range(rmax + 1):5251result += 'r=' + repr(r) + '\t'5252result += '\n'5253for i, (g, n) in enumerate(gnknowns):5254if i > 0 and g > gnknowns[i - 1][0] or n > gnknowns[i - 1][1]:5255result += '\n'5256result += repr(g) + '\t' + repr(n) + '\t'5257for r in range(rmax + 1):5258if (g, n, r) in bettidict:5259result += repr(bettidict[(g, n, r)]) + '\t'5260else:5261result += '\t'5262return result526352645265# if g == 0:5266# return True # [Keel - Intersection theory of moduli space of stable N-pointed curves of genus zero]5267# if g == 1:5268# return True # [Petersen - The structure of the tautological ring in genus one] %TODO: verify FZ => Getzler's rel. + WDVV52695270def kappapsipolys(g, n, d):5271r"""5272Returns a list of the polynomials in kappa and psi classes on Mbar_{g,n} of degree d.52735274EXAMPLES::52755276sage: from admcycles.admcycles import kappapsipolys5277sage: for a in kappapsipolys(0,4,1):5278....: print(a)5279Graph : [0] [[1, 2, 3, 4]] []5280Polynomial : (kappa_1)_05281Graph : [0] [[1, 2, 3, 4]] []5282Polynomial : psi_15283Graph : [0] [[1, 2, 3, 4]] []5284Polynomial : psi_25285Graph : [0] [[1, 2, 3, 4]] []5286Polynomial : psi_35287Graph : [0] [[1, 2, 3, 4]] []5288Polynomial : psi_45289"""5290# TODO: should we deprecate?5291from .tautological_ring import TautologicalRing5292return TautologicalRing(g, n).kappa_psi_polynomials(d)52935294# pairing_submatrix(tuple(generating_indices(4,1,4)),tuple(range(300)),4,4,(1,))52955296############5297#5298# Lookup and saving functions for Hdatabase5299#5300############530153025303def save_FZrels():5304"""5305Saving previously computed Faber-Zagier relations [PPZ15]_ to file new_geninddb.pkl.53065307Deprecated. Saving and loading of FZ relations is now handled automatically.5308"""5309from .superseded import deprecation5310deprecation(168, 'Saving and loading of FZ relations is now handled automatically.')531153125313def load_FZrels():5314"""5315Loading values -- Deprecated5316Please run convert_old_FZ_database() ones to convert your cache to the new format.5317Saving and loading of FZ relations is handled automatically afterwards.5318"""5319from .superseded import deprecation5320deprecation(168, ('The format of the cache file has changed. '5321'Please run convert_old_FZ_database() ones to convert your cache to the '5322'new format. '5323'Saving and loading of FZ relations is handled automatically afterwards.'))532453255326def convert_old_FZ_database():5327r"""5328Converts the relations saved using the old method.5329Converts the `new_geminddb.pkl` file to a different file for each combination of a function and its arguments.5330The old file contains data for the functions ``generating_indices``, ``genstobasis`` and ``FZ_conjecture_holds``.5331"""5332old_file = "new_geninddb.pkl"5333with open(old_file, 'rb') as old_rels:5334old_dic = pickle.load(old_rels)53355336for gi in old_dic[0].keys():5337generating_indices.set_cache(old_dic[0][gi], *gi[0])5338for gb in old_dic[1].keys():5339genstobasis.set_cache(old_dic[1][gb], *gb[0])5340for fzch in old_dic[2].keys():5341FZ_conjecture_holds.set_cache(old_dic[2][fzch], *fzch[0])534253435344def set_online_lookup(b):5345r"""5346Temporarily set online lookup for all supported functions.5347Use func:`set_online_lookup_default` to save a default setting.53485349Those are currently :func:`genstobasi`, func:`generating_indices`, func:`FZ_conjecture_holds`.53505351EXAMPLES::53525353sage: from admcycles.admcycles import set_online_lookup5354sage: from admcycles.admcycles import genstobasis, generating_indices, FZ_conjecture_holds5355sage: functions = [genstobasis, generating_indices, FZ_conjecture_holds]5356sage: set_online_lookup(True)5357sage: assert all(f.go_online == True for f in functions)5358sage: set_online_lookup(False)5359sage: assert all(f.go_online == False for f in functions)5360"""5361functions = [genstobasis, generating_indices, FZ_conjecture_holds]5362for f in functions:5363f.set_online_lookup(b)536453655366def set_online_lookup_default(b):5367r"""5368Sets a default for online lookup for all supported functions.5369Use func:`set_online_lookup` for a temporary setting.53705371Those are currently :func:`genstobasi`, func:`generating_indices`, func:`FZ_conjecture_holds`.5372"""5373functions = [genstobasis, generating_indices, FZ_conjecture_holds]5374for f in functions:5375f.set_online_lookup_default(b)537653775378def download_FZ_database():5379r"""5380Download the remove database for all supported functions.5381"""5382functions = [genstobasis, generating_indices, FZ_conjecture_holds]5383for f in functions:5384f.download_all()538553865387# looks up if the Hurwitz cycle for (g,dat) has already been computed. If yes, returns a fresh tautclass representing this5388# it takes care of the necessary reordering and possible pushforwards by forgetting markings5389# if this cycle has not been computed before, it raises a KeyError5390def Hdb_lookup(g, dat, markings):5391# first get a sorted version of dat, remembering the reordering5392li = dat.l5393sort_li = sorted(enumerate(li), key=lambda x: x[1])5394# [2,0,1] means that li looking like [a,b,c] now is reordered as li_reord=[c,a,b]5395ind_reord = [i[0] for i in sort_li]5396li_reord = [i[1] for i in sort_li]5397dat_reord = HurwitzData(dat.G, li_reord)53985399# now create a dictionary legpermute sending the marking-names in the space you want to the corresponding names in the standard-ordered space5400mn_list = []5401num_marks = 05402Gord = dat.G.order()5403for count in range(len(li)):5404new_marks = QQ(Gord) / li[count][1]5405mn_list.append(list(range(num_marks + 1, num_marks + new_marks + 1)))54065407mn_reord = []5408for i in ind_reord:5409mn_reord.append(mn_list[i])54105411Hdb = Hdatabase[(g, dat_reord)]5412for marks in Hdb:5413if set(markings).issubset(set(marks)):5414forgottenmarks = set(marks) - set(markings)5415result = deepcopy(Hdb[marks])5416result = result.forgetful_pushforward(list(forgottenmarks))5417# TODO: potentially include this result in Hdb541854195420############5421#5422# Library of interesting cycle classes5423#5424############54255426def fundclass(g, n):5427r"""5428Return the fundamental class of \Mbar_g,n.5429"""5430from .tautological_ring import TautologicalRing5431return TautologicalRing(g, n).fundamental_class()54325433# returns a list of generators of R^r(\bar M_{g,n}) as tautclasses in the order of Pixton's program5434# if decst=True, returns generators as decstrata, else as TautologicalClass543554365437def tautgens(g, n, r, decst=False, moduli='st'):5438r"""5439Returns a lists of all tautological classes of degree r on \bar M_{g,n}.54405441INPUT:54425443g : integer5444Genus g of curves in \bar M_{g,n}.5445n : integer5446Number of markings n of curves in \bar M_{g,n}.5447r : integer5448The degree r of of the classes.5449decst : boolean5450If set to True returns generators as decorated strata, else as tautological classes.5451moduli : string5452The moduli type (one of 'st', 'tl', 'ct', 'rt', 'sm').54535454EXAMPLES::54555456sage: from admcycles import *54575458sage: tautgens(2,0,2)[1]5459Graph : [2] [[]] []5460Polynomial : (kappa_1^2)_054615462::54635464sage: L=tautgens(2,0,2);2*L[3]+L[4]5465Graph : [1] [[2, 3]] [(2, 3)]5466Polynomial : (kappa_1)_05467<BLANKLINE>5468Graph : [1, 1] [[2], [3]] [(2, 3)]5469Polynomial : 2*psi_25470"""5471if decst:5472L = DR.all_strata(g, r, tuple(range(1, n + 1)), moduli_type=get_moduli(moduli, DRpy=True))5473return [Graphtodecstratum(l) for l in L]5474else:5475from .tautological_ring import TautologicalRing5476return TautologicalRing(g, n, moduli=get_moduli(moduli)).generators(r)54775478# prints a list of generators of R^r(\bar M_{g,n}) as tautclasses in the order of Pixton's program547954805481def list_tautgens(g, n, r):5482r"""5483Lists all tautological classes of degree r on \bar M_{g,n}.54845485INPUT:54865487g : integer5488Genus g of curves in \bar M_{g,n}.5489n : integer5490Number of markings n of curves in \bar M_{g,n}.5491r : integer5492The degree r of of the classes.54935494EXAMPLES::54955496sage: from admcycles import *54975498sage: list_tautgens(2,0,2)5499[0] : Graph : [2] [[]] []5500Polynomial : (kappa_2)_05501[1] : Graph : [2] [[]] []5502Polynomial : (kappa_1^2)_05503[2] : Graph : [1, 1] [[2], [3]] [(2, 3)]5504Polynomial : (kappa_1)_05505[3] : Graph : [1, 1] [[2], [3]] [(2, 3)]5506Polynomial : psi_25507[4] : Graph : [1] [[2, 3]] [(2, 3)]5508Polynomial : (kappa_1)_05509[5] : Graph : [1] [[2, 3]] [(2, 3)]5510Polynomial : psi_25511[6] : Graph : [0, 1] [[3, 4, 5], [6]] [(3, 4), (5, 6)]5512Polynomial : 15513[7] : Graph : [0] [[3, 4, 5, 6]] [(3, 4), (5, 6)]5514Polynomial : 15515"""5516L = tautgens(g, n, r)5517for i in range(len(L)):5518print('[' + repr(i) + '] : ' + repr(L[i]))551955205521def kappaclass(a, g=None, n=None):5522r"""5523Returns the (Arbarello-Cornalba) kappa-class kappa_a on \bar M_{g,n} defined by55245525kappa_a= pi_*(psi_{n+1}^{a+1})55265527where pi is the morphism \bar M_{g,n+1} --> \bar M_{g,n}.55285529INPUT:55305531a : integer5532The degree a of the kappa class.5533g : integer5534Genus g of curves in \bar M_{g,n}.5535n : integer5536Number of markings n of curves in \bar M_{g,n}.55375538EXAMPLES::55395540sage: from admcycles import kappaclass5541sage: a = kappaclass(1, 2, 1)5542sage: a5543Graph : [2] [[1]] []5544Polynomial : (kappa_1)_05545sage: parent(a)5546TautologicalRing(g=2, n=1, moduli='st') over Rational Field55475548When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is5549possible to construct a5550:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value5551of g and n and call its method5552:class:`~admcycles.tautological_ring.TautologicalRing.kappa`::55535554sage: from admcycles import TautologicalRing5555sage: R = TautologicalRing(2, 1)5556sage: R.kappa(1)5557Graph : [2] [[1]] []5558Polynomial : (kappa_1)_055595560TESTS::55615562sage: from admcycles import kappaclass, reset_g_n5563sage: reset_g_n(1, 2)5564doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.5565See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5566sage: a = kappaclass(2)5567doctest:...: DeprecationWarning: Use of kappaclass without specifying g,n is deprecated.5568See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5569sage: a5570Graph : [1] [[1, 2]] []5571Polynomial : (kappa_2)_05572sage: parent(a)5573TautologicalRing(g=1, n=2, moduli='st') over Rational Field5574"""5575if g is None or n is None:5576from .superseded import deprecation5577deprecation(109, 'Use of kappaclass without specifying g,n is deprecated.')5578if g is None:5579g = globals()['g']5580if n is None:5581n = globals()['n']5582from .tautological_ring import TautologicalRing5583return TautologicalRing(g, n).kappa(a)558455855586def psiclass(i, g=None, n=None):5587r"""5588Returns the class psi_i on \bar M_{g,n}.55895590INPUT:55915592i : integer5593The leg i associated to the psi class.5594g : integer5595Genus g of curves in \bar M_{g,n}.5596n : integer5597Number of markings n of curves in \bar M_{g,n}.55985599EXAMPLES::56005601sage: from admcycles import psiclass5602sage: a = psiclass(1, 2, 1)5603sage: a5604Graph : [2] [[1]] []5605Polynomial : psi_15606sage: parent(a)5607TautologicalRing(g=2, n=1, moduli='st') over Rational Field56085609When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is5610possible to construct a5611:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value5612of g and n and call its method5613:class:`~admcycles.tautological_ring.TautologicalRing.psi`::56145615sage: from admcycles import TautologicalRing5616sage: R = TautologicalRing(2, 1)5617sage: R.psi(1)5618Graph : [2] [[1]] []5619Polynomial : psi_156205621TESTS::56225623sage: from admcycles import psiclass, reset_g_n5624sage: reset_g_n(2, 2)5625doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.5626See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5627sage: a = psiclass(1)5628doctest:...: DeprecationWarning: Use of psiclass without specifying g,n is deprecated.5629See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5630sage: a5631Graph : [2] [[1, 2]] []5632Polynomial : psi_15633sage: parent(a)5634TautologicalRing(g=2, n=2, moduli='st') over Rational Field5635"""5636if g is None or n is None:5637from .superseded import deprecation5638deprecation(109, 'Use of psiclass without specifying g,n is deprecated.')5639if g is None:5640g = globals()['g']5641if n is None:5642n = globals()['n']5643from .tautological_ring import TautologicalRing5644return TautologicalRing(g, n).psi(i)564556465647def sepbdiv(g1, A, g=None, n=None):5648r"""5649Returns 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}.56505651INPUT:56525653g1 : integer5654The genus g1 of the first vertex.5655A: list5656The list A of markings on the first vertex.5657g : integer5658The total genus g of the graph.5659n : integer5660The total number of markings n of the graph.56615662EXAMPLES::56635664sage: from admcycles import sepbdiv56655666sage: a = sepbdiv(1, [1,3], 3, 4)5667sage: a5668Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]5669Polynomial : 15670sage: a5671Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]5672Polynomial : 15673sage: parent(a)5674TautologicalRing(g=3, n=4, moduli='st') over Rational Field56755676When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is5677possible to construct a5678:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value5679of g and n and call its method5680:class:`~admcycles.tautological_ring.TautologicalRing.sepbdiv`::56815682sage: from admcycles import TautologicalRing5683sage: R = TautologicalRing(3, 4)5684sage: R.sepbdiv(1, [1,3])5685Graph : [1, 2] [[1, 3, 5], [2, 4, 6]] [(5, 6)]5686Polynomial : 156875688TESTS::56895690sage: from admcycles import sepbdiv, reset_g_n5691sage: reset_g_n(2, 2)5692doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.5693See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5694sage: a = sepbdiv(1, [1])5695doctest:...: DeprecationWarning: Use of sepbdiv without specifying g,n is deprecated.5696See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5697sage: parent(a)5698TautologicalRing(g=2, n=2, moduli='st') over Rational Field5699"""5700if g is None or n is None:5701from .superseded import deprecation5702deprecation(109, 'Use of sepbdiv without specifying g,n is deprecated.')5703if g is None:5704g = globals()['g']5705if n is None:5706n = globals()['n']5707from .tautological_ring import TautologicalRing5708return TautologicalRing(g, n).separable_boundary_divisor(g1, A)570957105711def irrbdiv(g=None, n=None):5712r"""5713Returns the pushforward of the fundamental class under the irreducible boundary gluing map \bar M_{g-1,n+2} -> \bar M_{g,n}.57145715INPUT:57165717g : integer5718The total genus g of the graph.5719n : integer5720The total number of markings n of the graph.57215722EXAMPLES::57235724sage: from admcycles import irrbdiv, reset_g_n57255726sage: a = irrbdiv(2, 2)5727sage: a5728Graph : [1] [[1, 2, 3, 4]] [(3, 4)]5729Polynomial : 15730sage: parent(a)5731TautologicalRing(g=2, n=2, moduli='st') over Rational Field57325733sage: a = irrbdiv(1, 1)5734sage: a5735Graph : [0] [[1, 2, 3]] [(2, 3)]5736Polynomial : 15737sage: parent(a)5738TautologicalRing(g=1, n=1, moduli='st') over Rational Field57395740When working with fixed g and n for the moduli space `\bar M_{g,n}`, it is5741possible to construct a5742:class:`~admcycles.tautological_ring.TautologicalRing` with the desired value5743of g and n and call its method5744:class:`~admcycles.tautological_ring.TautologicalRing.irrbdiv`::57455746sage: from admcycles import TautologicalRing5747sage: R = TautologicalRing(1, 1)5748sage: R.irreducible_boundary_divisor()5749Graph : [0] [[1, 2, 3]] [(2, 3)]5750Polynomial : 157515752TESTS::57535754sage: from admcycles import irrbdiv, reset_g_n5755sage: reset_g_n(1, 1)5756doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.5757See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5758sage: irrbdiv()5759doctest:...: DeprecationWarning: Use of irrbdiv without specifying g,n is deprecated.5760See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5761Graph : [0] [[1, 2, 3]] [(2, 3)]5762Polynomial : 15763"""5764from .superseded import deprecation5765if g is None or n is None:5766deprecation(109, 'Use of irrbdiv without specifying g,n is deprecated.')5767if g is None:5768g = globals()['g']5769if n is None:5770n = globals()['n']5771from .tautological_ring import TautologicalRing5772return TautologicalRing(g, n).irreducible_boundary_divisor()577357745775def psi_correlator(*args):5776r"""5777Return the integral of psi classes.57785779Given either a sequence k1, ..., kn of non-negative integer arguments or5780a single list containing such numbers, this function computes the integral57815782\int_{Mbar_g,n} psi_1^k1 ... psi_n^kn .57835784EXAMPLES:57855786Examples in genus 0::57875788sage: from admcycles import psi_correlator5789sage: psi_correlator(0,0,0)579015791sage: psi_correlator(1,0,0,0)579215793sage: psi_correlator(0,2,0,0,0)579415795sage: psi_correlator(1,1,0,0,0)5796257975798Examples in genus 1, with argument a list of integers::57995800sage: psi_correlator([1])58011/245802sage: psi_correlator([2, 0])58031/245804sage: psi_correlator([1, 1])58051/245806sage: psi_correlator([3, 0, 0])58071/245808sage: psi_correlator([2, 1, 0])58091/125810sage: psi_correlator([1, 1, 1])58111/1258125813genus 2::58145815sage: psi_correlator(7)58161/829445817sage: psi_correlator(7, 1)58185/829445819sage: psi_correlator(6, 2)582077/4147205821sage: psi_correlator(5, 3)5822503/14515205823sage: psi_correlator(4, 4)5824607/14515205825"""5826if len(args) == 1 and isinstance(args[0], Iterable):5827# user handed over list/tuple of numbers5828args = args[0]5829if not all(x in ZZ and x >= 0 for x in args):5830raise ValueError('arguments of psi_correlators must be nonnegative integers')5831n = len(args)5832s = sum(args)5833if (s - n) % 3:5834raise ValueError("the composition should sum up to 3*g - 3 + n")5835g = (s - n) // 3 + 15836from .tautological_ring import TautologicalRing5837R = TautologicalRing(g, n)5838poly = {i + 1: ki for i, ki in enumerate(args) if ki}5839return R(R.trivial_graph(), psi=poly).evaluate()58405841# tries to return the class lambda_d on \bar M_{g,n}. We hope that this is a variant of the DR-cycle5842# def lambdaish(d,g,n):5843# dvector=vector([0 for i in range(n)])5844# return (-2)**(-g) * DR_cycle(g,dvector,d)58455846# 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)5847# implements the formula from [Mumford - Towards an enumerative geometry ...]584858495850@cached_function5851def hodge_chern_char(g, n, d):5852from .superseded import deprecation5853deprecation(109, 'hodge_chern_char is deprecated. Please use the hodge_chern_char method from TautologicalRing instead.')5854from .tautological_ring import TautologicalRing5855TautologicalRing(g, n).hodge_chern_character(d)58565857# converts the function chclass (sending m to ch_m) to the corresponding chern polynomial, up to degree dmax585858595860def chern_char_to_poly(chclass, dmax, g, n):5861result = deepcopy(tautgens(g, n, 0)[0])58625863argum = sum([factorial(m - 1) * chclass(m) for m in range(1, dmax + 1)])5864expo = deepcopy(argum)5865result = result + argum58665867for m in range(2, dmax + 1):5868expo *= argum5869expo.degree_cap(dmax)5870result += (QQ(1) / factorial(m)) * expo58715872return result587358745875def chern_char_to_class(t, char, g=None, n=None):5876r"""5877Turns a Chern character into a Chern class in the tautological ring of \Mbar_{g,n}.58785879INPUT:58805881t : integer5882degree of the output Chern class5883char : tautclass or function5884Chern character, either represented as a (mixed-degree) tautological class or as5885a function m -> ch_m58865887EXAMPLES:58885889Note that the output of generalized_hodge_chern takes the form of a chern character::58905891sage: from admcycles import *5892sage: from admcycles.GRRcomp import *5893sage: g=2;n=2;l=0;d=[1,-1];a=[[1,[1],-1]]5894sage: chern_char_to_class(1,generalized_hodge_chern(l,d,a,1,g,n))5895Graph : [2] [[1, 2]] []5896Polynomial : 1/12*(kappa_1)_0 - 13/12*psi_1 - 1/12*psi_25897<BLANKLINE>5898Graph : [1] [[4, 5, 1, 2]] [(4, 5)]5899Polynomial : 1/245900<BLANKLINE>5901Graph : [0, 2] [[1, 2, 4], [5]] [(4, 5)]5902Polynomial : 1/125903<BLANKLINE>5904Graph : [1, 1] [[2, 4], [1, 5]] [(4, 5)]5905Polynomial : 13/125906<BLANKLINE>5907Graph : [1, 1] [[4], [1, 2, 5]] [(4, 5)]5908Polynomial : 1/125909"""5910from .tautological_ring import TautologicalRing, TautologicalClass5911if isinstance(char, TautologicalClass):5912arg = [(-1)**(s - 1) * factorial(s - 1) * char.simplified(r=s) for s in range(1, t + 1)]5913else:5914arg = [(-1)**(s - 1) * factorial(s - 1) * char(s) for s in range(1, t + 1)]5915exp = sum(multinomial(s.to_exp()) / factorial(len(s)) * prod(arg[k - 1] for k in s) for s in Partitions(t))5916if t == 0:5917if g is None or n is None:5918from .superseded import deprecation5919deprecation(109, 'Use of chern_char_to_class for t==0 without specifying g,n is deprecated.')5920return exp5921if g is None:5922g = globals()['g']5923if n is None:5924n = globals()['n']5925return exp * TautologicalRing(g, n).fundamental_class()5926return exp.simplified(r=t)59275928# the degree t part of the exponential function5929# we sum over the partitions of degree element to give a degree t element5930# The length of a partition is the power in the exponent we are looking at5931# Then we need to see the number of ways to divide the degrees over the x's to get the right thing.593259335934def lambdaclass(d, g=None, n=None, pull=True):5935r"""5936Returns the tautological class lambda_d on \bar M_{g,n} defined as the d-th Chern class59375938lambda_d = c_d(E)59395940of the Hodge bundle E. The result is represented as a sum of stable graphs with kappa and psi classes.59415942INPUT:59435944d : integer5945The degree d.5946g : integer5947Genus g of curves in \bar M_{g,n}.5948n : integer5949Number of markings n of curves in \bar M_{g,n}.5950pull : boolean, default = True5951Whether lambda_d on \bar M_{g,n} is computed as pullback from \bar M_{g}59525953EXAMPLES::59545955sage: from admcycles import *59565957sage: lambdaclass(1,2,0)5958Graph : [2] [[]] []5959Polynomial : 1/12*(kappa_1)_05960<BLANKLINE>5961Graph : [1] [[2, 3]] [(2, 3)]5962Polynomial : 1/245963<BLANKLINE>5964Graph : [1, 1] [[2], [3]] [(2, 3)]5965Polynomial : 1/2459665967When working with fixed g and n for the moduli space `\bar M_{g,n}`,5968it is possible to construct a :class:`~admcycles.tautological_ring.TautologicalRing`5969with the desired value of g and n::59705971sage: R = TautologicalRing(1, 1)5972sage: R.lambdaclass(1)5973Graph : [1] [[1]] []5974Polynomial : 1/12*(kappa_1)_0 - 1/12*psi_15975<BLANKLINE>5976Graph : [0] [[3, 4, 1]] [(3, 4)]5977Polynomial : 1/2459785979TESTS::59805981sage: from admcycles import lambdaclass, reset_g_n5982sage: inputs = [(0,0,4), (1,1,3), (1,2,1), (2,2,1), (3,2,1), (-1,2,1), (2,3,2)]5983sage: for d,g,n in inputs:5984....: assert (lambdaclass(d, g, n)-lambdaclass(d, g, n, pull=False)).is_zero()59855986sage: reset_g_n(1,1)5987doctest:...: DeprecationWarning: reset_g_n is deprecated. Please use TautologicalRing instead.5988See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5989sage: lambdaclass(1)5990doctest:...: DeprecationWarning: Use of lambdaclass without specifying g,n is deprecated.5991See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.5992Graph : [1] [[1]] []5993Polynomial : 1/12*(kappa_1)_0 - 1/12*psi_15994<BLANKLINE>5995Graph : [0] [[3, 4, 1]] [(3, 4)]5996Polynomial : 1/245997"""5998from .superseded import deprecation5999if g is None or n is None:6000deprecation(109, 'Use of lambdaclass without specifying g,n is deprecated.')6001if g is None:6002g = globals()['g']6003if n is None:6004n = globals()['n']6005from .tautological_ring import TautologicalRing6006return TautologicalRing(g, n).lambdaclass(d, pull=pull)600760086009def barH(g, dat, markings=None):6010"""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"""6011Hbar = trivGgraph(g, dat)6012n = len(Hbar.gamma.list_markings())60136014# global Hexam6015# Hexam=(g,dat,method,vecout,redundancy,markings)60166017if markings is None:6018markings = list(range(1, n + 1))60196020N = len(markings)6021msorted = sorted(markings)6022markingdictionary = {i + 1: msorted[i] for i in range(N)}6023return prodHclass(stgraph([g], [list(range(1, N + 1))], []), [decHstratum(stgraph([g], [list(range(1, N + 1))], []), {0: (g, dat)}, [[0, markingdictionary]])])602460256026def Hyperell(g, n=0, m=0):6027"""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}.60286029TESTS::60306031sage: from admcycles import Hyperell6032sage: H=Hyperell(3) # long time6033sage: H.basis_vector() # long time6034(3/4, -9/4, -1/8)6035sage: H2 = Hyperell(1,1,1)6036sage: H2.forgetful_pushforward([3]).fund_evaluate()6037160386039"""6040if n > 2 * g + 2:6041print('A hyperelliptic curve of genus ' + repr(g) + ' can only have ' + repr(2 * g + 2) + ' fixed points!')6042return 06043if 2 * g - 2 + n + 2 * m <= 0:6044print('The moduli space \\barM_{' + repr(g) + ',' + repr(n + 2 * m) + '} does not exist!')6045return 060466047G = PermutationGroup([(1, 2)])6048H = HurData(G, [G[1] for i in range(2 * g + 2)] + [G[0] for i in range(m)])6049marks = list(range(1, n + 1)) + list(range(2 * g + 2 + 1, 2 * g + 2 + 1 + 2 * m))6050factor = QQ(1) / factorial(2 * g + 2 - n)6051result = factor * Hidentify(g, H, markings=marks)60526053# currently, the markings on result are named 1,2,..,n, 2*g+3, ..., 2*g+2+2*m6054# shift the last set of markings down by 2*g+3-(n+1)6055rndict = {i: i for i in range(1, n + 1)}6056rndict.update({j: j - (2 * g + 2 - n) for j in range(2 * g + 2 + 1, 2 * g + 2 + 1 + 2 * m)})6057return result.rename_legs(rndict, inplace=False)605860596060def Biell(g, n=0, m=0):6061r"""6062Returns 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}`.60636064TESTS::60656066sage: from admcycles import Biell6067sage: B=Biell(2)6068sage: B.basis_vector()6069(15/2, -9/4)6070sage: B1 = Biell(2,0,1)6071sage: B.forgetful_pullback([1]).basis_vector()6072(15/2, -15/2, -9/2)6073sage: B1.forgetful_pushforward([2]).basis_vector()6074(15, -15, -9)60756076"""6077if g == 0:6078print('There are no bielliptic curves of genus 0!')6079return 06080if n > 2 * g - 2:6081print('A bielliptic curve of genus ' + repr(g) + ' can only have ' + repr(2 * g - 2) + ' fixed points!')6082return 06083if 2 * g - 2 + n + 2 * m <= 0:6084print('The moduli space \\barM_{' + repr(g) + ',' + repr(n + 2 * m) + '} does not exist!')6085return 060866087G = PermutationGroup([(1, 2)])6088H = HurData(G, [G[1] for i in range(2 * g - 2)] + [G[0] for i in range(m)])6089marks = list(range(1, n + 1)) + list(range(2 * g - 2 + 1, 2 * g - 2 + 1 + 2 * m))6090factor = QQ((1, factorial(2 * g - 2 - n)))6091if g == 2 and n == 0 and m == 0:6092factor /= 26093result = factor * Hidentify(g, H, markings=marks)60946095# currently, the markings on result are named 1,2,..,n, 2*g-1, ..., 2*g-2+2*m6096# shift the last set of markings down by 2*g-1-(n+1)6097rndict = {i: i for i in range(1, n + 1)}6098rndict.update({j: j - (2 * g - 2 - n) for j in range(2 * g - 2 + 1, 2 * g - 2 + 1 + 2 * m)})6099return result.rename_legs(rndict, inplace=False)610061016102############6103#6104# Transfer maps6105#6106############61076108def Hpullpush(g, dat, alpha):6109"""Pulls the class alpha to the space \\bar H_{g,dat} via map i forgetting the action, then pushes forward under delta."""6110Hb = Htautclass([Hdecstratum(trivGgraph(g, dat), poly=onekppoly(1))])6111Hb = Hb * alpha6112if not Hb.terms:6113gam = trivGgraph(g, dat).quotient_graph()6114gd = gam.g()6115nd = gam.n()6116from .tautological_ring import TautologicalRing6117return TautologicalRing(gd, nd).zero()6118return Hb.quotient_pushforward()611961206121def FZreconstruct(g, n, r, moduli='st'):6122genind = generating_indices(g, n, r, moduli=moduli)6123gentob = genstobasis(g, n, r, moduli=moduli)6124numgens = len(gentob)6125M = []61266127for i in range(numgens):6128v = insertvec(gentob[i], numgens, genind)6129v[i] -= 16130M.append(v)61316132return matrix(M)613361346135def insertvec(w, length, positions):6136v = vector(QQ, length)6137for j in range(len(positions)):6138v[positions[j]] = w[j]6139return v61406141############6142#6143# Test routines6144#6145############614661476148def pullpushtest(g, dat, r):6149r"""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.6150"""6151Gr = trivGgraph(g, dat)6152n = Gr.gamma.n()61536154M = FZreconstruct(g, n, r)61556156N = matrix([Hpullpush(g, dat, alpha).basis_vector(r) for alpha in tautgens(g, n, r)])61576158return (N.transpose() * M.transpose()).is_zero()61596160for v in M.rows():6161if not v.is_zero():6162alpha = Tautv_to_tautclass(v, g, n, r)6163pb = Hpullpush(g, dat, alpha)6164print(pb.is_zero())616561666167# compares intersection numbers of Pixton generators computed by Pixton's program and by own program6168# computes all numbers between generators for \bar M_{g,n} in degrees r, 3g-3+n-r, respectively6169def checkintnum(g, n, r):6170from .tautological_ring import TautologicalRing6171R = TautologicalRing(g, n)6172markings = tuple(range(1, n + 1))6173Mpixton = DR.pairing_matrix(g, r, markings)61746175strata1 = [Graphtodecstratum(Grr) for Grr in DR.all_strata(g, r, markings)]6176strata2 = [Graphtodecstratum(Grr) for Grr in DR.all_strata(g, 3 * g - 3 + n - r, markings)]6177Mself = [[s1.multiply(s2, R).evaluate() for s2 in strata2] for s1 in strata1]61786179return Mpixton == Mself618061816182# pulls back all generators of degree r to the boundary divisors and identifies them, checks if results are compatible with FZ-relations between generators6183def pullandidentify(g, n, r):6184markings = tuple(range(1, n + 1))6185M = DR.FZ_matrix(g, r, markings)6186Mconv = matrix([DR.convert_vector_to_monomial_basis(M.row(i), g, r, markings) for i in range(M.nrows())])61876188L = list_strata(g, n, 1)6189strata = DR.all_strata(g, r, markings)6190decst = [Graphtodecstratum(Grr) for Grr in strata]61916192for l in L:6193pullbacks = [((l.boundary_pullback(st)).dimension_filter()).totensorTautbasis(r, True) for st in decst]6194for i in range(Mconv.nrows()):6195vec = 0 * pullbacks[0]6196for j in range(Mconv.ncols()):6197if Mconv[i, j] != 0:6198vec += Mconv[i, j] * pullbacks[j]6199if not vec.is_zero():6200return False6201return True620262036204# 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<n26205# compares with result when pulling back and intersecting6206# condition: r1 + r2- (n2-n1) = 3*g-3+n16207def pushpullcompat(g, n1, n2, r1):6208# global cu6209# global cd62106211r2 = 3 * g - 3 + n2 - r162126213from .tautological_ring import TautologicalRing6214Rdown = TautologicalRing(g, n1)6215Rup = TautologicalRing(g, n2)6216strata_down = Rdown.generators(r1)6217strata_up = Rup.generators(r2)62186219fmarkings = list(range(n1 + 1, n2 + 1))62206221for class_down in strata_down:6222for class_up in strata_up:6223intersection_down = (class_down * class_up.forgetful_pushforward(fmarkings)).evaluate()6224intersection_up = (class_down.forgetful_pullback(fmarkings) * class_up).evaluate()62256226if not intersection_down == intersection_up:6227return False6228return True622962306231def deltapullpush(g, H, r):6232r"""6233Test that delta-pullback composed with delta-pushforward is multiplication with delta-degree6234for classes in R^r(\bar M_{g',b}) on space of quotient curves for Hurwitz data (g, H).62356236TESTS::62376238sage: from admcycles.admcycles import deltapullpush, HurData6239sage: G = SymmetricGroup(2)6240sage: H = HurData(G,[G[1],G[1],G[0]])6241sage: deltapullpush(2, H, 2)6242True6243"""6244trivGg = trivGgraph(g, H)6245trivGclass = Htautclass([Hdecstratum(trivGg)])6246ddeg = trivGg.delta_degree(0)6247quotgr = trivGg.quotient_graph()6248gprime = quotgr.genera(0)6249bmarkings = quotgr.list_markings()62506251from .tautological_ring import TautologicalRing6252R = TautologicalRing(gprime, bmarkings)6253gens = R.generators(r)62546255for cl in gens:6256clpp = trivGclass.quotient_pullback(cl).quotient_pushforward()6257if not (clpp.vector(r) == ddeg * cl.vector(r)):6258return False62596260# print 'The class \n'+repr(cl)+'\npulls back to\n'+repr(trivGclass.quotient_pullback(cl))+'\nunder delta*.\n'6261return True626262636264# Lambda-classes must satisfy this:6265def lambdaintnumcheck(g):6266from .tautological_ring import TautologicalRing6267R = TautologicalRing(g)6268intnum = ((R.lambdaclass(g) * R.lambdaclass(g - 1)).simplified() * R.lambdaclass(g - 2)).evaluate()6269result = QQ((-1, 2)) / factorial(2 * (g - 1)) * bernoulli(2 * (g - 1)) / (2 * (g - 1)) * bernoulli(2 * g) / (2 * g)6270return intnum == result627162726273@cached_function6274def op_subset_space(g, n, r, moduli):6275r"""6276Returns a vector space W over QQ which is the quotient W = V / U, where6277V = space of vectors representing classes in R^r(\Mbar_{g,n}) in a basis6278U = subspace of vectors representing classes supported outside the open subset6279of \Mbar_{g,n} specified by moduli (= 'sm', 'rt', 'ct' or 'tl')6280Thus for v in V, the vector W(v) is the representation of the class represented by v6281in a basis of R^r(M^{moduli}_{g,n}).6282"""6283from .tautological_ring import TautologicalRing6284L = TautologicalRing(g, n).generators(r)6285Msm = [(0 * L[0]).basis_vector(r)] # start with zero vector to get dimensions right later62866287moduli = get_moduli(moduli)62886289for t in L:6290gamma = next(iter(t._terms)) # we have a unique graph6291if gamma.vanishes(moduli):6292# t is supported on a graph outside the locus specified by moduli6293Msm.append(t.basis_vector())62946295MsmM = matrix(QQ, Msm)6296U = MsmM.row_space()6297W = U.ambient_vector_space() / U6298return W629963006301############6302#6303# Further doctests6304#6305############6306r"""6307EXAMPLES::63086309sage: from admcycles import *6310sage: from admcycles.admcycles import pullpushtest, deltapullpush, checkintnum, pullandidentify, pushpullcompat, lambdaintnumcheck6311sage: G=PermutationGroup([(1,2)])6312sage: H=HurData(G,[G[1],G[1]])6313sage: pullpushtest(2,H,1)6314True6315sage: pullpushtest(2,H,2) # long time6316True6317sage: deltapullpush(2,H,1)6318True6319sage: deltapullpush(2,H,2) # long time6320True6321sage: G=PermutationGroup([(1,2,3)])6322sage: H=HurData(G,[G[0]])6323sage: c=Hidentify(1,H)6324sage: c.basis_vector()6325(5, -3, 2, 2, 2)6326sage: checkintnum(2,0,1)6327True6328sage: checkintnum(2,1,2)6329True6330sage: pullandidentify(2,1,2)6331True6332sage: pullandidentify(3,0,2)6333True6334sage: pushpullcompat(2,0,1,2) # long time6335True6336sage: lambdaintnumcheck(2)6337True6338sage: lambdaintnumcheck(3) # long time6339True6340"""634163426343