| Download
Project: admcycles
Views: 724Visibility: Unlisted (only visible to those who know the link)
Image: ubuntu2004from __future__ import absolute_import1from __future__ import print_function23import itertools4import sys5from collections import Counter, defaultdict6from sympy.utilities.iterables import partitions78# pylint does not know sage9from sage.structure.sage_object import SageObject # pylint: disable=import-error10from sage.matrix.constructor import matrix # pylint: disable=import-error11from sage.misc.flatten import flatten # pylint: disable=import-error12from sage.misc.cachefunc import cached_method # pylint: disable=import-error13from sage.structure.formal_sum import FormalSum, FormalSums # pylint: disable=import-error14from sage.rings.rational_field import QQ # pylint: disable=import-error15from sage.arith.functions import lcm # pylint: disable=import-error16from sage.functions.other import factorial # pylint: disable=import-error17from sage.symbolic.ring import SR # pylint: disable=import-error18from sage.combinat.integer_vector_weighted import WeightedIntegerVectors # pylint: disable=import-error19from sage.functions.other import binomial # pylint: disable=import-error2021import sage.misc.persist2223from copy import deepcopy2425import admcycles.admcycles2627import admcycles.diffstrata.levelgraph28import admcycles.diffstrata.bic29import admcycles.diffstrata.sig30import admcycles.stratarecursion31import admcycles.diffstrata.stratatautring3233from .cache import ADM_EVALS, TOP_XIS3435#######################################################################36#######################################################################37###### Recursive Calculations and Degeneration Graphs38#######################################################################39## The idea is to do all calculations recursively.40## In particular, the Degeneration Graph is itself a recursive object.41##42## The key observation is that:43## * Each 3-level graph arises by either clutching a top component of44## a BIC to a BIC of its bottom component of a BIC of the top45## component to the bottom component.46## * On the other hand, each 3-level graph is the intersection of47## two (different) BICs of the Stratum.48## * Therefore, for each BIC B of the Stratum, every BIC Bt in the top49## component corresponds to a unique BIC B' of the stratum, so50## that the 3-level graph (Bt clutched to the bottom component of B)51## is B*B' (i.e. delta_1 of this graph is B', delta_2 is B).52## The same is true for the BICs of the bottom component.53## * We thus obtain two maps, for each BIC B of the stratum:54## * top_to_bic mapping BICs of the top component to BICs of55## the stratum56## * bot_to_bic mapping BICs of the bottom component to BICs of57## the stratum58## * These maps have disjoint images.59## * These maps fail to be embeddings precisely when the intersection60## of two BICs is not irreducible (i.e. clutching different BICs61## to a component results in the intersection with the same divisor)62## or when there are automorphisms involved (i.e. several ways of63## undegenerating to the same BIC).64## We can thereby express the clutching of a product of BICs in the top65## and bottom components of a BIC in our stratum as a product of BICs of66## our stratum. Hence the procedure is recursive.67##68## Therefore, the GenDegenerationGraph needs to remember only the BICs69## together with, for each BIC, top and bottom components and the two maps.70##71## More precisely, the Degeneration Graph of a GeneralisedStratum72## consists of the following information:73## * The BICs inside the Stratum.74## * For each BIC, its top and bottom component (GeneralisedStratum75## together with a dictionary Stratum points -> LevelGraph points)76## * For each BIC, the BICs of its top and bottom component, together77## with the maps top_to_bic and bot_to_bic.78##79## We can now calculate the GenDegenerationGraph:80## * Step 1: Calculate all BICs in a GeneralisedStratum.81## * Step 2: Separate these into top an bottom component.82## * Step 3: Calculate all BICs in every top and bottom component.83## * Step 4: Calculate top_to_bic and bot_to_bic for each BIC in the84## Stratum (as dictionaries: index of BIC in top/bottom ->85## index of BIC in stratum)86##87## In particular, we this implies the following recursive algorithm for88## the EmbeddedLevelGraph of an arbitrary product of BICs in the stratum:89## INPUT: Product of BICs.90## OUTPUT: EmbeddedLevelGraph.91## * Step 1: Choose a BIC B from the product (e.g. the first).92## * Step 2: Find the preimages of the other BICs in the product under93## top_to_bic and bot_to_bic of B.94## * This gives (possibly multiple) products of BICs in the top and bottom95## stratum of B.96## * Step 3: Apply to product in top an bottom to get two EmbeddedLevelGraphs97## * Step 4: Return the clutching of the top and bottom graph.98##99## Moreover, we can generate the "lookup list", consisting of the non-empty100## products of BICs in each stratum.101## For this, we record all intersections that give 3-level graphs in each102## GenDegenerationGraph (i.e. (2,1) means that there exists a 3-level graph103## C such that delta(1) of C is bics[2] and delta(2) of C is bics[1]).104## Note that this is equivalent to 2 being in the image of top_to_bic(1).105##106## The key observation here is that any profile (i_1,...,i_n) can be107## written as a "domino" of 3-level graphs, i.e. (i_1,i_2)(i_2,_3)...(i_n-1,i_n).108##109## However, for the recursive generation of the lookup list, it is enough110## to take a profile and add the top generations of the first bic and the111## bottom degenerations of the last bic to obtain a profile with length112## increased by one (see the implementation below for more details.)113##114#######################################################################115#######################################################################116117class GeneralisedStratum(SageObject):118"""119A union of (meromorphic) strata with residue conditions.120121A GeneralisedStratum is uniquely identified by the following information:122123* sig_list : list of signatures [sig_1,...,sig_n], where sig_i is the Signature124of the component i,125126* res_cond : list of residue conditions, i.e. [R_1,...,R_n] where each R_l is127a list of tuples (i,j), corresponding to the j-th component of sig_i, that128share a residue condition (i.e. the residues at these poles add up to 0).129Note that the residue theorem for each component will be added automatically.130"""131def __init__(self,sig_list,res_cond=None):132self._h0 = len(sig_list)133self._sig_list = sig_list134self._n = sum([sig.n for sig in sig_list]) # total number of points135self._g = [sig.g for sig in sig_list]136# remember poles as (i,j) where i is the component and j is the index in sig_i137self._polelist = [(i,j) for i,sig in enumerate(sig_list) for j in sig.pole_ind]138self._p = len(self._polelist)139if res_cond is None:140res_cond = []141self._res_cond = res_cond142self.init_more()143144def init_more(self):145self._bics = None146self._smooth_LG = None147self._all_graphs = None148self._lookup_list = None149self._lookup = {}150if not self.is_empty():151self.DG = admcycles.diffstrata.stratatautring.GenDegenerationGraph(self)152# cache AdditiveGenerators:153self._AGs = {}154# tautological class of self:155one = self.additive_generator((tuple(),0))156self.ONE = one.as_taut()157# tautological class of zero:158self.ZERO = ELGTautClass(self,[])159160def __repr__(self):161return "GeneralisedStratum(sig_list=%r,res_cond=%r)" % (self._sig_list,self._res_cond)162def __str__(self):163rep = ''164if self._h0 > 1:165rep += 'Product of Strata:\n'166else:167rep += 'Stratum: '168for sig in self._sig_list:169rep += repr(sig.sig) + '\n'170rep += 'with residue conditions: '171if not self._res_cond:172rep += repr([]) + '\n'173for res in self._res_cond:174rep += repr(res) + '\n'175return rep176177def info(self):178"""179Print facts about self.180181This calculates everything, so could take long(!)182183EXAMPLES ::184185sage: from admcycles.diffstrata import *186sage: X=GeneralisedStratum([Signature((1,1))])187sage: X.info()188Stratum: (1, 1)189with residue conditions: []190Genus: [2]191Dimension: 4192Boundary Graphs (without horizontal edges):193Codimension 0: 1 graph194Codimension 1: 4 graphs195Codimension 2: 4 graphs196Codimension 3: 1 graph197Total graphs: 10198199sage: X=GeneralisedStratum([Signature((4,))])200sage: X.info()201Stratum: (4,)202with residue conditions: []203Genus: [3]204Dimension: 5205Boundary Graphs (without horizontal edges):206Codimension 0: 1 graph207Codimension 1: 8 graphs208Codimension 2: 19 graphs209Codimension 3: 16 graphs210Codimension 4: 4 graphs211Total graphs: 48212"""213print(self)214print("Genus: %s" % self._g)215print("Dimension: %s" % self.dim())216print("Boundary Graphs (without horizontal edges):")217tot = 0218for c, graphs in enumerate(self.all_graphs):219n = len(graphs)220print("Codimension %s: %s %s" % (c,n,_graph_word(n)))221tot += n222print("Total graphs: %s" % tot)223224def additive_generator(self,enh_profile,leg_dict=None):225"""226The AdditiveGenerator for the psi-polynomial given by leg_dict on enh_profile.227228For example, if psi_2 is the psi-class at leg 2 of enh_profile,229the polynomial psi_2^3 would be encoded by the leg_dict {2 : 3}.230231This method should always be used instead of generating AdditiveGenerators232directly, as the objects are cached here, i.e. the _same_ object is returned233on every call.234235Args:236enh_profile (tuple): enhanced profile237leg_dict (dict, optional): Dictionary mapping legs of the underlying238graph of enh_profile to positive integers, corresponding to239the power of the psi class associated to this leg. Defaults to None.240241Returns:242AdditiveGenerator: the (cached) AdditiveGenerator243244EXAMPLES ::245246sage: from admcycles.diffstrata import *247sage: X=Stratum((2,))248sage: a = X.additive_generator(((0,),0))249sage: a is X.additive_generator(((0,),0))250True251sage: a is AdditiveGenerator(X,((0,),0))252False253"""254ag_hash = hash_AG(leg_dict, enh_profile)255return self.additive_generator_from_hash(ag_hash)256257def additive_generator_from_hash(self,ag_hash):258if not ag_hash in self._AGs:259self._AGs[ag_hash] = AdditiveGenerator.from_hash(self,ag_hash)260return self._AGs[ag_hash]261262def simple_poles(self):263simple_poles = [p for p in self._polelist if self.stratum_point_order(p) == -1]264return simple_poles265266@cached_method267def is_empty(self):268"""269Checks if self fails to exist for residue reasons (simple pole with residue forced zero).270271Returns:272bool: existence of simple pole with residue zero.273274EXAMPLES ::275276sage: from admcycles.diffstrata import *277sage: X=Stratum((1,-1))278sage: X.is_empty()279True280"""281for p in self.simple_poles():282if self.smooth_LG.residue_zero(p):283return True284return False285286def is_disconnected(self):287return self._h0 > 1288289def stratum_point_order(self,p):290"""291The pole order at the stratum point p.292293Args:294p (tuple): Point (i,j) of self.295296Returns:297int: Pole order of p.298"""299i, j = p300return self._sig_list[i].sig[j]301302@property303def bics(self):304"""305Initialise BIC list on first call.306307Note that _bics is a list of tuples of EmbeddedLevelGraphs308(each tuple consists of one EmbeddedLevelGraph for every309connected component).310"""311if self.is_empty():312return []313if self._bics is None:314return self.gen_bic()315return self._bics316317@property318def res_cond(self):319return self._res_cond320321@property322def lookup_list(self):323"""324The list of all (ordered) profiles.325326Note that starting with SAGE 9.0 profile numbering is no longer deterministic.327328Returns:329list: Nested list of tuples.330331EXAMPLES ::332333sage: from admcycles.diffstrata import *334sage: X=Stratum((2,))335sage: assert len(X.lookup_list) == 3336sage: X.lookup_list[0]337[()]338sage: X.lookup_list[1]339[(0,), (1,)]340sage: assert len(X.lookup_list[2]) == 1341"""342if self.is_empty():343return []344if self._lookup_list is None:345# First, we build the "lookup-list", i.e. the list of all profiles:346# the non-empty profiles can be found recursively:347# given a profile, we create new profiles by adding top and bottom348# degenerations of the corresponding bic to the begining and end.349self._lookup_list = [[tuple()]] # only one with 0 levels350n = len(self.bics)351self._lookup_list += [[(i,) for i in range(n)]] # bics352new_profiles = n353while new_profiles:354# we temporarily work with a set to avoid duplicates355self._lookup_list.append(set())356for profile in self._lookup_list[-2]:357first = profile[0]358for i in self.DG.top_to_bic(first).values():359self._lookup_list[-1].add((i,)+profile)360if len(profile) > 1:361last = profile[-1]362for i in self.DG.bot_to_bic(last).values():363self._lookup_list[-1].add(profile+(i,))364self._lookup_list[-1] = list(self._lookup_list[-1])365new_profiles = len(self._lookup_list[-1])366self._lookup_list.pop()367return self._lookup_list368369@property370def all_graphs(self):371"""372Nested list of all EmbeddedLevelGraphs in self.373374This list is built on first call.375376EXAMPLES ::377378sage: from admcycles.diffstrata import *379sage: X=GeneralisedStratum([Signature((1,1))])380sage: assert comp_list(X.all_graphs[0], [EmbeddedLevelGraph(X, LG=LevelGraph([2],[[1, 2]],[],{1: 1, 2: 1},[0],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0})])381sage: assert comp_list(X.all_graphs[1], \382[EmbeddedLevelGraph(X, LG=LevelGraph([1, 0],[[1, 2], [3, 4, 5, 6]],[(1, 5), (2, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, -1],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -1: -1}),\383EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2], [3, 4, 5, 6]],[(2, 5), (1, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, 0, -1],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -1: -1}),\384EmbeddedLevelGraph(X, LG=LevelGraph([1, 1],[[1], [2, 3, 4]],[(1, 4)],{1: 0, 2: 1, 3: 1, 4: -2},[0, -1],True),dmp={2: (0, 0), 3: (0, 1)},dlevels={0: 0, -1: -1}),\385EmbeddedLevelGraph(X, LG=LevelGraph([2, 0],[[1], [2, 3, 4]],[(1, 4)],{1: 2, 2: 1, 3: 1, 4: -4},[0, -1],True),dmp={2: (0, 0), 3: (0, 1)},dlevels={0: 0, -1: -1})])386sage: assert comp_list(X.all_graphs[2],\387[EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0],[[1], [2, 3, 4], [5, 6, 7, 8]],[(1, 4), (3, 8), (2, 7)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 1, 6: 1, 7: -2, 8: -2},[0, -1, -2],True),dmp={5: (0, 0), 6: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\388EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0],[[1, 2], [3, 4, 5], [6, 7, 8]],[(1, 4), (2, 5), (3, 8)],{1: 0, 2: 0, 3: 2, 4: -2, 5: -2, 6: 1, 7: 1, 8: -4},[0, -1, -2],True),dmp={6: (0, 0), 7: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\389EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2, 3], [4, 5, 6]],[(1, 3), (2, 6)],{1: 0, 2: 2, 3: -2, 4: 1, 5: 1, 6: -4},[0, -1, -2],True),dmp={4: (0, 0), 5: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\390EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2], [3, 4, 5, 6]],[(2, 5), (1, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, -1, -2],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -2: -2, -1: -1})])391sage: assert comp_list(X.all_graphs[2], [EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0, 0],[[1], [2, 3, 4], [5, 6, 7], [8, 9, 10]],[(1, 4), (3, 7), (2, 6), (5, 10)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2, 8: 1, 9: 1, 10: -4},[0, -1, -2, -3],True),dmp={8: (0, 0), 9: (0, 1)},dlevels={0: 0, -2: -2, -1: -1, -3: -3})])392"""393if self.is_empty():394return []395if self._all_graphs is None:396# We build the graph list from the lookup list:397# Note that lookup returns a list of graphs.398self._all_graphs = []399for l in self.lookup_list:400self._all_graphs.append(401list(itertools.chain.from_iterable(self.lookup(g)402for g in l))403)404# Ensure that degenerations of top and bottom match up:405assert all(k in self.DG.bot_to_bic(j).values()406for k in range(self.DG.n)407for j in self.DG.top_to_bic(k).values())408return self._all_graphs409410@property411def smooth_LG(self):412"""413The smooth EmbeddedLevelGraph inside a LevelStratum.414415Note that the graph might be disconnected!416417EXAMPLES ::418419sage: from admcycles.diffstrata import *420sage: X=GeneralisedStratum([Signature((1,1))])421sage: assert X.smooth_LG.is_isomorphic(EmbeddedLevelGraph(X,LG=LevelGraph([2],[[1, 2]],[],{1: 1, 2: 1},[0],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0}))422423Note that we get a single disconnected graph if the stratum is424disconnected.425426sage: X=GeneralisedStratum([Signature((0,)), Signature((0,))])427sage: X.smooth_LG428EmbeddedLevelGraph(LG=LevelGraph([1, 1],[[1], [2]],[],{1: 0, 2: 0},[0, 0],True),dmp={1: (0, 0), 2: (1, 0)},dlevels={0: 0})429430Returns:431EmbeddedLevelGraph: The output of unite_embedded_graphs applied to432the (embedded) smooth_LGs of each component of self.433"""434if not self._smooth_LG:435graph_list = []436for i,sig in enumerate(self._sig_list):437g = admcycles.diffstrata.levelgraph.smooth_LG(sig)438dmp = {j:(i,j-1) for j in range(1,sig.n+1)}439graph_list.append((self,g,dmp,{0:0}))440self._smooth_LG = unite_embedded_graphs(tuple(graph_list))441return self._smooth_LG442443@cached_method444def residue_matrix(self):445"""446Calculate the matrix associated to the residue space,447i.e. a matrix with a line for every residue condition and a column for every pole of self.448449The residue conditions consist ONLY of the ones coming from the GRC (in _res_cond)450For inclusion of the residue theorem on each component, use smooth_LG.full_residue_matrix!451"""452return self.matrix_from_res_conditions(self._res_cond)453454def matrix_from_res_conditions(self,res_conds):455"""456Calculate the matrix for a list of residue conditions, i.e.457a matrix with one line for every residue condition and a column for each pole of self.458459Args:460res_conds (list): list of residue conditions, i.e. a nested list R461R = [R_1,R_2,...] where each R_i is a list of poles (stratum points)462whose residues add up to zero.463464Returns:465SAGE matrix: residue matrix (with QQ coefficients)466467EXAMPLES ::468469sage: from admcycles.diffstrata import *470sage: X=GeneralisedStratum([Signature((2,-2,-2)),Signature((2,-2,-2))])471sage: X.matrix_from_res_conditions([[(0,1),(0,2),(1,2)],[(0,1),(1,1)],[(1,1),(1,2)]])472[1 1 0 1]473[1 0 1 0]474[0 0 1 1]475"""476res_vec = []477for res_c in res_conds:478# note: int(True)=1, int(False)=0479res_vec += [[int(p in res_c) for p in self._polelist]]480return matrix(QQ,res_vec)481482@cached_method483def residue_matrix_rank(self):484return self.residue_matrix().rank()485486@cached_method487def dim(self):488"""489Return the dimension of the stratum, that is the sum of 2g_i + n_i - 1 - residue conditions -1 for projective.490491The residue conditions are given by the rank of the (full!) residue matrix.492493Empty strata return -1.494495EXAMPLES ::496497sage: from admcycles.diffstrata import *498sage: X=GeneralisedStratum([Signature((4,))])499sage: all(B.top.dim() + B.bot.dim() == X.dim()-1 for B in X.bics)500True501"""502if self.is_empty():503return -1504# add residue conditions from RT for every connected component:505M = self.smooth_LG.full_residue_matrix506return (sum([2*sig.g + sig.n - 1 for sig in self._sig_list])507- M.rank() - 1)508509def gen_bic(self):510"""511Generates all BICs (using bic) as EmbeddedLevelGraphs.512513Returns:514list: self._bics i.e. a list of (possibly disconnected)515EmbeddedLevelGraphs.516(More precisely, each one is a tuple consisting of one517EmbeddedLevelGraph for every connected component that has518been fed to unite_embedded_graphs).519"""520self._bics = []521if self.is_empty():522return523## The BICs are the products of BICs for each connected component524## (satisfying the residue condition).525## Moreover, if there are several connected components, we also need526## to include the smooth stratum on each level.527emb_bic_list = []528529## First, we establish the dictionaries for the EmbeddedLevelGraphs:530## * the marked points of the stratum are numbered (i,j) where (i,j)531## is the j-th point on the i-th connected component.532## Note that j is the index in sig, i.e. starts at 0.533## * on each BIC, the i-th point of the signature is the point i+1534## mp_dict maps the points on the BIC to the points of the stratum535for i, sig in enumerate(self._sig_list):536mp_dict = {j : (i,j-1) for j in range(1,sig.n+1)}537# We can't build the EmbeddedLevelGraph until we have the data for all538# components (otherwise we mess up the legality check, etc.)539# So for now, we just store the generating info for each connected component seperately.540emb_bic_list_cur = []541for g in admcycles.diffstrata.bic.bic_alt_noiso(sig.sig): # TODO: Adjust for Signature!!542level_dict = {g.internal_level_number(0): 0,543g.internal_level_number(-1): -1}544EG = (self,g,mp_dict,level_dict)545emb_bic_list_cur.append(EG)546if self._h0 > 1:547# we need the smooth component on each level548for l in [0,-1]:549emb_bic_list_cur.append((self, admcycles.diffstrata.levelgraph.smooth_LG(sig), mp_dict,550{0 : l}, # one for each level551)552)553emb_bic_list.append(emb_bic_list_cur)554# The elements of _bics are now products of the (embedded) bics of the components555# Careful: The only ones that are not allowed are those, where all556# components are on one level!!557prod_bics = itertools.product(*emb_bic_list)558for prod_graph in prod_bics:559# levels are stored in values of the dict in component 3 of each tuple:560if (any(0 in g[3].values() for g in prod_graph) and561any(-1 in g[3].values() for g in prod_graph)562):563# NOTE: This actually builds the EmbeddedLevelGraphs!564pg = unite_embedded_graphs(prod_graph)565if pg.is_legal(): # check r-GRC566self._bics.append(pg)567# isomorphism classes: (possibly figure out how to check earlier?)568self._bics = admcycles.diffstrata.bic.isom_rep(self._bics)569return self._bics570571#### Ideally, we could always work with enhanced profiles, never with graphs.572#### Then edge maps could work like this:573## Def: A leg is a tuple consisting of:574## * an enhanced profile (of a levelgraph)575## * the levelstratum inside the profile (e.g. for the profile (1,2,3) this would576## be either 1^top, 3^bot, (12) or (23)). These were computed for the stratum anyway.577## * an orbit of a marked point of this gen levelstratum, which corresponds to an edge578## of the corresponding graph579## i.e. an ordered tuple of marked points equivalent by automorphisms of the corresponding580## BIC or 3-level graph (which should be also an automorphism of the full graph??!!)581##582## Then:583## INPUT: (leg, enhanced profile)584## The enhanced profile should be that of a degeneration of the graph of leg (!)585##586## OUTPUT: leg (on the second profile)587##588## Case 1:589## The levelstratum of the leg is unchanged by the degeneration.590## i.e.: (1,2) and (1,2,3) for an edge on (1,2).591## In this case the output is trivially the same edge embedded into (1,2,3)592## (because (1,2) is still a level of (1,2,3)).593##594## Case 2:595## The levelstratum is degenerated,596## i.e.: (1,2) and (1,3,2) for an leg e on (1,2).597## In this case we know that e (by checking the sign of the order) is either598## a leg on 1^bot or 2^top and the degeneration is given by top_to_bic_inv (or599## bot_to_bic_inv) of 3, where we can then track the marked point associated to e.600####601602# TODO: This should work "smarter", see above.603@cached_method604def explicit_leg_maps(self,enh_profile,enh_deg_profile,only_one=False):605"""606Provide explicit leg maps (as list of dictionaries: legs of LG to legs of LG), from607the graph associated to enh_profile to the one associated to enh_deg_profile.608609If enh_deg_profile is not a degeneration (on the level of profiles), None is610returned.611612Args:613enh_profile (enhanced profile): tuple (profile, index).614enh_deg_profile (enhanced profile): tuple (profile, index).615only_one (bool, optional): Give only one (the 'first') map (or None if none exist).616Defaults to False.617618Raises:619RuntimeError: If enh_profile is empty.620UserWarning: If there are no degenerations in the appropriate profile.621622Returns:623list of dicts: List of the leg isomorphisms, None if not a degeneration,624only one dict if only_one=True.625626EXAMPLES ::627628"""629profile = enh_profile[0]630deg_profile = enh_deg_profile[0]631# check if deg_profile is actually a (profile) degeneration:632if not set(profile).issubset(set(deg_profile)):633return None634g = self.lookup_graph(*enh_profile)635degen = self.lookup_graph(*enh_deg_profile)636if not degen:637raise RuntimeError ("%r is not a graph in %r!" % (enh_deg_profile,self))638# To obtain g, we have to squish degen at the places in the profile639# of degen that are not in the profile of g.640# We work from right to left to avoid confusion with the level numbering.641degen_squish = degen642for level, bic_index in list(enumerate(deg_profile))[::-1]:643if bic_index in profile:644continue645degen_squish = degen_squish.squish_vertical(level)646isoms = (l_iso for v_iso, l_iso in g.isomorphisms(degen_squish))647try:648first_isom = next(isoms)649except StopIteration:650# No isomorphisms found651raise UserWarning("Squish of %r not isomorphic to %r!" % (enh_deg_profile, enh_profile))652if only_one:653return first_isom654else:655return [first_isom] + list(isoms)656657#####658### Common degenerations:659## This should eat two graphs, given by their "enhanced profile" i.e. things we can660## feed to graph_lookup (a list of BICs and an index) and also return a list of661## enhanced profiles.662663### Naive approach:664## do a (set-wise) degeneration of the profile and just go through the list665## checking which ones are actually degenerations:666## INPUT: Profile + index667## OUTPUT: List of profiles + indices668669## TODO: There should be a smart way! For that one has to understand670## how to correctly encode the irreducible components of the profiles.671672@cached_method673def common_degenerations(self,s_enh_profile,o_enh_profile):674"""675Find common degenerations of two graphs.676677Args:678s_enh_profile (tuple): Enhanced profile, i.e. a tuple (p,k) consisting of679* a sorted (!) profile p680* an index in self.lookup(p)681thus giving the information of an EmbeddedLevelGraph in self.682o_enh_profile (tuple): Enhanced profile.683684Returns:685list: list of enhanced profiles, i.e. entries of type [tuple profile, index]686(that undegenerate to the two given graphs).687688EXAMPLES ::689690sage: from admcycles.diffstrata import *691sage: X=GeneralisedStratum([Signature((4,))])692693To retrieve the actual EmbeddedLevelGraphs, we must use lookup_graph.694(Because of BIC renumbering between different SAGE versions we can't provide any concrete examples :/)695696Note that the number of components can also go down.697698Providing common graphs works:699700sage: X.common_degenerations(((2,),0),((2,),0))701[((2,), 0)]702703Empty intersection gives empty list.704705"""706s_profile = s_enh_profile[0]707o_profile = o_enh_profile[0]708try:709# merge_profiles returns None if there aren't any...710deg_profile = tuple(self.merge_profiles(s_profile,o_profile))711except TypeError:712return []713return_list = []714# careful with reducible profiles:715for i in range(len(self.lookup(deg_profile))):716if self.is_degeneration((deg_profile,i),s_enh_profile) and self.is_degeneration((deg_profile,i),o_enh_profile):717return_list.append((deg_profile,i))718return return_list719720# Excess intersection of two additive generators in an ambient graph721def intersection(self,s_taut_class,o_taut_class,amb_enh_profile=None):722"""723Excess intersection of two tautological classes in Chow of ambient_enh_profile.724725Args:726s_taut_class (ELGTautClass): tautological class727o_taut_class (ELGTautClass): tautological class728amb_enh_profile (tuple, optional): enhanced profile of ambient graph.729Defaults to None.730731Raises:732RuntimeError: raised if any summand of any tautological class is not on733a degeneration of ambient_enh_profile.734735Returns:736ELGTautClass: Tautological class on common degenerations737"""738# check input:739if amb_enh_profile is None:740amb_enh_profile = ((),0)741if s_taut_class == 0 or s_taut_class == self.ZERO:742return self.ZERO743if s_taut_class == 1 or s_taut_class == self.ONE:744return o_taut_class745if o_taut_class == 0 or o_taut_class == self.ZERO:746return self.ZERO747if o_taut_class == 1 or o_taut_class == self.ONE:748return s_taut_class749return_list = []750# unpack tautological classes:751for s_coeff, s_add_gen in s_taut_class.psi_list:752for o_coeff, o_add_gen in o_taut_class.psi_list:753prod = self.intersection_AG(s_add_gen, o_add_gen, amb_enh_profile)754if prod == 0 or prod == self.ZERO:755continue756return_list.append(s_coeff*o_coeff * prod)757return_value = self.ELGsum(return_list)758if return_value == 0:759return self.ZERO760if s_taut_class.is_equidimensional() and o_taut_class.is_equidimensional():761assert return_value.is_equidimensional(),\762"Product of equidimensional classes is not equidimensional! %s * %s = %s"\763% (s_taut_class, o_taut_class, return_value)764return return_value765766@cached_method767def intersection_AG(self, s_add_gen, o_add_gen, amb_enh_profile=None):768"""769Excess intersection formula for two AdditiveGenerators in Chow of amb_enh_profile.770771Note that as AdditiveGenerators and enhanced profiles are hashable,772this method can (and will) be cached (in contrast with intersection).773774Args:775s_add_gen (AdditiveGenerator): first AG776o_add_gen (AdditiveGenerator): second AG777amb_enh_profile (tuple, optional): enhanced profile of ambient graph.778Defaults to None.779780Raises:781RuntimeError: raised if any of the AdditiveGenerators is not on782a degeneration of ambient_enh_profile.783784Returns:785ELGTautClass: Tautological class on common degenerations786"""787if amb_enh_profile is None:788amb_enh_profile = ((),0)789s_enh_profile = s_add_gen.enh_profile790o_enh_profile = o_add_gen.enh_profile791if not self.is_degeneration(s_enh_profile,amb_enh_profile):792raise RuntimeError("%r is not a degeneration of %r" % (s_enh_profile,amb_enh_profile))793if not self.is_degeneration(o_enh_profile,amb_enh_profile):794raise RuntimeError("%r is not a degeneration of %r" % (o_enh_profile,amb_enh_profile))795# Degree check:796# * the degree of the product is the sum of the degreees797# * the product is supported on a set of codim >= max(codim(s),codim(o))798# => if the sum of the degrees is > (dim(self) - max(codim(s),codim(o)))799# the product will be 0 in any case800# NOTE: degree = codim + psi-degree801deg_sum = s_add_gen.psi_degree + o_add_gen.psi_degree802if deg_sum > self.dim() - max(len(s_enh_profile[0]),len(o_enh_profile[0])):803return self.ZERO804degenerations = self.common_degenerations(s_enh_profile,o_enh_profile)805if not degenerations:806return self.ZERO807NB = self.cnb(s_enh_profile,o_enh_profile,amb_enh_profile)808if NB == 1:809# Intersection is transversal, in this case we are done:810# the pullback of an additive generator is a taut class811# where all classes live on the same graph:812prod = [(_cs * _co, s_pb * o_pb)813for L in degenerations814for _cs, s_pb in s_add_gen.pull_back(L).psi_list815for _co, o_pb in o_add_gen.pull_back(L).psi_list]816return ELGTautClass(self,[(c, AG) for c, AG in prod])817elif NB == 0 or NB == self.ZERO:818# product with 0 is 0 ...819return NB820else:821# intersect the pullback to L with the normal bundle pulled back to L (in L):822summands = [self.intersection(823self.intersection(824s_add_gen.pull_back(L),825o_add_gen.pull_back(L),826L),827self.gen_pullback_taut(828NB,829L,830self.minimal_common_undegeneration(s_enh_profile,o_enh_profile)831),832L)833for L in degenerations]834return self.ELGsum([tcls for tcls in summands])835836def normal_bundle(self, enh_profile, ambient=None):837"""838Normal bundle of enh_profile in ambient.839840Note that this is equivalent to cnb(enh_profile, enh_profile, ambient).841842Args:843enh_profile (tuple): enhanced profile844ambient (tuple, optional): enhanced profile. Defaults to None.845846Raises:847ValueError: Raised if enh_profile is not a codim 1 degeneration of ambient848849Returns:850ELGTautClass: Normal bundle N_{enh_profile, ambient}851"""852if ambient is None:853ambient = ((),0)854else:855ambient = (tuple(ambient[0]),ambient[1])856if len(enh_profile[0]) != len(ambient[0]) + 1 or not self.is_degeneration(enh_profile,ambient):857raise ValueError("%r is not a codim 1 degeneration of %r" % (enh_profile,ambient))858return self.cnb(enh_profile, enh_profile, ambient)859860# This is an element of CH^s(ambient) where s is the cardinality of the intersection of profiles861# or equivalently in CH^(c+s)(B) where c is the codimension of ambient.862@cached_method863def cnb(self,s_enh_profile,o_enh_profile,amb_enh_profile=None):864"""865Common Normal bundle of two graphs in an ambient graph.866867Note that for a trivial normal bundle (transversal intersection)868we return 1 (int) and NOT self.ONE !!869870The reason is that the ``correct'' ONE would be the ambient graph and that871is a pain to keep track of in intersection....872873Args:874s_enh_profile (tuple): enhanced profile875o_enh_profile (tuple): enhanced profile876amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.877878Raises:879RuntimeError: Raised if s_enh_profile or o_enh_profile do not degenerate880from amb_enh_profile.881882Returns:883ELGTautClass: Product of normal bundles appearing.8841 if the intersection is transversal.885"""886# check/normalise input:887if amb_enh_profile is None:888amb_enh_profile = ((),0)889else:890amb_enh_profile = (tuple(amb_enh_profile[0]),amb_enh_profile[1])891if not self.is_degeneration(s_enh_profile,amb_enh_profile):892raise RuntimeError("%r is not a degeneration of %r" % (s_enh_profile,amb_enh_profile))893if not self.is_degeneration(o_enh_profile,amb_enh_profile):894raise RuntimeError("%r is not a degeneration of %r" % (o_enh_profile,amb_enh_profile))895min_com = self.minimal_common_undegeneration(s_enh_profile,o_enh_profile)896if min_com == amb_enh_profile:897return 1 # terminating condition, transversal898else:899assert self.codim_one_common_undegenerations(s_enh_profile,o_enh_profile,amb_enh_profile),\900"minimal common undegeneration is %r, ambient profile is %r, but there aren't codim one common undegenerations!"\901% (min_com,amb_enh_profile)902return_list = []903for ep in self.codim_one_common_undegenerations(s_enh_profile,o_enh_profile,amb_enh_profile):904G = self.lookup_graph(*ep)905p, i = ep906AGG = self.additive_generator(ep,None)907# This is the "difference" between ep and amb_enh_profile:908# i.e. the inserted level, i in paper notation909squished_level = get_squished_level(ep,amb_enh_profile)910ll = self.bics[p[squished_level]].ell911xi_top = self.xi_at_level(squished_level,ep,quiet=True)912xi_bot = self.xi_at_level(squished_level+1,ep,quiet=True)913xis = -xi_top + xi_bot914summand = 1/QQ(ll) * self.gen_pullback_taut(xis,min_com,ep)915# calL pulled back to min_com:916summand -= 1/QQ(ll) * self.gen_pullback_taut(self.calL(ep, squished_level),min_com,ep)917if summand == 0:918# product is zero!919return self.ZERO920assert summand.is_equidimensional(),\921"Not all summands in %s of same degree!" % summand922return_list.append(summand)923# product over normal bundles:924if not return_list:925return 1 # empty product => transversal926NBprod = return_list[0]927for nb in return_list[1:]:928NBprod = self.intersection(NBprod,nb,min_com)929assert NBprod.is_equidimensional(), "Not all summands in %s of same degree!" % NBprod930return NBprod931932@cached_method933def gen_pullback(self,add_gen,o_enh_profile,amb_enh_profile=None):934"""935Generalised pullback of additive generator to o_enh_profile in amb_enh_profile.936937Args:938add_gen (AdditiveGenerator): additive generator on a degeneration of amb_enh_profile.939o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)940amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.941942Raises:943RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.944945Returns:946ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.947"""948# check input:949if amb_enh_profile is None:950amb_enh_profile = ((),0)951if not self.is_degeneration(o_enh_profile,amb_enh_profile):952raise RuntimeError("%r is not a degeneration of %r" % (o_enh_profile,amb_enh_profile))953s_enh_profile = add_gen.enh_profile954if not self.is_degeneration(s_enh_profile,amb_enh_profile):955raise RuntimeError("%r is not a degeneration of %r" % (s_enh_profile,amb_enh_profile))956degenerations = self.common_degenerations(s_enh_profile,o_enh_profile)957# if there are no common degenerations, pullback is 0958if not degenerations:959return self.ZERO960NB = self.cnb(s_enh_profile,o_enh_profile,amb_enh_profile)961# stop condition962if NB == 0 or NB == self.ZERO:963return 0964return_list = []965for L in degenerations:966if NB == 1:967# transversal968return_list.append(add_gen.pull_back(L))969else:970return_list.append(971self.intersection(972self.gen_pullback_taut(NB, L, self.minimal_common_undegeneration(s_enh_profile,o_enh_profile)),973add_gen.pull_back(L),974L975)976)977return_value = self.ELGsum(return_list)978if return_value != 0:979return return_value980else:981return self.ZERO982983def gen_pullback_taut(self, taut_class, o_enh_profile,amb_enh_profile=None):984"""985Generalised pullback of tautological class to o_enh_profile in amb_enh_profile.986987This simply returns the ELGSum of gen_pullback of all AdditiveGenerators.988989Args:990taut_class (ELGTautClass): tautological class each summand on a degeneration of amb_enh_profile.991o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)992amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.993994Raises:995RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.996997Returns:998ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.999"""1000return_list = []1001for c, AG in taut_class.psi_list:1002return_list.append(c * self.gen_pullback(AG, o_enh_profile, amb_enh_profile))1003return self.ELGsum(return_list)10041005## TODO: There should be a better way for this, using just BICs and where1006## marked points go ... (see discussion above)1007@cached_method1008def explicit_edge_becomes_long(self,enh_profile,edge):1009"""1010A list of enhanced profiles where the (explicit) edge 'edge' became long.10111012We go through the codim one degenerations of enh_profile and check1013each graph, if edge became long (under any degeneration).10141015However, we count each graph only once, even if there are several ways1016to undegenerate (see examples).10171018Args:1019enh_profile (tuple): enhanced profile: (profile, index).1020edge (tuple): edge of the LevelGraph associated to enh_profile:1021(start leg, end leg).10221023Raises:1024RuntimeError: Raised if the leg is not a leg of the graph of enh_profile.10251026Returns:1027list: list of enhanced profiles.10281029EXAMPLES ::10301031sage: from admcycles.diffstrata import *1032sage: X=Stratum((1,1))1033sage: V=[ep for ep in X.enhanced_profiles_of_length(1) if X.lookup_graph(*ep).level(0)._h0 == 2]1034sage: epV=V[0]1035sage: VLG=X.lookup_graph(*epV).LG1036sage: assert len(X.explicit_edge_becomes_long(epV, VLG.edges[1])) == 11037sage: assert X.explicit_edge_becomes_long(epV, VLG.edges[1]) == X.explicit_edge_becomes_long(epV, VLG.edges[1])10381039"""1040ep_list = []1041for ep in self.codim_one_degenerations(enh_profile):1042g = self.lookup_graph(*ep)1043if g.LG.has_long_edge:1044for leg_map in self.explicit_leg_maps(enh_profile,ep):1045try:1046if g.LG.is_long((leg_map[edge[0]],leg_map[edge[1]])):1047ep_list.append(ep)1048break # Not sure, if we want to record several occurences...1049except KeyError:1050raise RuntimeError("%r does not seem to be an edge of %r"1051% (edge, enh_profile))1052return ep_list10531054@cached_method1055def explicit_edges_between_levels(self,enh_profile,start_level,stop_level):1056"""1057Edges going from (relative) level start_level to (relative) level stop_level.10581059Note that we assume here that edges respect the level structure, i.e.1060start on start_level and end on end_level!10611062Args:1063enh_profile (tuple): enhanced profile1064start_level (int): relative level number (0...codim)1065stop_level (int): relative level number (0...codim)10661067Returns:1068list: list of edges, i.e. tuples (start_point,end_point)10691070EXAMPLES ::10711072sage: from admcycles.diffstrata import *1073sage: X=Stratum((2,))10741075Compact type:10761077sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 1]) == 110781079Banana:10801081sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 2]) == 110821083"""1084G = self.lookup_graph(*enh_profile)1085# TODO: There should be a way smarter way for doing this...1086edges = [e for e in G.LG.edges1087if (G.LG.level_number(G.LG.levelofleg(e[0])) == start_level and1088G.LG.level_number(G.LG.levelofleg(e[1])) == stop_level)]1089return edges10901091### Finding codimension one degenerations:1092# This is not very fancy yet.1093# At the moment, we take a profile and check at which places we can compatibly1094# insert a BIC (similarly to creating the lookup_list).1095# We then check "by hand", if this is ok with the enhanced structure, i.e.1096# on connected components.1097# Note that this check is bypassed if the input profile is irreducible.1098@cached_method1099def codim_one_degenerations(self,enh_profile):1100"""1101Degenerations of enh_profile with one more level.11021103Args:1104enh_profile (enhanced profile): tuple (profile, index)11051106Raises:1107RuntimeError: Error if we find a degeneration that doesn't squish1108back to the graph we started with.11091110Returns:1111list: list of enhanced profiles.11121113EXAMPLES ::11141115sage: from admcycles.diffstrata import *1116sage: X=GeneralisedStratum([Signature((4,))])1117sage: assert all(len(p) == 2 for p, _ in X.codim_one_degenerations(((2,),0)))11181119Empty profile gives all bics:11201121sage: assert X.codim_one_degenerations(((),0)) == [((0,), 0), ((1,), 0), ((2,), 0), ((3,), 0), ((4,), 0), ((5,), 0), ((6,), 0), ((7,), 0)]1122"""1123profile = list(enh_profile[0])1124# empty profile gives all bics:1125if not profile:1126return [((b,),0) for b in range(len(self.bics))]1127deg_list = []1128# build all length 1 profile extensions:1129# The first and last entry don't have any compatibility conditions:1130# add all top degenerations of the first guy1131for bic in self.DG.top_to_bic(profile[0]).values():1132deg_list.append(tuple([bic] + profile[:]))1133# and all bottom degenerations of the last guy1134for bic in self.DG.bot_to_bic(profile[-1]).values():1135deg_list.append(tuple(profile[:] + [bic]))1136# For the "middle" entries of the profile, we have to check compatibility1137for i in range(len(profile)-1):1138for bic in self.DG.bot_to_bic(profile[i]).values(): # candidates1139if bic in self.DG.top_to_bic(profile[i+1]).values():1140deg_list.append(tuple(profile[:i+1] + [bic] + profile[i+1:]))1141deg_list = list(set(deg_list)) # remove duplicates1142# We now build the list of enhanced profiles:1143enh_list = []1144for p in deg_list:1145for i in range(len(self.lookup(p))):1146if self.is_degeneration((p,i),enh_profile):1147enh_list.append((p,i))1148return enh_list11491150@cached_method1151def codim_one_common_undegenerations(self,s_enh_profile,o_enh_profile,amb_enh_profile=None):1152"""1153Profiles that are 1-level degenerations of amb_enh_profile and include1154s_enh_profile and o_enh_profile.11551156Args:1157s_enh_profile (tuple): enhanced profile1158o_enh_profile (tuple): enhanced profile1159amb_enh_profile (tuple): enhanced profile11601161Returns:1162list: list of enhanced profiles11631164EXAMPLES ::11651166"""1167if amb_enh_profile is None:1168amb_enh_profile = ((),0)1169profile_list = []1170for ep in self.codim_one_degenerations(amb_enh_profile):1171if self.is_degeneration(s_enh_profile,ep) and self.is_degeneration(o_enh_profile,ep):1172profile_list.append(ep)1173return profile_list11741175@cached_method1176def minimal_common_undegeneration(self,s_enh_profile,o_enh_profile):1177"""1178The minimal dimension graph that is undegeneration of both s_enh_profile1179and o_enh_profile.11801181Args:1182s_enh_profile (tuple): enhanced profile1183o_enh_profile (tuple): enhanced profile11841185Raises:1186RuntimeError: If there are no common undgenerations in the intersection profile.11871188Returns:1189tuple: enhanced profile11901191EXAMPLES ::11921193"""1194s_profile = s_enh_profile[0]1195o_profile = o_enh_profile[0]1196# build "sorted" intersection1197intersection = []1198for b in s_profile:1199if b in o_profile:1200intersection.append(b)1201# make hashable1202intersection = tuple(intersection)1203# if the intersection profile is irreducible, we are done:1204if len(self.lookup(intersection)) == 1:1205return (intersection, 0)1206else:1207for i in range(len(self.lookup(intersection))):1208if (self.is_degeneration(s_enh_profile,(intersection,i)) and1209self.is_degeneration(o_enh_profile,(intersection,i))):1210return (intersection, i)1211else:1212raise RuntimeError("No common undegeneration in profile %r" % intersection)12131214@cached_method1215def is_degeneration(self,s_enh_profile,o_enh_profile):1216"""1217Check if s_enh_profile is a degeneration of o_enh_profile.12181219Args:1220s_enh_profile (tuple): enhanced profile1221o_enh_profile (tuple): enhanced profile12221223Returns:1224bool: True if the graph associated to s_enh_profile is a degeneration1225of the graph associated to o_enh_profile, False otherwise.12261227EXAMPLES ::12281229sage: from admcycles.diffstrata import *1230sage: X=GeneralisedStratum([Signature((4,))])1231sage: assert X.is_degeneration(((7,),0),((7,),0))12321233The empty tuple corresponds to the stratum:12341235sage: assert X.is_degeneration(((2,),0),((),0))1236"""1237s_profile = s_enh_profile[0]1238o_profile = o_enh_profile[0]1239# first check: subset:1240if not set(o_profile) <= set(s_profile):1241return False1242# in the irreducible case, we are done:1243if len(self.lookup(s_profile)) == len(self.lookup(o_profile)) == 1:1244assert self.explicit_leg_maps(o_enh_profile,s_enh_profile),\1245"%r and %r contain only one graph, but these are not degenerations!"\1246% (o_enh_profile,s_enh_profile)1247return True1248else:1249# otherwise: check if an undegeneration map exists:1250try:1251if self.explicit_leg_maps(o_enh_profile,s_enh_profile,only_one=True) is None:1252return False1253else:1254return True1255except UserWarning:1256# This is raised if there is no undegeneration inside the expected profile...1257return False12581259@cached_method1260def squish(self, enh_profile, l):1261"""1262Squish level l of the graph associated to enh_profile. Returns the enhanced profile1263associated to the squished graph.12641265Args:1266enh_profile (tuple): enhanced profile1267l (int): level of graph associated to enhanced profile12681269Raises:1270RuntimeError: Raised if a BIC is squished at a level other than 0.1271RuntimeError: Raised if the squished graph is not found in the squished profile.12721273Returns:1274tuple: enhanced profile.12751276EXAMPLES ::12771278sage: from admcycles.diffstrata import *1279sage: X=Stratum((2,))1280sage: assert all(X.squish(ep,0) == ((),0) for ep in X.enhanced_profiles_of_length(1))1281sage: assert all(X.squish((p,i),1-l) == ((p[l],),0) for p, i in X.enhanced_profiles_of_length(2) for l in range(2))1282"""1283p, i = enh_profile1284if len(p) == 1:1285if l != 0:1286raise RuntimeError("BIC can only be squished at level 0!" % (enh_profile, l))1287return ((), 0)1288new_p = list(p)1289new_p.pop(l)1290new_p = tuple(new_p)1291enhancements = []1292for j in range(len(self.lookup(new_p))):1293if self.is_degeneration(enh_profile, (new_p,j)):1294enhancements.append(j)1295if len(enhancements) != 1:1296raise RuntimeError("Cannot squish %r at level %r! No unique graph found in %r!" % (enh_profile, l, new_p))1297return (new_p, enhancements[0])12981299### Partial order1300## The lookup graph gives a partial order on the BICs (the 3-level graph (i,j)1301## implies i > j).1302@cached_method1303def lies_over (self,i,j):1304"""1305Determine if (i,j) is a 3-level graph.13061307Args:1308i (int): Index of BIC.1309j (int): Index of BIC.13101311Returns:1312bool: True if (i,j) is a 3-level graph, False otherwise.13131314EXAMPLES ::13151316"""1317if j in self.DG.bot_to_bic(i).values():1318assert i in self.DG.top_to_bic(j).values(),\1319"%r is a bottom degeneration of %r, but %r is not a top degeneration of %r!"\1320% (j,i,i,j)1321return True1322else:1323assert i not in self.DG.top_to_bic(j).values(),\1324"%r is not a bottom degeneration of %r, but %r is a top degeneration of %r!"\1325% (j,i,i,j)1326return False13271328### Merging profiles (with respect to lies_over)1329@cached_method1330def merge_profiles(self,p,q):1331"""1332Merge profiles with respect to the ordering "lies_over".13331334Args:1335p (iterable): sorted profile1336q (iterable): sorted profile13371338Returns:1339tuple: sorted profile or None if no such sorted profile exists.13401341EXAMPLES ::13421343sage: from admcycles.diffstrata import *1344sage: X=GeneralisedStratum([Signature((4,))])1345sage: X.merge_profiles((5,),(5,))1346(5,)1347"""1348# input profiles should be sorted:1349assert all(self.lies_over(p[i],p[i+1]) for i in range(len(p)-1)),\1350"Profile %r not sorted!" % (p,)1351assert all(self.lies_over(q[i],q[i+1]) for i in range(len(q)-1)),\1352"Profile %r not sorted!" % (q,)1353new_profile = []1354next_p = 01355next_q = 01356while next_p < len(p) and next_q < len(q):1357if p[next_p] == q[next_q]:1358new_profile.append(p[next_p])1359next_p += 11360next_q += 11361else:1362if self.lies_over(p[next_p],q[next_q]):1363new_profile.append(p[next_p])1364next_p += 11365else:1366if self.lies_over(q[next_q],p[next_p]):1367new_profile.append(q[next_q])1368else:1369return None1370next_q += 11371# pick up rest (one of these is empty!):1372new_profile += p[next_p:]1373new_profile += q[next_q:]1374return tuple(new_profile)13751376### Better graph lookup:1377## Here we should really work with "enhanced dominos", because1378## otherwise it's not clear how the list indices of degenerations are related1379## to each other.1380## Therefore, arguments are:1381## * a sorted(!) list of BICs, i.e. an element of the lookup_list1382## * a (consistent) choice of components of the involved 3-level graph (i.e.1383## enhanced dominos)1384## This can consistently produce a graph.1385##1386## For now, we use the workaround to forcably only work with sorted profiles1387## where the indexing is at least consistent.1388###13891390def lookup_graph(self,bic_list,index=0):1391"""1392Return the graph associated to an enhanced profile.13931394Note that starting in SAGE 9.0 profile numbering will change between sessions!13951396Args:1397bic_list (iterable): (sorted!) tuple/list of indices of bics.1398index (int, optional): Index in lookup list. Defaults to 0.13991400Returns:1401EmbeddedLevelGraph: graph associated to the enhanced (sorted) profile1402(None if empty).14031404EXAMPLES ::14051406sage: from admcycles.diffstrata import *1407sage: X=Stratum((2,))1408sage: X.lookup_graph(())1409EmbeddedLevelGraph(LG=LevelGraph([2],[[1]],[],{1: 2},[0],True),dmp={1: (0, 0)},dlevels={0: 0})14101411Note that an enhanced profile needs to be unpacked with *:14121413sage: X.lookup_graph(*X.enhanced_profiles_of_length(2)[0]) # 'unsafe' (edge ordering may change) # doctest:+SKIP1414EmbeddedLevelGraph(LG=LevelGraph([1, 0, 0],[[1], [2, 3, 4], [5, 6, 7]],[(1, 4), (2, 6), (3, 7)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2},[0, -1, -2],True),dmp={5: (0, 0)},dlevels={0: 0, -1: -1, -2: -2})14151416"""1417# this is a bit stupid, but whatever...1418if all(self.lies_over(bic_list[i],bic_list[i+1]) for i in range(len(bic_list)-1)):1419return self.lookup(bic_list)[index]1420else:1421return None14221423def lookup(self,bic_list, quiet=True):1424"""1425Take a profile (i.e. a list of indices of BIC) and return the corresponding1426EmbeddedLevelGraphs (i.e. the product of these BICs).14271428Note that this will be a one element list "most of the time", but1429it can happen that this is not irreducible:14301431This implementation is not dependent on the order (!) (we look in top and1432bottom degenerations and clutch...)14331434However, for caching purposes, it makes sense to use only the sorted profiles...14351436NOTE THAT IN PYTHON3 PROFILES ARE NO LONGER DETERMINISTIC!!!!!14371438(they typically change with every python session...)14391440Args:1441bic_list (iterable): list of indices of bics14421443Returns:1444list: The list of EmbeddedLevelGraphs corresponding to the profile.14451446EXAMPLES ::14471448sage: from admcycles.diffstrata import *1449sage: X=GeneralisedStratum([Signature((4,))])14501451This is independent of the order.14521453sage: p, _ = X.enhanced_profiles_of_length(2)[0]1454sage: assert any(X.lookup(p)[0].is_isomorphic(G) for G in X.lookup((p[1],p[0])))14551456Note that the profile can be empty or reducible.14571458"""1459if not quiet:1460print("Looking up enhanced profiles in %r..." % (bic_list,))1461sys.stdout.flush() # MPI computer has congestion issues...1462lookup_key = tuple(bic_list)1463if not bic_list: # empty1464if not quiet:1465print("Empty profile, returning smooth_LG. Done.")1466sys.stdout.flush()1467return [self.smooth_LG]1468if len(bic_list) == 1:1469if not quiet:1470print("BIC, profile irreducible by definition. Done.")1471sys.stdout.flush()1472return [self.bics[bic_list[0]]]1473try:1474cached_list = self._lookup[lookup_key]1475if not quiet:1476print("Using cached lookup. Done.")1477sys.stdout.flush()1478return cached_list1479except KeyError:1480bic_list = list(bic_list) # in case we are passed a tuple...1481# otherwise, making a copy if we're about to manipulate is also not1482# such a bad idea...1483i = bic_list.pop() # index in self.bics1484B = self.bics[i] # this might build bics (!)1485# We split the remainder of bic_list into those coming from1486# degenerations of the top component and those from bottom.1487# Note that these lists will contain their indices in B.top1488# and B.bot, respectively.1489# Moreover, they have to be nested in case there are multiple components.1490top_lists = [[]]1491bot_lists = [[]]1492for j in bic_list:1493if not quiet:1494print("Looking at BIC %r:" % j, end=' ')1495sys.stdout.flush()1496# a bic is either in the image of top_to_bic1497# or bot_to_bic.1498# If it isn't in any image, the intersection is empty1499# and we return None.1500# Note that again this might build the maps.1501try:1502top_bics = self.DG.top_to_bic_inv(i)[j]1503if not quiet:1504print("Adding %r BICs from top component to top_lists..." % len(top_bics))1505sys.stdout.flush()1506# if there are several components, we "branch out":1507new_top_lists = []1508for b in top_bics:1509for top_list in top_lists:1510new_top_lists.append(top_list + [b])1511top_lists = new_top_lists1512except KeyError:1513try:1514bot_bics = self.DG.bot_to_bic_inv(i)[j]1515if not quiet:1516print("Adding %r BICs from bottom component to bot_lists..." % len(bot_bics))1517sys.stdout.flush()1518# if there are several components, we "branch out":1519new_bot_lists = []1520for b in bot_bics:1521for bot_list in bot_lists:1522new_bot_lists.append(bot_list + [b])1523bot_lists = new_bot_lists1524except KeyError:1525# Intersection empty.1526return []1527if not quiet:1528print("Done building top_lists and bot_lists.")1529print("This gives us %r profiles in %s and %r profiles in %s that we will now clutch pairwise and recursively." % \1530(len(top_lists), B.top, len(bot_lists), B.bot))1531sys.stdout.flush()1532graph_list = [admcycles.diffstrata.stratatautring.clutch(1533self,1534top,1535bot,1536B.clutch_dict,1537B.emb_top,1538B.emb_bot1539)1540for top_list, bot_list in itertools.product(top_lists,bot_lists)1541for top in B.top.lookup(top_list, quiet=quiet)1542for bot in B.bot.lookup(bot_list, quiet=quiet)1543]1544# we might have picked up isomorphic guys (e.g. v-graph)1545if not quiet:1546print("For profile %r in %s, we have thus obtained %r graphs." %\1547(bic_list, self, len(graph_list)))1548print("Sorting these by isomorphism class...", end=' ')1549sys.stdout.flush()1550rep_list = admcycles.diffstrata.bic.isom_rep(graph_list)1551self._lookup[lookup_key] = rep_list1552if not quiet:1553print("Done. Found %r isomorphism classes." % len(rep_list))1554sys.stdout.flush() # MPI computer has congestion issues...1555return rep_list15561557@cached_method1558def sub_graph_from_level(self,enh_profile,l,direction='below',return_split_edges=False):1559"""1560Extract an EmbeddedLevelGraph from the subgraph of enh_profile that is either1561above or below level l.15621563This is embedded into the top/bottom component of the bic at profile[l-1].1564In particular, this is a 'true' sub graph, i.e. the names of the vertices and1565legs are the same as in enh_profile.15661567Note: For l==0 or l==number_of_levels we just return enh_profile.15681569Args:1570l (int): (relative) level number.1571direction (str, optional): 'above' or 'below'. Defaults to 'below'.1572return_split_edges (bool, optional. Defaults to False): also return a tuple1573of the edges split across level l.15741575Returns:1576EmbeddedLevelGraph: Subgraph of top/bottom component of the bic at profile[l-1].15771578If return_split_edges=True: Returns tuple (G,e) where1579* G is the EmbeddedLevelGraph1580* e is a tuple of edges of enh_profile that connect legs above level1581l with those below (i.e. those edges needed for clutching!)15821583EXAMPLES ::15841585sage: from admcycles.diffstrata import *1586sage: X=Stratum((2,))1587sage: ep = X.enhanced_profiles_of_length(2)[0]1588sage: X.sub_graph_from_level(ep, 1, 'above')1589EmbeddedLevelGraph(LG=LevelGraph([1],[[1]],[],{1: 0},[0],True),dmp={1: (0, 0)},dlevels={0: 0})1590sage: X.sub_graph_from_level(ep, 1, 'below') # 'unsafe' (edge order might change) # doctest:+SKIP1591EmbeddedLevelGraph(LG=LevelGraph([0, 0],[[2, 3, 4], [5, 6, 7]],[(2, 6), (3, 7)],{2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2},[-1, -2],True),dmp={5: (0, 0), 4: (0, 1)},dlevels={-1: -1, -2: -2})1592sage: X.bics[ep[0][0]].top1593LevelStratum(sig_list=[Signature((0,))],res_cond=[],leg_dict={1: (0, 0)})1594sage: X.bics[ep[0][1]].bot1595LevelStratum(sig_list=[Signature((2, -2, -2))],res_cond=[[(0, 1), (0, 2)]],leg_dict={3: (0, 0), 4: (0, 1), 5: (0, 2)})1596"""1597G = self.lookup_graph(*enh_profile)1598if l == 0:1599if direction == 'below':1600if return_split_edges:1601return (G,tuple())1602return G1603if return_split_edges:1604return (None,tuple())1605return None1606if l == G.number_of_levels:1607if direction == 'above':1608if return_split_edges:1609return (G,tuple())1610return G1611if return_split_edges:1612return (None,tuple())1613return None1614profile, _i = enh_profile1615# The BIC that will give us the level is BIC l-1 in the profile:1616bic_number = profile[l-1]1617B = self.bics[bic_number]1618# We extract the subgraph from the underlying LevelGraph, so we have1619# to work with the internal level numbering:1620internal_l = G.LG.internal_level_number(l)1621# Actually only three things depend on above/below:1622# * The choice of vertices in the subgraph.1623# * The choice of level to embed into (top/bottom of B).1624# * The new level dictionary (as extracting does not change the levels,1625# this just consists of the releveant part of G.dlevels)1626# Note that in the 'below' case we consider levels <= l, while in 'above'1627# we consider > l (we want to cut level passage l!)1628if direction == 'below':1629new_vertices = [v for v in range(len(G.LG.genera))1630if G.LG.levelofvertex(v) <= internal_l]1631# in this case, the level we want to embed into is the bottom of B1632L = B.bot1633# the levels <= internal_l survive into dlevels1634new_dlevels = {k:v for k,v in G.dlevels.items() if k <= internal_l}1635else:1636assert direction == 'above'1637new_vertices = [v for v in range(len(G.LG.genera))1638if G.LG.levelofvertex(v) > internal_l]1639# in this case, the level we want to embed into is the top of B1640L = B.top1641# the levels >= internal_l survive into dlevels1642new_dlevels = {k:v for k,v in G.dlevels.items() if k > internal_l}1643vertex_set = set(new_vertices)1644new_edges = [e for e in G.LG.edges1645if G.LG.vertex(e[0]) in vertex_set and \1646G.LG.vertex(e[1]) in vertex_set]1647new_LG = G.LG.extract(new_vertices,new_edges)1648leg_set = set(flatten(new_LG.legs))1649# Next, we take the part of dmp that we still need:1650# Note that G.dmp maps legs of G to points of X, but we want is a map1651# to points of L.1652# We get this from the following observation:1653# We have1654# * L.leg_dict: points of B -> points of L1655# * B.dmp_inv: points of X -> points of B1656# Therefore the composition gives the desired map1657# points of G -> points of L1658new_dmp = {k : L.leg_dict[B.dmp_inv[v]]1659for k, v in G.dmp.items() if k in leg_set}1660# The only thing missing is to add the marked points of the edges1661# that we have cut:1662# We do this in no particular order, as the clutching information will1663# have to be retrieved anyways when actually splitting the graph.1664# Note that != is boolean xor (!)1665split_edges = [e for e in G.LG.edges1666if (e[0] in leg_set) != (e[1] in leg_set)]1667split_half_edges = [e[0] if e[0] in leg_set else e[1]1668for e in split_edges]1669# To place these into new_dmp, we pick an undegeneration map G -> B1670# Note that the choice of map *should* not matter, as they should differ1671# only by an automorphism of B... (except for psi classes, where we have1672# to be careful with xi_on_level!!!)1673B_to_G = self.explicit_leg_maps(((bic_number,),0),enh_profile,only_one=True)1674assert B_to_G # G is actually a degeneration of B!1675G_to_B = {v : k for k, v in B_to_G.items()}1676# check the points we already placed are consistent:1677assert all(L.leg_dict[G_to_B[leg]] == new_dmp[leg] for leg in new_dmp)1678while split_half_edges:1679leg = split_half_edges.pop()1680new_dmp[leg] = L.leg_dict[G_to_B[leg]]1681# some more checks:1682legs_in_new_edges = set(flatten(new_edges))1683marked_points = set(new_dmp.keys())1684assert legs_in_new_edges.isdisjoint(marked_points)1685assert leg_set == (legs_in_new_edges | marked_points)1686sub_graph = EmbeddedLevelGraph(L,new_LG,new_dmp,new_dlevels)1687if return_split_edges:1688return (sub_graph, tuple(split_edges))1689return sub_graph16901691# @cached_method1692def split_graph_at_level(self,enh_profile,l):1693"""1694Splits enh_profile self into top and bottom component at level l.16951696(Note that the 'cut' occurs right above level l, i.e. to get the top level1697and the rest, l should be 1! (not 0))16981699The top and bottom components are EmbeddedLevelGraphs, embedded into1700top and bottom of the corresponding BIC (obtained via sub_graph_from_level).17011702The result is made so that it can be fed into clutch.17031704Args:1705enh_profile (tuple): enhanced profile.1706l (int): (relative) level of enh_profile.17071708Returns:1709dict: dictionary consising of1710* X: GeneralisedStratum self.X1711* top: EmbeddedLevelGraph: top component1712* bottom: EmbeddedLevelGraph: bottom component1713* clutch_dict: clutching dictionary mapping ex-half-edges on1714top to their partners on bottom (both as points in the1715respective strata via dmp!)1716* emb_dict_top: a dictionary embedding top into the stratum of self1717* emb_dict_bot: a dictionary embedding bot into the stratum of self1718* leg_dict: a dictionary legs of enh_profile -> legs of top/bottom17191720Note that clutch_dict, emb_top and emb_bot are dictionaries between1721points of strata, i.e. after applying dmp to the points!17221723EXAMPLES ::17241725In particular, we can use this to "glue" the BICs of 10^top into (10,9,6) and1726obtain all components of the profile.17271728"""1729# Split the graph into top and bottom components at level l:1730top_graph, se_top = self.sub_graph_from_level(enh_profile,l,direction='above',return_split_edges=True)1731bot_graph, se_bot = self.sub_graph_from_level(enh_profile,l,direction='below',return_split_edges=True)1732assert se_top == se_bot1733split_edges = se_top1734# We construct the clutching info by splitting the BIC that corresponds1735# to level l:1736p, _i = enh_profile1737# TODO: edge cases1738B = self.bics[p[l-1]]1739clutching_info = B.split()1740# we simply replace the top and bottom components of B by our graphs:1741assert clutching_info['top'] == top_graph.X == B.top1742clutching_info['top'] = top_graph1743assert clutching_info['bottom'] == bot_graph.X == B.bot1744clutching_info['bottom'] = bot_graph1745# the clutch_dict has to be replaced by the split_edges:1746# Note that these are currently edges of enh_profile, so they need to be1747# translated to points on the corresponding stratum via the embedding1748# of top_graph/bot_graph:1749# WARNING: We use here (once again) implicitly that e[0] is above e[1]!1750clutching_info['clutch_dict'] = {top_graph.dmp[e[0]] : bot_graph.dmp[e[1]]1751for e in split_edges}1752return clutching_info17531754# @cached_method1755def doublesplit(self,enh_profile):1756"""1757Splits embedded 3-level graph into top, middle and bottom component, along with1758all the information required (by clutch) to reconstruct self.17591760We return a dictionary so that the result can be fed into clutch (naming of1761optional arguments...)17621763This is mainly a technical backend for doublesplit_graph_before_and_after_level.17641765Note that in contrast to EmbeddedLevelGraph.split, we want to feed a length-2-profile1766so that we can really split into the top and bottom of the associated BICs (the only1767strata we can control!)17681769This method is mainly intended for being fed into clutch.17701771Args:1772enh_profile (tuple): enhanced profile.17731774Raises:1775ValueError: Raised if self is not a 3-level graph.17761777Returns:1778dict: A dictionary consisting of:1779X: GeneralisedStratum self,1780top: LevelStratum top level of top BIC,1781bottom: LevelStratum bottom level of bottom BIC,1782middle: LevelStratum level -1 of enh_profile,1783emb_dict_top: dict: points of top stratum -> points of X,1784emb_dict_bot: dict: points of bottom stratum -> points of X,1785emb_dict_mid: dict: points of middle stratum -> points of X,1786clutch_dict: dict: points of top stratum -> points of middle stratum,1787clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,1788clutch_dict_long: dict: points of top stratum -> points of bottom stratum.17891790EXAMPLES ::179117921793Long edges work.17941795"""1796p, i = enh_profile1797if not len(p) == 2:1798raise ValueError("Error: Not a 3-level graph! %r" % self)1799G = self.lookup_graph(p,i)1800# Here it is important that we pick top and bot of the corresponding BICs and identify them with1801# level(0) and level(2) of G (as these might not be the same (e.g. switched components!)!)1802top = self.bics[p[0]].top1803middle = G.level(1)1804bottom = self.bics[p[1]].bot1805# To construct the embedding dictionaries, we have to identify legs of G1806# with (stratum) points of top/middle/bottom as keys and points on self as1807# values.1808#1809# The values (points on self) are given by G.dmp.1810#1811# The keys for middle are given via leg_dict.1812#1813# For top and bottom, we have to first fix a map from G to1814# p[0] and p[1] and then combine self.dmp with the leg_dicts of the LevelStrata.1815# It *shouldn't* matter, which undegeneration we take:1816top_to_G = self.explicit_leg_maps(((p[0],),0),enh_profile,only_one=True)1817G_to_top = {v : k for k, v in top_to_G.items()}1818bot_to_G = self.explicit_leg_maps(((p[1],),0),enh_profile,only_one=True)1819G_to_bot = {v : k for k, v in bot_to_G.items()}1820# More precisely: We now have the following maps (e.g. for top):1821#1822# G_to_top: points in G -> points in p[0]1823# top.leg_dict: points in p[0] -> stratum points of top1824#1825# and1826#1827# top.leg_dict_inv: stratum points of top -> points in p[0]1828# top_to_G: points in p[0] -> points in G1829# G.dmp: points in G -> stratum points on self1830#1831# i.e. emb_top is the composition of the inverse of the leg_dict1832# of top, i.e. top.stratum_number, with top_to_G and G.dmp1833# (giving a map from the points of top to the points of self)1834# and the same for middle and bottom.1835#1836# We implement this by iterating over the marked points of G on top level,1837# which are exactly the keys of G.dmp that are on top level.1838#1839# For this, we have to compose with G_to_top and top.leg_dict again.1840#1841# Note that we make extra sure that we didn't mess up the level numbering by1842# using the relative level numbering (where the top level is guaranteed to be 0,1843# the middle is 1 and the bottom level is 2 (positive!)).1844emb_dict_top = {top.leg_dict[G_to_top[l]] : G.dmp[l]1845for l in iter(G.dmp)1846if G.LG.level_number(G.LG.levelofleg(l)) == 0}1847emb_dict_mid = {middle.leg_dict[l] : G.dmp[l]1848for l in iter(G.dmp)1849if G.LG.level_number(G.LG.levelofleg(l)) == 1}1850emb_dict_bot = {bottom.leg_dict[G_to_bot[l]] : G.dmp[l]1851for l in iter(G.dmp)1852if G.LG.level_number(G.LG.levelofleg(l)) == 2}1853# Because this is a 3-level graph, all edges of self are cut in this process1854# and this gives us exactly the dictionary we must remember:1855# Note however, that we have to check if the edge connects top - middle, middle - bottom1856# or top - bottom.1857# Note that all these dictionaries map points of GeneralisedStrata to each1858# other so we must take the corresponding stratum_number!1859clutch_dict = {}1860clutch_dict_lower = {}1861clutch_dict_long = {}1862# If the edges are not sorted with e[0] above e[1], we complain.1863for e in G.LG.edges:1864if G.LG.level_number(G.LG.levelofleg(e[0])) == 0:1865if G.LG.level_number(G.LG.levelofleg(e[1])) == 1:1866clutch_dict[top.stratum_number(G_to_top[e[0]])] = middle.stratum_number(e[1])1867else:1868assert G.LG.level_number(G.LG.levelofleg(e[1])) == 21869clutch_dict_long[top.stratum_number(G_to_top[e[0]])] = bottom.stratum_number(G_to_bot[e[1]])1870else:1871assert G.LG.level_number(G.LG.levelofleg(e[0])) == 11872assert G.LG.level_number(G.LG.levelofleg(e[1])) == 21873clutch_dict_lower[middle.stratum_number(e[0])] = bottom.stratum_number(G_to_bot[e[1]])1874return {'X': self, 'top': top, 'bottom': bottom, 'middle': middle,1875'emb_dict_top': emb_dict_top, 'emb_dict_mid': emb_dict_mid, 'emb_dict_bot': emb_dict_bot,1876'clutch_dict': clutch_dict, 'clutch_dict_lower': clutch_dict_lower, 'clutch_dict_long': clutch_dict_long}18771878@cached_method1879def three_level_profile_for_level(self,enh_profile,l):1880"""1881Find the 3-level graph that has level l of enh_profile as its middle level.18821883Args:1884enh_profile (tuple): enhanced profile1885l (int): (relative) level number18861887Raises:1888RuntimeError: raised if no unique (or no) 3-level graph is found.18891890Returns:1891tuple: enhanced profile of the 3-level graph.18921893EXAMPLES ::18941895"""1896profile, _ = enh_profile1897three_level_profile = (profile[l-1],profile[l])1898# in case this is reducible, we have to find the correct enhanced profile:1899possible_enhancements = len(self.lookup(three_level_profile))1900assert possible_enhancements > 0, "No 3-level graph for subprofile %r of %r found!" % (three_level_profile, profile)1901enhancements = []1902for i in range(possible_enhancements):1903if self.is_degeneration(enh_profile,(three_level_profile,i)):1904enhancements.append(i)1905if len(enhancements) != 1:1906raise RuntimeError("No unique 3-level undegeneration in %r around level %r! %r" % (three_level_profile, l, enhancements))1907return (three_level_profile,enhancements[0])19081909# @cached_method1910def doublesplit_graph_before_and_after_level(self,enh_profile,l):1911"""1912Split the graph enh_profile directly above and below level l.19131914This can be used for gluing an arbitrary degeneration of level l into enh_profile.19151916The result is made so that it can be fed into clutch.19171918To ensure compatibility with top/bot/middle_to_bic when gluing, we have1919to make sure that everything is embedded into the "correct" generalised strata.19201921We denote the 3-level graph around level l by H.19221923Then the top part will be embedded into the top of the top BIC of H,1924the bottom part will be embedded into the bot of the bottom BIC of H1925and the middle will be the middle level of H.19261927For a 3-level graph is (almost) equivalent to doublesplit(), the only difference1928being that here we return the 0-level graph for each level.19291930Args:1931enh_profile (tuple): enhanced profile.1932l (int): (relative) level of enh_profile.19331934Raises:1935ValueError: Raised if l is 0 or lowest level.1936RuntimeError: Raised if we don't find a unique 3-level graph around l.19371938Returns:1939dict: A dictionary consisting of:1940X: GeneralisedStratum self.X,1941top: LevelStratum top level of top BIC of H,1942bottom: LevelStratum bottom level of bottom BIC of H,1943middle: LevelStratum middle level of H,1944emb_dict_top: dict: points of top stratum -> points of X,1945emb_dict_bot: dict: points of bottom stratum -> points of X,1946emb_dict_mid: dict: points of middle stratum -> points of X,1947clutch_dict: dict: points of top stratum -> points of middle stratum,1948clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,1949clutch_dict_long: dict: points of top stratum -> points of bottom stratum.19501951EXAMPLES ::19521953sage: from admcycles.diffstrata import *1954sage: X=GeneralisedStratum([Signature((4,))])1955sage: assert all(clutch(**X.doublesplit_graph_before_and_after_level(ep,l)).is_isomorphic(X.lookup_graph(*ep)) for levels in range(3,X.dim()+1) for ep in X.enhanced_profiles_of_length(levels-1) for l in range(1,levels-1))1956sage: X=GeneralisedStratum([Signature((2,2,-2))])1957sage: assert all(clutch(**X.doublesplit_graph_before_and_after_level(ep,l)).is_isomorphic(X.lookup_graph(*ep)) for levels in range(3,X.dim()+2) for ep in X.enhanced_profiles_of_length(levels-1) for l in range(1,levels-1)) # long time1958"""1959p, i = enh_profile1960if l == 0 or l == len(p) + 1:1961raise ValueError("Doublesplit must occur at 'inner' level! %r" % l)1962G = self.lookup_graph(p,i)1963# Split the graph into top and bottom components around level l:1964top_graph, se_top = self.sub_graph_from_level(enh_profile,l,direction='above',return_split_edges=True)1965bot_graph, se_bot = self.sub_graph_from_level(enh_profile,l+1,direction='below',return_split_edges=True)1966# We construct the clutching info by splitting the 3-level graph around l1967# Note that the middle level is really the same as that of enh_profile (that's1968# why we have to care about components of the profile here), but the leg1969# numbering might be different, so we still have to work with an undegeneration map:1970t_l_enh_profile = self.three_level_profile_for_level(enh_profile,l)1971clutching_info = self.doublesplit(t_l_enh_profile)1972assert top_graph.X == clutching_info['top']1973assert bot_graph.X == clutching_info['bottom']1974L = clutching_info['middle']1975assert L == self.lookup_graph(*t_l_enh_profile).level(1)1976# we simply replace the top and bottom components of B by our graphs:1977clutching_info['top'] = top_graph1978clutching_info['bottom'] = bot_graph1979# Now we have to match up the edges:1980# Note that se_top consists of the edges connecting top_graph to any vertex1981# on or below level l1982# We therefore start by distinguishing those edges ending on level l from the others1983# (long edges):1984# WARNING: We use here (once again) implicitly that e[0] is above e[1]!1985top_to_l = []1986top_to_bot = []1987for e in se_top:1988if G.LG.level_number(G.LG.levelofleg(e[1])) == l:1989top_to_l.append(e)1990else:1991top_to_bot.append(e)1992# the same for se_bot:1993bot_to_l = []1994bot_to_top = []1995for e in se_bot:1996if G.LG.level_number(G.LG.levelofleg(e[0])) == l:1997bot_to_l.append(e)1998else:1999bot_to_top.append(e)2000assert set(top_to_bot) == set(bot_to_top)2001# Translating the edges into points on the strata immediately gives the2002# three clutching dictionaries:2003# Note that instead of directly using leg_dict for the middle level,2004# we first pick an undegeneration map to the 3-level graph and compose2005# with (the inverse of) that:2006middle_leg_map = self.explicit_leg_maps(t_l_enh_profile,enh_profile,only_one=True)2007ep_to_m = {v : k for k, v in middle_leg_map.items()}2008# WARNING: We use here (once again) implicitly that e[0] is above e[1]!2009clutching_info['clutch_dict'] = {top_graph.dmp[e[0]] : L.leg_dict[ep_to_m[e[1]]]2010for e in top_to_l}2011clutching_info['clutch_dict_lower'] = {L.leg_dict[ep_to_m[e[0]]] : bot_graph.dmp[e[1]]2012for e in bot_to_l}2013clutching_info['clutch_dict_long'] = {top_graph.dmp[e[0]] : bot_graph.dmp[e[1]]2014for e in top_to_bot}2015return clutching_info20162017# @cached_method2018def splitting_info_at_level(self,enh_profile,l):2019"""2020Retrieve the splitting and embedding dictionaries for splitting at level l,2021as well as the level in 'standard form', i.e. as either:2022* a top of a BIC2023* a bot of a BIC2024* a middle of a 3-level graph20252026This is essentially only a frontend for split_graph_at_level and2027doublesplit_graph_before_and_after_level and saves us the annoying2028case distinction.20292030This is important, because when we glue we should *always* use the2031dmp's of the splitting dictionary, which can (and will) be different2032from leg_dict of the level!20332034Args:2035enh_profile (tuple): enhanced profile2036l (int): (relative) level number20372038Returns:2039tuple: (splitting dict, leg_dict, level) where2040splitting dict is the splitting dictionary:2041* X: GeneralisedStratum self.X2042* top: EmbeddedLevelGraph: top component2043* bottom: EmbeddedLevelGraph: bottom component2044* clutch_dict: clutching dictionary mapping ex-half-edges on2045top to their partners on bottom (both as points in the2046respective strata via dmp!)2047* emb_dict_top: a dictionary embedding top into the stratum of self2048* emb_dict_bot: a dictionary embedding bot into the stratum of self20492050leg_dict is the dmp at the current level (to be used instead2051of leg_dict of G.level(l)!!!)20522053and level is the 'standardised' LevelStratum at l (as described above).20542055Note that clutch_dict, emb_top and emb_bot are dictionaries between2056points of strata, i.e. after applying dmp to the points!20572058"""2059profile, _ = enh_profile2060# For this, we have to distinguish again, if we're gluing into the middle2061# (two cuts) or at one end of the profile (1 cut):2062if l == 0:2063d = self.split_graph_at_level(enh_profile,1)2064assert d['top'].is_isomorphic(d['top'].X.smooth_LG)2065return d, d['top'].dmp, d['top'].X2066if l == len(profile):2067d = self.split_graph_at_level(enh_profile,l)2068assert d['bottom'].is_isomorphic(d['bottom'].X.smooth_LG)2069return d, d['bottom'].dmp, d['bottom'].X2070d = self.doublesplit_graph_before_and_after_level(enh_profile,l)2071three_level_profile = self.three_level_profile_for_level(enh_profile,l)2072assert self.lookup_graph(*three_level_profile).level(1) == d['middle']2073# for the middle level, we have to use the undegeneration map to2074# the 3-level graph:2075middle_leg_map = self.explicit_leg_maps(three_level_profile,enh_profile,only_one=True)2076L_to_m = {v : d['middle'].leg_dict[k] for k, v in middle_leg_map.items()2077if k in d['middle'].leg_dict}2078return d, L_to_m, d['middle']20792080@cached_method2081def enhanced_profiles_of_length(self,l,quiet=True):2082"""2083A little helper for generating all enhanced profiles in self of a given length.20842085Args:2086l (int): length (codim) of profiles to be generated.20872088Returns:2089tuple: tuple of enhanced profiles20902091EXAMPLES ::20922093sage: from admcycles.diffstrata import *2094sage: X=Stratum((4,))2095sage: len(X.lookup_list[2])2096172097sage: len(X.enhanced_profiles_of_length(2))20981920992100"""2101if not quiet:2102print('Generating enhanced profiles of length %r...' % l)2103sys.stdout.flush()2104if l >= len(self.lookup_list):2105return tuple()2106ep_list = []2107for c, p in enumerate(self.lookup_list[l]):2108if not quiet:2109print('Building all graphs in %r (%r/%r)...' % (p, c+1, len(self.lookup_list[l])))2110sys.stdout.flush()2111for i in range(len(self.lookup(p, quiet=True))): # quiet=False gives A LOT of output here...2112ep_list.append((p,i))2113return tuple(ep_list)21142115#########################################################2116#### Checks2117#########################################################21182119def check_dims(self,codim=None,quiet=False):2120"""2121Check if, for each non-horizontal level graph of codimension codim2122the dimensions of the levels add up to the dimension of the level graph2123(dim of stratum - codim).21242125If codim is ommitted, check the entire stratum.21262127EXAMPLES ::21282129sage: from admcycles.diffstrata import *2130sage: X=GeneralisedStratum([Signature((1,1))])2131sage: X.check_dims()2132Codimension 0 Graph 0: Level sums ok!2133Codimension 1 Graph 0: Level sums ok!2134Codimension 1 Graph 1: Level sums ok!2135Codimension 1 Graph 2: Level sums ok!2136Codimension 1 Graph 3: Level sums ok!2137Codimension 2 Graph 0: Level sums ok!2138Codimension 2 Graph 1: Level sums ok!2139Codimension 2 Graph 2: Level sums ok!2140Codimension 2 Graph 3: Level sums ok!2141Codimension 3 Graph 0: Level sums ok!2142True21432144sage: X=GeneralisedStratum([Signature((4,))])2145sage: X.check_dims(quiet=True)2146True21472148sage: X=GeneralisedStratum([Signature((10,0,-10))])2149sage: X.check_dims()2150Codimension 0 Graph 0: Level sums ok!2151Codimension 1 Graph 0: Level sums ok!2152Codimension 1 Graph 1: Level sums ok!2153Codimension 1 Graph 2: Level sums ok!2154Codimension 1 Graph 3: Level sums ok!2155Codimension 1 Graph 4: Level sums ok!2156Codimension 1 Graph 5: Level sums ok!2157Codimension 1 Graph 6: Level sums ok!2158Codimension 1 Graph 7: Level sums ok!2159Codimension 1 Graph 8: Level sums ok!2160Codimension 1 Graph 9: Level sums ok!2161Codimension 1 Graph 10: Level sums ok!2162Codimension 1 Graph 11: Level sums ok!2163True21642165sage: X=GeneralisedStratum([Signature((2,2,-2))])2166sage: X.check_dims(quiet=True) # long time (3 seconds)2167True2168"""2169return_value = True2170if codim is None:2171codims = range(self.dim())2172else:2173codims = [codim]2174for c in codims:2175for i,emb_g in enumerate(self.all_graphs[c]):2176g = emb_g.LG2177dimsum = 02178if not quiet:2179print("Codimension", c, "Graph", repr(i) + ":", end=" ")2180for l in range(g.numberoflevels()):2181L = g.stratum_from_level(l)2182if L.dim() == -1:2183if quiet:2184print("Codimension", c, "Graph", repr(i) + ":", end=" ")2185print("Error: Level", l, "is of dimension -1!")2186return_value = False2187dimsum += L.dim()2188if dimsum != self.dim() - c:2189if quiet:2190print("Codimension", c, "Graph", repr(i) + ":", end=" ")2191print("Error: Level dimensions add up to", dimsum, "not", self.dim() - c, "!")2192return_value = False2193else:2194if not quiet:2195print("Level sums ok!")2196return return_value21972198###########2199### Chern class calculation:2200def psi(self,leg):2201"""2202CURRENTLY ONLY ALLOWED FOR CONNECTED STRATA!!!!22032204The psi class on the open stratum at leg.22052206Args:2207leg (int): leg number (as index of signature, not point of stratum!!!)22082209Returns:2210ELGTautClass: Tautological class associated to psi.2211"""2212psi = self.additive_generator([tuple(),0],{leg:1})2213return psi.as_taut()22142215# @cached_method2216def taut_from_graph(self,profile,index=0):2217"""2218Tautological class from the graph with enhanced profile (profile, index).22192220Args:2221profile (iterable): profile2222index (int, optional): Index of profile. Defaults to 0.22232224Returns:2225ELGTautClass: Tautological class consisting just of this one graph.22262227EXAMPLES ::22282229"""2230return self.additive_generator((tuple(profile),index)).as_taut()22312232def ELGsum(self, L):2233"""2234Sum of tautological classes.22352236This is generally faster than += (i.e. sum()), because reduce is only called2237once at the end and not at every step.22382239Args:2240L (iterable): Iterable of ELGTautClasses on self.22412242Returns:2243ELGTautClass: Sum over input classes.2244"""2245new_psi_list = []2246for T in L:2247if T == 0:2248continue2249new_psi_list.extend(T.psi_list)2250return ELGTautClass(self, new_psi_list)22512252def pow(self, T, k, amb=None):2253"""2254Calculate T^k with ambient amb.22552256Args:2257T (ELGTautClass): Tautological class on self.2258k (int): positive integer.2259amb (tuple, optional): enhanced profile. Defaults to None.22602261Returns:2262ELGTautClass: T^k in CH(amb).2263"""2264if amb is None:2265amb = ((), 0)2266ONE = self.ONE2267else:2268ONE = self.taut_from_graph(*amb)2269prod = ONE2270for _ in range(k):2271prod = self.intersection(prod, T, amb)2272return prod22732274def exp(self,T,amb=None,quiet=True,prod=True,stop=None):2275"""2276(Formal) exp of a Tautological Class.22772278This is done (by default) by calculating exp of every AdditiveGenerator2279(which is cached) and calculating the product of these.22802281Alternatively, prod=False computes sums of powers of T.22822283Args:2284T (ELGTautClass): Tautological Class on X.22852286Returns:2287ELGTautClass: Tautological Class on X.2288"""2289N = self.dim()2290if amb is None:2291amb = ((), 0)2292if not prod:2293if not quiet:2294print("Calculating exp of %s..." % T)2295def _status(i):2296# primitive, but whatever2297if not quiet:2298print("Calculating power %r..." % i)2299return 12300return self.ELGsum([_status(i) * QQ(1)/QQ(factorial(i)) * self.pow(T,i,amb) for i in range(N+1)])2301# Calculate instead product of exp(AG):2302e = self.taut_from_graph(*amb)2303if not quiet:2304print("Calculating exp as product of %r factors..." % len(T.psi_list), end=' ')2305sys.stdout.flush()2306for c, AG in T.psi_list:2307f = AG.exp(c, amb, stop)2308if f == 0 or f == self.ZERO:2309return self.ZERO2310e = self.intersection(e, f, amb)2311if not quiet:2312print('Done!')2313return e23142315@cached_method2316def exp_bic(self, i):2317l = self.bics[i].ell2318AG = self.additive_generator(((i,),0))2319return AG.exp(l, amb=None) - self.ONE23202321def td_contrib(self,l,T,amb=None):2322"""2323(Formal) td^-1 contribution, i.e. (1-exp(-l*T))/T.23242325Args:2326l (int): weight2327T (ELGTautClass): Tautological class on self.23282329Returns:2330ELGTautClass: Tautological class on self.2331"""2332N = self.dim()2333if amb is None:2334amb = ((), 0)2335return self.ELGsum([QQ(-l)**k/QQ(factorial(k+1)) * self.pow(T,k,amb) for k in range(N+1)])23362337@property2338def xi(self):2339"""2340xi of self in terms of psi and BICs according to Sauvaget's formula.23412342Note that we first find an "optimal" leg.23432344Returns:2345ELGTautClass: psi class on smooth stratum + BIC contributions (all2346with multiplicities...)23472348EXAMPLES ::23492350sage: from admcycles.diffstrata import *2351sage: X=Stratum((2,))2352sage: print(X.xi) # 'unsafe' (order of summands might change) # doctest:+SKIP2353Tautological class on Stratum: (2,)2354with residue conditions: []2355<BLANKLINE>23563 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +2357-1 * Graph ((0,), 0) +2358-1 * Graph ((1,), 0) +2359<BLANKLINE>2360"""2361try:2362return self._xi2363except AttributeError:2364self._xi = self.xi_with_leg(quiet=True)2365return self._xi23662367@cached_method2368def xi_pow(self,n):2369"""2370Cached method for calculating powers of xi.23712372Args:2373n (int): non-negative integer (exponent)23742375Returns:2376ELGTautClass: xi^n2377"""2378if n == 0:2379return self.ONE2380return self.xi * self.xi_pow(n-1)23812382@cached_method2383def xi_with_leg(self,leg=None,quiet=True,with_leg=False):2384"""2385xi class of self expressed using Sauvaget's relation (with optionally a choice of leg)23862387Args:2388leg (tuple, optional): leg on self, i.e. tuple (i,j) for the j-th element2389of the signature of the i-th component. Defaults to None. In this case,2390an optimal leg is chosen.2391quiet (bool, optional): No output. Defaults to False.2392with_leg (bool, optional): Return choice of leg. Defaults to False.23932394Returns:2395ELGTautClass: xi in terms of psi and bics according to Sauvaget.2396(ELGTautClass, tuple): if with_leg=True, where tuple is the corresponding2397leg on the level i.e. (component, signature index) used.23982399EXAMPLES ::24002401In the stratum (2,-2) the pole is chosen by default (there is no 'error term'):24022403sage: from admcycles.diffstrata import *2404sage: X=Stratum((2,-2))2405sage: print(X.xi)2406Tautological class on Stratum: (2, -2)2407with residue conditions: []2408<BLANKLINE>2409-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +2410<BLANKLINE>2411sage: print(X.xi_with_leg(leg=(0,1)))2412Tautological class on Stratum: (2, -2)2413with residue conditions: []2414<BLANKLINE>2415-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +2416<BLANKLINE>24172418We can specify the zero instead and pick up the extra divisor:24192420sage: print(X.xi_with_leg(leg=(0,0))) # 'unsafe' (order of summands might change) # doctest:+SKIP2421Tautological class on Stratum: (2, -2)2422with residue conditions: []2423<BLANKLINE>24243 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +2425-1 * Graph ((0,), 0) +2426<BLANKLINE>2427"""2428if not quiet:2429print("Applying Sauvaget's relation to express xi for %r..." % self)2430if leg is None:2431# choose a "good" leg:2432l, k, bot_bic_list = self._choose_leg_for_sauvaget_relation(quiet)2433else:2434l = leg2435k = self._sig_list[l[0]].sig[l[1]]2436bot_bic_list = self.bics_with_leg_on_bottom(l)2437# find internal leg number on smooth graph correspoding to l:2438G = self.lookup_graph(tuple())2439internal_leg = G.dmp_inv[l] # leg number on graph2440xi = (k+1) * self.psi(internal_leg)2441add_gens = [self.additive_generator([(b,),0]) for b in bot_bic_list]2442self._xi = xi + ELGTautClass(self, [(-self.bics[bot_bic_list[i]].ell, AG) \2443for i, AG in enumerate(add_gens)])2444# self._xi = xi + sum([QQ(1)/QQ(AG.stack_factor)*AG.as_taut() \2445# for i, AG in enumerate(add_gens)])2446if with_leg:2447return (self._xi,l)2448else:2449return self._xi24502451def _choose_leg_for_sauvaget_relation(self,quiet=True):2452"""2453Choose the best leg for Sauvaget's relation, i.e. the one that appears on bottom2454level for the fewest BICs.24552456Returns:2457tuple: tuple (leg, order, bic_list) where:2458* leg (tuple), as a tuple (number of conn. comp., index of the signature tuple),2459* order (int) the order at leg, and2460* bic_list (list of int) is a list of indices of self.bics where leg2461is on bottom level.24622463EXAMPLES ::24642465sage: from admcycles.diffstrata import *2466sage: X=Stratum((2,-2))2467sage: X._choose_leg_for_sauvaget_relation()2468((0, 1), -2, [])24692470In the minimal stratum, we always find all BICS:24712472sage: X=Stratum((2,))2473sage: X._choose_leg_for_sauvaget_relation()2474((0, 0), 2, [0, 1])2475"""2476best_case = len(self.bics)2477best_leg = -12478# points of the stratum are best accessed through the embedding of the smooth graph:2479# (we sort for better testing...)2480leg_list = sorted(list(self.smooth_LG.dmp_inv.keys()), key=lambda x:x[1])2481for l in leg_list:2482bot_list = self.bics_with_leg_on_bottom(l)2483# none is best we can do:2484if not bot_list:2485order = self._sig_list[l[0]].sig[l[1]]2486if not quiet:2487print("Choosing leg %r (of order %r) because it never appears on bottom level."2488% (l, order))2489return (l,order,[])2490on_bottom = len(bot_list)2491if on_bottom <= best_case:2492best_case = on_bottom2493best_leg = l2494best_bot_list = bot_list[:] # copy!2495assert best_leg != -1, "No best leg found for %r!" % self2496order = self._sig_list[best_leg[0]].sig[best_leg[1]]2497if not quiet:2498print("Choosing leg %r (of order %r), because it only appears on bottom %r out of %r times."\2499% (best_leg, order, best_case, len(self.bics)))2500return (best_leg, order, best_bot_list)25012502def bics_with_leg_on_bottom(self,l):2503"""2504A list of BICs where l is on bottom level.25052506Args:2507l (tuple): leg on self (i.e. (i,j) for the j-th element of the signature2508of the i-th component)25092510Returns:2511list: list of indices self.bics25122513EXAMPLES ::25142515sage: from admcycles.diffstrata import *2516sage: X=GeneralisedStratum([Signature((2,))])2517sage: X.bics_with_leg_on_bottom((0,0))2518[0, 1]2519"""2520bot_list = []2521# the corresponding point on each EmbeddedLevelGraph is leg2522for i, B in enumerate(self.bics):2523# reminder: l is leg on stratum, i.e. (i,j)2524# dmp_inv maps this to a leg on a graph (integer)2525leg = B.dmp_inv[l]2526leg_level = B.dlevels[B.LG.levelofleg(leg)]2527assert leg_level in [0,-1], "Leg %r of BIC %r is not on level 0 or -1!"\2528% (leg, B)2529if leg_level == -1:2530bot_list.append(i)2531return bot_list25322533@cached_method2534def xi_at_level(self,l,enh_profile,leg=None,quiet=True):2535"""2536Pullback of xi on level l to enh_profile.25372538This corresponds to xi_Gamma^[i] in the paper.25392540Args:2541l (int): level number (0,...,codim)2542enh_profile (tuple): enhanced profile2543leg (int, optional): leg (as a leg of enh_profile!!!), to be used2544in Sauvaget's relation. Defaults to None, i.e. optimal choice.25452546Raises:2547RuntimeError: raised if classes produced by xi on the level have2548unexpected codimension.2549ValueError: if the leg provided is not found on the level.25502551Returns:2552ELGTautClass: tautological class consisting of psi classes on2553enh_profile and graphs with oner more level.25542555EXAMPLES ::25562557Compare multiplication with xi to xi_at_level (for top-degree):25582559sage: from admcycles.diffstrata import *2560sage: X=Stratum((2,-2,0))2561sage: assert all(X.xi_at_level(0, ((i,),0)) == X.xi*X.taut_from_graph((i,)) for i in range(len(X.bics)))25622563"""2564if enh_profile == ((),0):2565assert l == 02566if leg:2567level_leg = self.smooth_LG.dmp[leg]2568return self.xi_with_leg(level_leg)2569return self.xi2570G = self.lookup_graph(*enh_profile)2571GAG = self.additive_generator(enh_profile)2572# we need to use splitting info instead of direct level extraction,2573# because the embeddings might differ by an automorphism!2574d, leg_dict, L = self.splitting_info_at_level(enh_profile,l)2575inv_leg_dict = {v : k for k, v in leg_dict.items()}2576assert set(leg_dict.values()) == set(L.leg_dict.values())2577if leg is None:2578l_xi, level_leg = L.xi_with_leg(with_leg=True,quiet=quiet)2579else:2580if not (leg in leg_dict):2581raise ValueError('Leg %r is not on level %r of %r!' % (leg, l, enh_profile))2582level_leg = leg_dict[leg]2583l_xi = L.xi_with_leg(level_leg,quiet=quiet)2584taut_list = []2585if l_xi == 0:2586return self.ZERO2587for c, AG in l_xi.psi_list:2588if AG.codim == 0:2589# psi class on L:2590new_leg_dict = {}2591for AGleg in AG.leg_dict:2592leg_on_G = inv_leg_dict[L.smooth_LG.dmp[AGleg]]2593new_leg_dict[leg_on_G] = AG.leg_dict[AGleg]2594next_taut = (c, self.additive_generator(enh_profile,leg_dict=new_leg_dict))2595elif AG.codim == 1:2596coeff, glued_AG = self.glue_AG_at_level(AG,enh_profile,l)2597next_taut = (c*coeff,glued_AG)2598else:2599raise RuntimeError("Classes in xi should all be of codim 0 or 1! %s" % l_xi)2600taut_list.append(next_taut)2601return ELGTautClass(self,taut_list)26022603@cached_method2604def glue_AG_at_level(self,AG,enh_profile,l):2605"""2606Glue an AdditiveGenerator into level l of enh_profile.26072608Note that AG must be an AdditiveGenerator on the level obtained via2609self.splitting_info_at_level!26102611Currently this is only implemented for graphs (and only really tested2612for BICs!!!)26132614TODO: Test for AGs that are not BICs and psi classes.26152616Args:2617AG (AdditiveGenerator): AdditiveGenerator on level2618enh_profile (tuple): enhanced profile of self.2619l (int): level number of enh_profile.26202621Raises:2622RuntimeError: raised if the new profile is empty.26232624Returns:2625tuple: A tuple consisting of the stackfactor (QQ) and the2626AdditiveGenerator of the glued graph.2627"""2628# TODO: Check if longer profiles work + psis!2629#2630# First, we figure out the profile of the new graph of self.2631# For this, we must translate the profile (inside L) of the AG2632# into an extended profile (of self) as a degeneration of enh_profile:2633profile, _comp = enh_profile2634AGprofile, AGcomp = AG.enh_profile2635# We start by deciding where something must be inserted into enh_profile:2636#2637# We observe that level l is either:2638# * B^top of the first BIC in profile (level 0),2639# * B^bot of the last BIC in profile (lowest level), or2640# * the middle of the 3-level graph (profile[l-1],profile[l]).2641#2642# There is also the "degenerate case" of an empty profile that2643# we should exclude first:2644if len(profile) == 0:2645assert l == 02646# level stratum == stratum2647# stack_factor = QQ(AG.stack_factor)2648return (1, self.additive_generator((AGprofile,AGcomp)))2649elif l == 0:2650new_bics = [self.DG.top_to_bic(profile[l])[bic_index] for bic_index in AGprofile]2651elif l == len(profile):2652new_bics = [self.DG.bot_to_bic(profile[l-1])[bic_index] for bic_index in AGprofile]2653else: # we are in the middle of the corresponding 3-level graph:2654three_level_profile, enhancement = self.three_level_profile_for_level(enh_profile,l)2655new_bics = [self.DG.middle_to_bic((three_level_profile,enhancement))[bic_index]2656for bic_index in AGprofile]2657p = list(profile)2658p = tuple(p[:l] + new_bics + p[l:])2659# Now we know the profile, we have to figure out, which component2660# we're on.2661# For this, we split the enh_profile apart, replace one part by the BIC and2662# and glue it back together again.2663comp_list = []2664assert len(self.lookup(p)) > 0, "Error: Glued into empty profile %r" % p2665# The splitting information and the level in 'standard form' (i.e. one2666# of the three above possibilities), is given by splitting_info_at_level:2667d, leg_dict, L = self.splitting_info_at_level(enh_profile,l)2668if not AG._X is L:2669print("Warning! Additive Generator should live on level %r of %r! I hope you know what you're doing...." % (l,enh_profile))2670# We first build the "big" graph, i.e. glue in the AG.2671# For this, we have to distinguish again, if we're gluing into the middle2672# (two cuts) or at one end of the profile (1 cut):2673if l == 0:2674assert d['top'].X is L2675# we glue into top:2676d['top'] = d['top'].X.lookup_graph(*AG.enh_profile)2677elif l == len(profile):2678assert d['bottom'].X is L2679# we glue into bottom:2680d['bottom'] = d['bottom'].X.lookup_graph(*AG.enh_profile)2681else:2682assert d['middle'] is L2683# we glue into middle:2684d['middle'] = d['middle'].lookup_graph(*AG.enh_profile)2685glued_graph = admcycles.diffstrata.stratatautring.clutch(**d)2686# Now we check the components of p for glued_graph:2687for i, H in enumerate(self.lookup(p)):2688if glued_graph.is_isomorphic(H):2689comp_list.append(i)2690if len(comp_list) != 1:2691raise RuntimeError("%r is not a unique degeneration of %r! %r" % (p,enh_profile,comp_list))2692i = comp_list[0]2693glued_AG = self.additive_generator((p,i))2694GAG = self.additive_generator(enh_profile)2695stack_factor = 12696for i in range(len(AGprofile)):2697stack_factor *= QQ(self.bics[new_bics[i]].ell) / QQ(L.bics[AGprofile[i]].ell)2698stack_factor *= QQ(len(glued_graph.automorphisms)) / QQ(len(AG._G.automorphisms)*len(GAG._G.automorphisms))2699return (stack_factor, glued_AG)27002701def calL(self, enh_profile=None, l=0):2702"""2703The error term of the normal bundle on level l of enh_profile * -ll2704(pulled back to enh_profile)27052706Args:2707enh_profile (tuple, optional): enhanced profile. Defaults to None.2708l (int, optional): level. Defaults to 0.27092710Returns:2711ELGTautClass: Tautological class on self2712"""2713result = []2714if enh_profile is None or enh_profile == ((), 0):2715for i, B in enumerate(self.bics):2716ll = self.bics[i].ell2717result.append(ll*self.taut_from_graph((i,)))2718else:2719# Morally, L = G.level(squished_level)2720# but we have to use splitting_info_at_level to glue in safely!2721d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)2722for i, B in enumerate(L.bics):2723BAG = L.additive_generator(((i,),0))2724sf, glued_AG = self.glue_AG_at_level(BAG, enh_profile, l)2725coeff = QQ(sf*B.ell)2726result.append(coeff * glued_AG.as_taut())2727if not result:2728return self.ZERO2729return self.ELGsum(result)27302731################################################################2732############ SEC 9 FORMULAS ####################################2733################################################################2734## The following formulas check various identities used in ##2735## and around sec 9 of the paper. They also serve as examples ##2736## for the methods introduced above. ##2737################################################################27382739@property2740def c1_E(self):2741"""2742The first chern class of Omega^1(log) (Thm 1.1).27432744Returns:2745ELGTautClass: c_1(E) according to Thm 1.1.27462747EXAMPLES ::27482749"""2750N = self.dim() + 12751c1E = [N*self.xi]2752for i, B in enumerate(self.bics):2753Ntop = B.top.dim() + 12754l = B.ell2755c1E.append(((N-Ntop)*l)*self.taut_from_graph((i,)))2756return self.ELGsum(c1E)27572758@property2759def c2_E(self):2760"""2761A direct formula for the second Chern class.27622763Returns:2764ELGTautClass: c_2 of the Tangent bundle of self.2765"""2766N = QQ(self.dim() + 1)2767c2E = [N*(N-1)/QQ(2) * (self.xi_pow(2))]2768for i, B in enumerate(self.bics):2769Ntop = B.top.dim() + 12770Nbot = B.bot.dim() + 12771xitop = self.xi_at_level(0, ((i,),0))2772xibot = self.xi_at_level(1, ((i,),0))2773l = QQ(B.ell)2774c2E.append(l/2 * ((N*(N-1) - Ntop*(Ntop-1))*xitop +2775((N-Ntop)**2 + Ntop - N)*xibot))2776for ep in self.enhanced_profiles_of_length(2):2777p, _ = ep2778delta0 = self.bics[p[0]]2779delta1 = self.bics[p[1]]2780Nd0 = delta0.top.dim() + 12781Nd1 = delta1.top.dim() + 12782ld0 = QQ(delta0.ell)2783ld1 = QQ(delta1.ell)2784factor = QQ(1)/QQ(2) * ld0 * ld1 * (N*(N-2*Nd0)-Nd1*(Nd1-2*Nd0)-N+Nd1)2785c2E.append(factor * self.taut_from_graph(*ep))2786return self.ELGsum(c2E)27872788@cached_method2789def ch1_pow(self, n):2790"""2791A direct formula for powers of ch_127922793Args:2794n (int): exponent27952796Returns:2797ELGTautClass: ch_1(T)^n2798"""2799N = QQ(self.dim() + 1)2800chpow = [QQ(N**n)/QQ(factorial(n)) * self.xi_pow(n)]2801for L in range(1,n+1):2802summand = []2803for ep in self.enhanced_profiles_of_length(L):2804p, _ = ep2805delta = [self.bics[b] for b in p]2806ld = [B.ell for B in delta]2807Nd = [B.top.dim() + 1 for B in delta]2808exi = self.exp(N*self.xi_at_level(0,ep), amb=ep)2809factor = 12810td_prod = self.taut_from_graph(*ep)2811for i in range(L):2812factor *= (N - Nd[i])*ld[i]2813td_prod = self.intersection(td_prod,2814self.td_contrib(-ld[i]*(N-Nd[i]),2815self.cnb(ep, ep, self.squish(ep, i)), ep),2816ep)2817prod = self.intersection(exi, td_prod, ep)2818summand.append(factor * prod.degree(n))2819chpow.append(self.ELGsum(summand))2820return factorial(n) * self.ELGsum(chpow)28212822@property2823def ch2_E(self):2824"""2825A direct formula for ch_2.28262827Returns:2828ELGTautClass: ch_22829"""2830N = QQ(self.dim() + 1)2831ch2E = [N/QQ(2) * (self.xi_pow(2))]2832for i, B in enumerate(self.bics):2833Ntop = B.top.dim() + 12834Nbot = B.bot.dim() + 12835xitop = self.xi_at_level(0, ((i,),0))2836xibot = self.xi_at_level(1, ((i,),0))2837l = QQ(B.ell)2838ch2E.append(l/2 * ((N - Ntop)*(xitop + xibot)))2839for ep in self.enhanced_profiles_of_length(2):2840p, _ = ep2841delta0 = self.bics[p[0]]2842delta1 = self.bics[p[1]]2843Nd0 = delta0.top.dim() + 12844Nd1 = delta1.top.dim() + 12845ld0 = QQ(delta0.ell)2846ld1 = QQ(delta1.ell)2847factor = QQ(1)/QQ(2) * ld0 * ld1 * (N - Nd1)2848ch2E.append(factor * self.taut_from_graph(*ep))2849return self.ELGsum(ch2E)28502851def ch_E_alt(self, d):2852"""2853A formula for the Chern character.28542855Args:2856d (int): cut-off degree28572858Returns:2859ELGTautClass: sum of ch_0 to ch_d.2860"""2861N = QQ(self.dim() + 1)2862ch_E = [N/QQ(factorial(d)) * self.xi_pow(d)]2863for L in range(1, d+1):2864summand = []2865for ep in self.enhanced_profiles_of_length(L):2866p, _ = ep2867ld = [self.bics[b].ell for b in p]2868Nd = self.bics[p[-1]].top.dim() + 12869ld_prod = 12870for l in ld:2871ld_prod *= l2872factor = ld_prod * (N - Nd)2873td_prod = self.ONE2874for i in range(L):2875td_prod = self.intersection(td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)2876inner_sum = []2877for j in range(d-L+1):2878pr = self.intersection(self.pow(self.xi_at_level(0, ep), j, ep), td_prod.degree(d-j), ep)2879inner_sum.append(QQ(1)/QQ(factorial(j)) * pr)2880summand.append(factor * self.ELGsum(inner_sum))2881ch_E.append(self.ELGsum(summand))2882return self.ELGsum(ch_E)28832884@cached_method2885def exp_xi(self, quiet=True):2886"""2887Calculate exp(xi) using that no powers higher than 2g appear for connected2888holomorphic strata.28892890Args:2891quiet (bool, optional): No output. Defaults to True.28922893Returns:2894ELGTautClass: exp(xi)2895"""2896if not self._polelist and len(self._g) == 1:2897stop = 2*self._g[0]2898else:2899stop = None2900if not quiet:2901if stop:2902stop_str = stop - 12903else:2904stop_str = stop2905print('Stoping exp(xi) at degree %r' % stop_str)2906return self.exp(self.xi, quiet=quiet, stop=stop)29072908def xi_at_level_pow(self, level, enh_profile, exponent):2909"""2910Calculate powers of xi_at_level (using ambient enh_profile).29112912Note that when working with xi_at_level on enh_profile, multiplication2913should always take place in CH(enh_profile), i.e. using intersection2914instead of *. This is simplified for powers by this method.29152916Moreover, by Sauvaget, xi^n = 0 for n >= 2g for connected holomorphic2917strata, so we check this before calculating.29182919Args:2920level (int): level of enh_profile.2921enh_profile (tuple): enhanced profile of self.2922exponent (int): exponent29232924Returns:2925ELGTautClass: Pushforward of (xi_{enh_profile}^[l])^n to self.2926"""2927G = self.lookup_graph(*enh_profile)2928L = G.level(level)2929if not L._polelist and len(L._g) == 1:2930if exponent >= 2*L._g[0]:2931return self.ZERO2932if enh_profile == ((), 0):2933assert level == 02934return self.xi_pow(exponent)2935# ambient!2936power = self.taut_from_graph(*enh_profile)2937# maybe consecutive squaring is better? Seems that it isn't :/2938# xi = self.xi_at_level(level, enh_profile)2939# def _rec(x, n):2940# if n == 0:2941# return self.taut_from_graph(*enh_profile)2942# if n == 1:2943# return x2944# if n % 2 == 0:2945# return _rec(self.intersection(x, x, enh_profile), n // 2)2946# return self.intersection(x, _rec(self.intersection(x, x, enh_profile), (n - 1) // 2), enh_profile)2947# return _rec(xi, exponent)2948xi = self.xi_at_level(level, enh_profile)2949for _ in range(exponent):2950power = self.intersection(power, xi, enh_profile)2951return power29522953@cached_method2954def exp_L(self, quiet=True):2955"""2956exp(calL)29572958Args:2959quiet (bool, optional): No output. Defaults to True.29602961Returns:2962ELGTautClass: exp(calL)2963"""2964return self.exp(self.calL(), quiet=quiet)29652966@property2967def P_B(self):2968"""2969The twisted Chern character of self, see sec 9 of the paper.29702971Returns:2972ELGTautClass: class of P_B2973"""2974# Prop. 9.22975N = QQ(self.dim() + 1)2976PB = [N*self.exp_xi() + (-1)*self.ONE]2977for L in range(1,N):2978inner = []2979for enh_profile in self.enhanced_profiles_of_length(L):2980p, _ = enh_profile2981B = self.bics[p[0]]2982Ntop = B.top.dim() + 12983summand = (-1)**L * (Ntop*self.exp_xi() + (-1)*self.ONE)2984prod_list = []2985for i in range(L):2986ll = self.bics[p[i]].ell2987squish = self.squish(enh_profile, i)2988td_NB = ll * self.td_contrib(ll, self.cnb(enh_profile, enh_profile, squish), enh_profile)2989prod_list.append(td_NB)2990if prod_list:2991prod = prod_list[0]2992for f in prod_list[1:]:2993# multiply with ambient Gamma (=enh_profile)!2994prod = self.intersection(prod, f, enh_profile)2995const = prod.degree(0)2996prod += (-1) * const2997summand *= (prod + const*self.taut_from_graph(*enh_profile))2998inner.append(summand)2999PB.append(self.ELGsum(inner))3000return self.ELGsum(PB)30013002def charToPol(self, ch, upto=None, quiet=True):3003"""3004Newton's identity to recursively translate the Chern character into the3005Chern polynomial.30063007Args:3008ch (ELGTautClass): Chern character3009upto (int, optional): Calculate polynomial only up to this degree. Defaults to None (full polynomial).3010quiet (bool, optional): No output. Defaults to True.30113012Returns:3013list: Chern polynomial as list of ELGTautClasses (indexed by degree)3014"""3015if not quiet:3016print('Starting charToPol...')3017C = ch.list_by_degree()3018# throw out factorials:3019p = [factorial(k)*c for k, c in enumerate(C)]3020# calculate recursively using Newton's identity:3021E = [self.ONE]3022if upto is None:3023upto = self.dim()3024for k in range(1, upto + 1):3025if not quiet:3026print('Calculating c_%r...' % k)3027ek = []3028for i in range(1, k+1):3029ek.append((-1)**(i-1) * E[k-i]*p[i])3030E.append(QQ(1)/QQ(k) * self.ELGsum(ek))3031return E30323033def top_chern_class_alt(self, quiet=True):3034"""3035Top chern class from Chern polynomial.30363037Args:3038quiet (bool, optional): No output. Defaults to True.30393040Returns:3041ELGTautClass: c_top of the tangent bundle of self.3042"""3043ch = self.ch_E_fast(quiet=quiet).list_by_degree()3044top_c = []3045N = self.dim()3046for p in partitions(N):3047l = sum(p.values())3048factor = (-1)**(N-l)3049# for r, n in enumerate(p.values()):3050# factor *= QQ(factorial(r)**n)/QQ(factorial(n))3051ch_prod = self.ONE3052for i, n in p.items():3053factor *= QQ(factorial(i-1)**n)/QQ(factorial(n))3054if i == 1:3055ch_prod *= self.ch1_pow(n)3056else:3057ch_prod *= ch[i]**n3058top_c.append(factor*ch_prod)3059return self.ELGsum(top_c)30603061def top_chern_class_direct(self, quiet=True):3062"""3063A direct formula for the top Chern class using only xi_at_level.30643065Args:3066quiet (bool, optional): No output. Defaults to True.30673068Returns:3069ELGTautClass: c_top of the Tangent bundle of self.3070"""3071N = self.dim()3072top_c = []3073for L in range(N+1):3074if not quiet:3075print('Going through %r profiles of length %r...' % (len(self.enhanced_profiles_of_length(L)), L))3076summand = []3077for ep in self.enhanced_profiles_of_length(L):3078p, _ = ep3079ld = [self.bics[b].ell for b in p]3080ld_prod = 13081for l in ld:3082ld_prod *= l3083inner = []3084for K in WeightedIntegerVectors(N-L, [1]*(L+1)):3085xi_prod = self.taut_from_graph(*ep)3086for i, k in enumerate(K):3087xi_prod = self.intersection(xi_prod, self.xi_at_level_pow(i, ep, k), ep)3088inner.append((K[0] + 1) * xi_prod)3089summand.append(ld_prod * self.ELGsum(inner))3090top_c.append(self.ELGsum(summand))3091return self.ELGsum(top_c)30923093def top_xi_at_level_comparison(self, ep, quiet=False):3094"""3095Comparison of level-wise computation vs xi_at_level.30963097Args:3098ep (tuple): enhanced profile3099quiet (bool, optional): no output. Defaults to False.31003101Returns:3102bool: Should always be True.31033104EXAMPLES ::31053106sage: from admcycles.diffstrata import *3107sage: X=Stratum((2,))3108sage: assert all(X.top_xi_at_level_comparison(ep, quiet=True) for l in range(len(X.lookup_list)) for ep in X.enhanced_profiles_of_length(l))3109"""3110N = self.dim()3111p, _ = ep3112L = len(p)3113ld = [self.bics[b].ell for b in p]3114Nvec = [self.bics[b].top.dim() + 1 for b in p]3115Nvec.append(N+1)3116ld_prod = 13117for l in ld:3118ld_prod *= l3119inner = []3120xi_prod = self.xi_at_level_pow(0, ep, Nvec[0]-1)3121for i in range(1,L+1):3122xi_prod = self.intersection(xi_prod, self.xi_at_level_pow(i, ep, Nvec[i]-Nvec[i-1]-1), ep)3123xi_at_level_prod = (Nvec[0] * xi_prod).evaluate(quiet=True)3124if not quiet:3125print("Product of xis at levels: %r" % xi_at_level_prod)3126G = self.lookup_graph(*ep)3127AG = self.additive_generator(ep)3128top_xi_at_level = [(G.level(i).xi_at_level_pow(0,((),0),G.level(i).dim())).evaluate(quiet=True) for i in range(L+1)]3129if not quiet:3130print(top_xi_at_level)3131prod = Nvec[0]3132for x in top_xi_at_level:3133prod *= x3134tot_prod = AG.stack_factor*prod3135if not quiet:3136print("Stack factor: %r" % AG.stack_factor)3137print("Product: %r" % prod)3138print("Total product: %r" % tot_prod)3139return tot_prod == xi_at_level_prod31403141def top_xi_at_level(self, ep, level, quiet=True):3142"""3143Evaluate the top xi power on a level.31443145Note that this is _not_ computed on self but on the GeneralisedStratum3146corresponding to level l of ep (the result is a number!).31473148Moreover, all results are cached and the cache is synchronised with3149the ``XI_TOPS`` cache.31503151The key for the cache is L.dict_key (where L is the LevelStratum).31523153Args:3154ep (tuple): enhanced profile3155level (int): level number of ep3156quiet (bool, optional): No output. Defaults to True.31573158Returns:3159QQ: integral of the top xi power against level l of ep.31603161EXAMPLES ::31623163sage: from admcycles.diffstrata import *3164sage: X=Stratum((2,))3165sage: X.top_xi_at_level(((),0), 0)3166-1/6403167"""3168G = self.lookup_graph(*ep)3169L = G.level(level)3170key = L.dict_key()3171cache = TOP_XIS3172if key not in cache:3173N = L.dim()3174if not quiet:3175print('(calc)', end=' ')3176sys.stdout.flush()3177top_xi = L.xi_at_level_pow(0, ((),0), N)3178cache[key] = top_xi.evaluate(quiet=True)3179else:3180if not quiet:3181print ('(cache)', end=' ')3182sys.stdout.flush()3183if not quiet:3184print(cache[key], end=' ')3185sys.stdout.flush()3186assert QQ(cache[key]) == cache[key]3187return cache[key]31883189def euler_char_immediate_evaluation(self, quiet=True):3190"""3191Calculate the (Orbifold) Euler characteristic of self by evaluating top xi3192powers on levels.31933194This is (by far) the fastest way of computing Euler characteristics.31953196Note that only combinatorial information about the degeneration graph3197of self is used (enhanced_profiles_of_length(L)) and top_xi_at_level3198the values of which are cached and synched with ``TOP_XIS`` cache.31993200Args:3201quiet (bool, optional): No output. Defaults to True.32023203Returns:3204QQ: (Orbifold) Euler characteristic of self.32053206EXAMPLES ::32073208sage: from admcycles.diffstrata import *3209sage: X=Stratum((2,))3210sage: X.euler_char_immediate_evaluation()3211-1/403212"""3213N = self.dim()3214ec = 03215for L in range(N+1):3216if not quiet:3217total=len(self.enhanced_profiles_of_length(L, quiet=False))3218print('Going through %r profiles of length %r...' % (total, L))3219for i, ep in enumerate(self.enhanced_profiles_of_length(L)):3220if not quiet:3221print('%r / %r, %r:' % (i+1, total, ep), end=' ')3222sys.stdout.flush()3223p, _ = ep3224ld = [self.bics[b].ell for b in p]3225if p:3226NGammaTop = self.bics[p[0]].top.dim() + 13227else:3228NGammaTop = N + 13229ld_prod = 13230for l in ld:3231ld_prod *= l3232AG = self.additive_generator(ep)3233prod = ld_prod * NGammaTop * AG.stack_factor3234if not quiet:3235print("Calculating xi at", end=' ')3236sys.stdout.flush()3237for i in range(L+1):3238if not quiet:3239print('level %r' % i, end=' ')3240sys.stdout.flush()3241prod *= self.top_xi_at_level(ep, i, quiet=quiet)3242if prod == 0:3243if not quiet:3244print("Product 0.", end=' ')3245sys.stdout.flush()3246break3247if not quiet:3248print('Done.')3249sys.stdout.flush()3250ec += prod3251return (-1)**N * ec32523253def euler_characteristic(self):3254"""3255Calculate the (Orbifold) Euler characteristic of self by evaluating top xi3256powers on levels. See also euler_char_immediate_evaluation.32573258Returns:3259QQ: (Orbifold) Euler characteristic of self.32603261EXAMPLES ::32623263sage: from admcycles.diffstrata import *3264sage: X=Stratum((2,))3265sage: X.euler_characteristic()3266-1/403267"""3268return self.euler_char_immediate_evaluation()32693270def euler_char(self,quiet=True, alg='direct'):3271"""3272Calculate the (Orbifold) Euler characteristic of self by computing the top3273Chern class and evaluating this.32743275Note that this is significantly slower than using self.euler_characteristic!32763277The optional keyword argument alg determines how the top Chern class3278is computed and can be either:3279* direct (default): using top_chern_class_direct3280* alt: using top_chern_class_alt3281* other: using top_chern_class32823283Args:3284quiet (bool, optional): no ouput. Defaults to True.3285alg (str, optional): algorithm (see above). Defaults to 'direct'.32863287Returns:3288QQ: (Orbifold) Euler characteristic of self.32893290EXAMPLES ::32913292sage: from admcycles.diffstrata import *3293sage: X=Stratum((2,))3294sage: X.euler_char()3295-1/403296sage: X.euler_char(alg='alt')3297-1/403298sage: X.euler_char(alg='other')3299-1/403300"""3301if alg == 'direct':3302tcc = self.top_chern_class_direct(quiet=quiet)3303elif alg == 'alt':3304tcc = self.top_chern_class_alt(quiet=quiet)3305else:3306tcc = self.top_chern_class(quiet=quiet, alg=alg)3307if not quiet:3308print('Evaluating...')3309return (-1)**self.dim() * tcc.evaluate(quiet=True)33103311def top_chern_class(self, inside=True, prod=True, top=False, quiet=True, alg='fast'):3312"""3313Compute the top Chern class from the Chern polynomial via the Chern character.33143315This uses chern_poly.33163317Args:3318inside (bool, optional): passed to chern_poly. Defaults to True.3319prod (bool, optional): passed to chern_poly. Defaults to True.3320top (bool, optional): passed to chern_poly. Defaults to False.3321quiet (bool, optional): passed to chern_poly. Defaults to True.3322alg (str, optional): passed to chern_poly. Defaults to 'fast'.33233324Returns:3325ELGTautClass: c_top(T) of self.3326"""3327return self.chern_poly(inside=inside, prod=prod, top=top, quiet=quiet, alg=alg)[-1]33283329def chern_poly(self, inside=True, prod=True, top=False, quiet=True, alg='fast', upto=None):3330"""3331The Chern polynomial calculated from the Chern character.33323333The optional keyword argument alg determines how the Chern character3334is computed and can be either:3335* fast (default): use ch_E_fast3336* bic_prod: use ch_E_prod3337* other: use ch_E33383339Args:3340inside (bool, optional): passed to ch_E. Defaults to True.3341prod (bool, optional): passed to ch_E. Defaults to True.3342top (bool, optional): passed to ch_E. Defaults to False.3343quiet (bool, optional): no output. Defaults to True.3344alg (str, optional): algorithm used (see above). Defaults to 'fast'.3345upto (int, optional): highest degree of polynomial to calculate. Defaults to None (i.e. dim so the whole polynomial).33463347Returns:3348list: Chern polynomial as list of ELGTautClasses (indexed by degree)3349"""3350if alg == 'bic_prod':3351ch = self.ch_E_prod(quiet=quiet)3352elif alg == 'fast':3353ch = self.ch_E_fast(quiet=quiet)3354else:3355ch = self.ch_E(inside=inside, prod=prod, top=top, quiet=quiet)3356return self.charToPol(ch, quiet=quiet, upto=upto)33573358def chern_class(self, n, quiet=True):3359"""3360A direct formula for the n-th Chern class of the tangent bundle of self.33613362Args:3363n (int): degree3364quiet (bool, optional): No output. Defaults to True.33653366Returns:3367ELGTautClass: c_n(T) of self.3368"""3369N = self.dim() + 13370c_n = []3371for L in range(N):3372if not quiet:3373print('Going through %r profiles of length %r...' % (len(self.enhanced_profiles_of_length(L)), L))3374summand = []3375for ep in self.enhanced_profiles_of_length(L):3376if not quiet:3377print("Profile: %r" % (ep,), end=' ')3378p, _ = ep3379delta = [self.bics[b] for b in p]3380ld = [B.ell for B in delta]3381Nd = [B.top.dim() + 1 for B in delta]3382ld_prod = 13383for l in ld:3384ld_prod *= l3385inner = []3386for K in WeightedIntegerVectors(n-L, [1]*(L+1)):3387if not quiet:3388print('xi coefficient: k_0:', K[0], end=' ')3389print('N-L-sum:', N-L-sum(K[1:]), end=' ')3390print('Binomial:', binomial(N-L-sum(K[1:]), K[0]))3391factor = binomial(N-L-sum(K[1:]), K[0])3392prod = self.xi_at_level_pow(0, ep, K[0])3393for i, k in list(enumerate(K))[1:]:3394if not quiet:3395print('k_%r: %r' % (i, k), end=' ')3396print('r_Gamma,i:', (N-Nd[i-1]), end=' ')3397print('L-i: %r, sum: %r' % (L-i, sum(K[i+1:])), end=' ')3398print('Binomial:', binomial((N-Nd[i-1]) - (L-i) - sum(K[i+1:]), k+1))3399factor *= binomial((N-Nd[i-1]) - (L-i) - sum(K[i+1:]), k+1)3400squish = self.squish(ep, i-1)3401X_pow = self.pow(ld[i-1] * self.cnb(ep, ep, squish), k, ep)3402prod = self.intersection(prod, X_pow, ep)3403inner.append(factor * prod)3404summand.append(ld_prod * self.ELGsum(inner))3405c_n.append(self.ELGsum(summand))3406return self.ELGsum(c_n)34073408def ch_E_prod(self,quiet=True):3409"""3410The product version of the Chern character formula.34113412Args:3413quiet (bool, optional): No output. Defaults to True.34143415Returns:3416ELGTautClass: Chern character of the tangent bundle.3417"""3418N = QQ(self.dim() + 1)3419ch_E = [N*self.ONE]3420for L, profiles in enumerate(self.lookup_list):3421if not quiet:3422print('Going through %r profiles of length %r...' % (len(profiles), L))3423summand = []3424for p in profiles:3425if not p:3426continue3427Nd = self.bics[p[-1]].top.dim() + 13428if N == Nd: # factor == 03429continue3430factor = (N - Nd)3431bic_prod = self.ONE3432for Di in p:3433bic_prod *= self.exp_bic(Di)3434summand.append(factor*bic_prod)3435ch_E.append(self.ELGsum(summand))3436return self.exp_xi(quiet=quiet) * self.ELGsum(ch_E)34373438def ch_E_fast(self,quiet=True):3439"""3440A more direct (and faster) formula for the Chern character (see sec 9 of the paper).34413442Args:3443quiet (bool, optional): No output. Defaults to True.34443445Returns:3446ELGTautClass: Chern character of the tangent bundle.3447"""3448N = QQ(self.dim() + 1)3449ch_E = [N*self.exp_xi(quiet=quiet)]3450for L in range(1, N):3451if not quiet:3452print('Going through %r profiles of length %r...' % (len(self.enhanced_profiles_of_length(L)), L))3453summand = []3454for ep in self.enhanced_profiles_of_length(L):3455p, _ = ep3456ld = [self.bics[b].ell for b in p]3457Nd = self.bics[p[-1]].top.dim() + 13458if N == Nd: # factor == 03459continue3460ld_prod = 13461for l in ld:3462ld_prod *= l3463factor = ld_prod * (N - Nd)3464td_prod = self.ONE3465for i in range(L):3466td_prod = self.intersection(td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)3467exi = self.exp(self.xi_at_level(0, ep), ep)3468pr = self.intersection(exi, td_prod, ep)3469summand.append(factor*pr)3470ch_E.append(self.ELGsum(summand))3471return self.ELGsum(ch_E)34723473def top_chern_alt(self):3474"""3475The top Chern class of self, computed by calulating the Chern polynomial3476from the Chern character as P_B*exp(L) and taking the top-degree part.34773478Returns:3479ELGTautClass: top Chern class of the tangent bundle.3480"""3481return self.charToPol(self.P_B*self.exp_L())[-1]34823483def first_term(self, top=False, quiet=True):3484"""3485The calculation of (N*self.exp_xi() - self.ONE)*self.exp_L() split into3486pieces with more debugging outputs (calculation can take a LONG time!)34873488Args:3489top (bool, optional): Do calculations on level. Defaults to False.3490quiet (bool, optional): No output. Defaults to True.34913492Returns:3493ELGTautClass: First term of ch.3494"""3495if not quiet:3496print('Calculating first term...')3497N = QQ(self.dim() + 1)3498BICs = []3499for i, B in enumerate(self.bics):3500BICs.append((B.ell, self.additive_generator(((i,),0))))3501L = ELGTautClass(self, BICs, reduce=False)3502if top:3503if not quiet:3504print('Calculating exp_xi_L...')3505exp_xi_L = self.ELGsum([N*B.ell*self.exp(self.xi_at_level(0,((i,),0)), ((i,),0),quiet=quiet) for i, B in enumerate(self.bics)] + [(-1)*L])3506last = exp_xi_L3507if not quiet:3508print('Calculating recursive exponential factors: ', end=' ')3509for k in range(1, N-1):3510if not quiet:3511print(k, end=' ')3512last = QQ(1)/QQ(k+1) * L * last3513if last == self.ZERO:3514break3515exp_xi_L._psi_list.extend(last.psi_list)3516if not quiet:3517print('Done!')3518print('Adding exp_xi...')3519res = self.ELGsum([N*self.exp_xi(quiet=quiet), -self.ONE, exp_xi_L])3520else:3521if not quiet:3522print('Calculating exp(xi+L)...')3523res = N * self.exp(self.xi + L, quiet=quiet)3524if not quiet:3525print('Subtracting exp_L...')3526res -= self.exp_L(quiet=quiet)3527if not quiet:3528print('Done calculating first term!')3529return res35303531def ch_E(self, inside=True, prod=True, top=False, quiet=True):3532"""3533The Chern character (accoring to sec. 9 of the paper)35343535Args:3536inside (bool, optional): work with ambient. Defaults to True.3537prod (bool, optional): product instead of sum. Defaults to True.3538top (bool, optional): work on level. Defaults to False.3539quiet (bool, optional): no output. Defaults to True.35403541Returns:3542ELGTautClass: Chern character of the tangent bundle of self.3543"""3544# Prop. 9.23545N = QQ(self.dim() + 1)3546# ch = [(N*self.exp_xi() + (-1)*self.ONE)*self.exp_L()]3547ch = [self.first_term(top=top,quiet=quiet)]3548for L in range(1,N):3549inner = []3550if not quiet:3551print('Going through profiles of length %r...' % L)3552for enh_profile in self.enhanced_profiles_of_length(L):3553p, _ = enh_profile3554B = self.bics[p[0]]3555Ntop = B.top.dim() + 13556if not inside:3557summand = (-1)**L * (Ntop*self.exp_xi() - self.ONE)3558else:3559if not quiet:3560print('Calculating inner exp(xi): ', end=' ')3561summand = (-1)**L * (Ntop*self.exp(self.xi_at_level(0, enh_profile), enh_profile, quiet=quiet) - self.taut_from_graph(*enh_profile))3562prod_list = []3563for i in range(L):3564ll = self.bics[p[i]].ell3565squish = self.squish(enh_profile, i)3566td_NB = ll * self.td_contrib(-ll, self.cnb(enh_profile, enh_profile, squish), enh_profile)3567prod_list.append(td_NB)3568if prod_list:3569prod = prod_list[0]3570for f in prod_list[1:]:3571# multiply with ambient Gamma (=enh_profile)!3572prod = self.intersection(prod, f, enh_profile)3573if prod:3574for l in range(len(p) + 1):3575prod = self.intersection(prod,\3576self.exp(self.calL(enh_profile, l), enh_profile),\3577enh_profile)3578else:3579prod = self.intersection(prod,\3580self.exp(3581self.ELGsum(self.calL(enh_profile, l) for l in range(len(p)+1)),\3582enh_profile),3583enh_profile)3584if inside:3585prod = self.intersection(prod, summand, enh_profile)3586# multiply constant term with Gamma (for i_*)3587const = prod.degree(0)3588prod += (-1) * const3589if inside:3590summand = prod3591else:3592summand *= (prod + const*self.taut_from_graph(*enh_profile))3593inner.append(summand)3594ch.append(self.ELGsum(inner))3595return self.ELGsum(ch)35963597################################################################3598############ END OF SEC 9 FORMULAS #############################3599################################################################36003601def res_stratum_class(self,cond,debug=False):3602"""3603The class of the stratum cut out by cond inside self.36043605Args:3606cond (list): list of a residue condition, i.e. a list of poles of self.36073608Returns:3609ELGTautClass: Tautological class of Prop. 9.336103611EXAMPLES ::361236133614"""3615st_class = -1 * self.xi_with_leg(quiet=True)3616bic_list = []3617if debug:3618print("Calculating the class of the stratum cut out by %r in %r..." % (cond,self))3619print("-xi = %s" % st_class)3620for i, B in enumerate(self.bics):3621if debug:3622print("Checking BIC %r:" % i)3623top = B.top3624# we restrict/translate cond to top:3625poles_on_bic = [B.dmp_inv[p] for p in cond]3626cond_on_top = [top.leg_dict[leg] for leg in poles_on_bic if leg in top.leg_dict]3627# if there are RCs on top, we must check that they don't change the rank3628if cond_on_top:3629MT = top.matrix_from_res_conditions([cond_on_top])3630top_G = top.smooth_LG3631RT = top_G.full_residue_matrix3632if (MT.stack(RT)).rank() != RT.rank():3633assert (MT.stack(RT)).rank() > RT.rank()3634if debug:3635print("Discarding (because of top).")3636continue3637l = B.ell3638if debug:3639print("Appending with coefficient -%r" % l)3640bic_list.append((l,i))3641st_class += self.ELGsum([-l*self.taut_from_graph((i,),0) for l, i in bic_list])3642return st_class36433644def adm_evaluate(self,stgraph,psis,sig,g,quiet=False,admcycles_output=False):3645"""3646Evaluate the psi monomial on a (connected) stratum without residue conditions3647using admcycles.36483649stgraph should be the one-vertex graph associated to the stratum sig.36503651We use admcycles Strataclass to calculate the class of the stratum inside3652Mbar_{g,n} and multiply this with psis (in admcycles) and evaluate the product.36533654The result is cached and synched with the ``ADM_EVALS`` cache.36553656Args:3657stgraph (stgraph): admcycles stgraph3658psis (dict): psi polynomial on stgraph3659sig (tuple): signature tuple3660g (int): genus of sig3661quiet (bool, optional): No output. Defaults to False.3662admcycles_output (bool, optional): Print the admcycles classes. Defaults to False.36633664Returns:3665QQ: integral of psis on stgraph.3666"""3667# key = (tuple(sorted(psis.items())), tuple(sig))3668key = adm_key(sig, psis)3669cache = ADM_EVALS3670if key not in cache:3671DS = admcycles.admcycles.decstratum(stgraph,psi=psis)3672Stratum_class = admcycles.stratarecursion.Strataclass(g,1,sig)3673if not quiet or admcycles_output:3674print("DS: %r\n Stratum_class: %r" % (DS,Stratum_class))3675product = Stratum_class*DS # in admcycles!3676if not quiet or admcycles_output:3677print("Product: %r" % product.evaluate())3678cache[key] = product.evaluate() # in admcycles!3679return cache[key]36803681def remove_res_cond(self, psis=None):3682"""3683Remove residue conditions until the rank drops (or there are none left).36843685We return the stratum with fewer residue conditions and, in3686case the rank dropped, with the product of the stratum class.36873688Note that this does *not* ensure that all residue conditions are removed!36893690Args:3691psis (dict, optional): Psi dictionary on self. Defaults to None.36923693Returns:3694ELGTautClass: ELGTautClass on Stratum with less residue conditions3695(or self if there were none!)36963697EXAMPLES ::36983699sage: from admcycles.diffstrata import *3700sage: X=GeneralisedStratum([Signature((1,1,-2,-2))], res_cond=[[(0,2)], [(0,3)]])3701sage: print(X.remove_res_cond())3702Tautological class on Stratum: Signature((1, 1, -2, -2))3703with residue conditions:3704dimension: 13705leg dictionary: {}3706<BLANKLINE>37071 * Psi class 3 with exponent 1 on level 0 * Graph ((), 0) +3708<BLANKLINE>3709sage: X.evaluate(quiet=True) == X.remove_res_cond().evaluate()3710True3711"""3712if psis is None:3713psis = {}37143715if not self.res_cond:3716return self.additive_generator(((),0), psis).as_taut()37173718try:3719new_leg_dict = deepcopy(self._leg_dict)3720except AttributeError:3721new_leg_dict = {}37223723# Create new stratum with one residue condition less:3724new_rc = deepcopy(self._res_cond)3725# conditions from RT:3726RT_M = self.smooth_LG.residue_matrix_from_RT3727# we remove conditions until the rank drops:3728while new_rc:3729lost_cond = new_rc.pop()3730new_M = self.matrix_from_res_conditions(new_rc)3731if new_M:3732full_M = new_M.stack(RT_M)3733else:3734full_M = RT_M3735if full_M.rank() == self.smooth_LG.full_residue_matrix.rank() - 1:3736# rank dropped3737break3738new_stratum = LevelStratum(self._sig_list,new_rc,new_leg_dict)3739# Because only the RCs changed, X.smooth_LG still lives inside this stratum3740# so we can use it to build our new AdditiveGenerator:3741new_AG = new_stratum.additive_generator(((),0), psis)3742if new_stratum.dim() == self.dim() + 1:3743new_class = new_AG.as_taut()*new_stratum.res_stratum_class(lost_cond)3744else:3745# rank did not drop so all residue conditions are gone:3746assert not new_rc3747new_class = new_AG.as_taut()37483749return new_class375037513752def zeroStratumClass(self):3753"""3754Check if self splits, i.e. if a subset of vertices can be scaled3755independently (then the stratum class is ZERO).37563757We do this by checking if BICs B, B' exist with:3758* no edges3759* the top vertices of B are the bottom vertices of B'3760* the bottom vertices of B' are the top vertices of B.37613762Explicitly, we loop through all BICs with no edges, constructing for3763each one the BIC with the levels interchanged (as an EmbeddedLevelGraph)3764and check its legality.37653766Returns:3767boolean: True if splitting exists, False otherwise.37683769EXAMPLES ::37703771sage: from admcycles.diffstrata import *3772sage: GeneralisedStratum([Signature((0,)),Signature((0,))]).zeroStratumClass()3773True3774sage: GeneralisedStratum([Signature((2,))]).zeroStratumClass()3775False3776sage: GeneralisedStratum([Signature((4,-2,-2,-2)),Signature((4,-2,-2,-2))], res_cond=[[(0,2),(1,2)]]).zeroStratumClass()3777True3778sage: GeneralisedStratum([Signature((2, -2, -2)), Signature((1, 1, -2, -2))],[[(0, 2), (1, 2)], [(0, 1), (1, 3)]]).zeroStratumClass()3779False3780"""3781bics_no_edges = [b for b in self.bics if not b.LG.edges]3782if not bics_no_edges:3783return False3784for b in bics_no_edges:3785internal_top = b.LG.internal_level_number(0)3786internal_bot = b.LG.internal_level_number(1)3787top_vertices = b.LG.verticesonlevel(internal_top)3788bot_vertices = b.LG.verticesonlevel(internal_bot)3789assert len(top_vertices) + len(bot_vertices) == len(b.LG.genera)3790# build graph levels exchanged:3791new_levels = [internal_bot if v in top_vertices else internal_top3792for v in range(len(b.LG.genera))]3793new_vertices = deepcopy(b.LG.genera)3794new_legs = deepcopy(b.LG.legs)3795new_edges = []3796new_poleorders = deepcopy(b.LG.poleorders)3797new_LG = admcycles.diffstrata.levelgraph.LevelGraph(new_vertices,new_legs,new_edges,new_poleorders,new_levels)3798new_ELG = EmbeddedLevelGraph(self, new_LG, deepcopy(b.dmp), deepcopy(b.dlevels))3799# check if new graph is legal:3800if new_ELG.is_legal():3801return True3802# no splitting found3803return False38043805def evaluate(self,psis={},quiet=False,warnings_only=False,admcycles_output=False):3806"""3807Evaluate the psi monomial psis on self.38083809Psis is a dictionary legs of self.smooth_LG -> exponents encoding a psi monomial.38103811We translate residue conditions of self into intersections of simpler classes3812and feed the final pieces into admcycles for actual evaluation.38133814Args:3815psis (dict, optional): Psi monomial (as legs of smooth_LG -> exponent). Defaults to {}.3816quiet (bool, optional): No output. Defaults to False.3817warnings_only (bool, optional): Only warnings. Defaults to False.3818admcycles_output (bool, optional): adm_eval output. Defaults to False.38193820Raises:3821RuntimeError: raised if a required residue condition is not found.38223823Returns:3824QQ: integral of psis against self.3825"""3826G = self.smooth_LG3827LG = G.LG3828# Check if the rGRC doesn't cut down the dimension:3829# Recall:3830# * residue_matrix_from_RT has the RT on each component of G as rows3831# * full_residue_matrix is this + the res_cond of self3832if G.full_residue_matrix.rank() == G.residue_matrix_from_RT.rank():3833if self._h0 > 1:3834if not quiet or warnings_only:3835print("----------------------------------------------------")3836print("Level %r disconnected." % self)3837print("----------------------------------------------------")3838print("No residue conditions: contribution is 0.")3839return 03840# stratum is connected!3841# 0 dimensional strata contribute 13842if self.dim() == 0:3843return 13844# We can just use admcycles to evaluate:3845return self.adm_evaluate(LG.stgraph,psis,self._sig_list[0].sig,LG.g(),quiet=quiet,admcycles_output=admcycles_output)3846# There *are* non-trivial residue conditions!3847if self._h0 > 1:3848if not quiet or warnings_only:3849print("----------------------------------------------------")3850print("Level %r disconnected." % self)3851print("----------------------------------------------------")3852# Check if graph of residue conditions is disconnected:3853if not LG.underlying_graph.is_connected():3854if not quiet or warnings_only:3855print("Level is product: contribution is 0.")3856return 03857# Create new stratum with one residue condition less:3858new_rc = deepcopy(self._res_cond)3859# conditions from RT:3860RT_M = G.residue_matrix_from_RT3861# we remove conditions until the rank drops:3862while new_rc:3863lost_cond = new_rc.pop()3864new_M = self.matrix_from_res_conditions(new_rc)3865if new_M:3866full_M = new_M.stack(RT_M)3867else:3868full_M = RT_M3869if full_M.rank() == G.full_residue_matrix.rank() - 1:3870# rank dropped3871break3872else:3873raise RuntimeError("No Conditions cause dimension to drop in %r!" % self._res_cond)3874try:3875new_leg_dict = deepcopy(self._leg_dict)3876except AttributeError:3877new_leg_dict = {}3878new_stratum = LevelStratum(self._sig_list,new_rc,new_leg_dict)3879assert new_stratum.dim() == self.dim() + 13880# Because only the RCs changed, G still lives inside this stratum3881# so we can use it to build our new AdditiveGenerator:3882new_AG = new_stratum.additive_generator(((),0),psis)3883new_class = new_AG.as_taut()*new_stratum.res_stratum_class(lost_cond)3884result = new_class.evaluate(quiet=quiet)3885return result38863887#################################################################3888#################################################################3889#################################################################3890#################################################################38913892class Stratum(GeneralisedStratum):3893"""3894A simpler frontend for a GeneralisedStratum with one component and3895no residue conditions.3896"""3897def __init__(self,sig):3898super(Stratum, self).__init__([admcycles.diffstrata.sig.Signature(sig)])38993900#################################################################3901#################################################################3902#################################################################3903#################################################################39043905class LevelStratum(GeneralisedStratum):3906"""3907A stratum that appears as a level of a levelgraph.39083909This is a GeneralisedStratum together with a dictionary mapping the3910leg numbers of the (big) graph to the legs of the Generalisedstratum.39113912Note that if this is initialised from an EmbeddedLevelGraph, we also3913have the attribute leg_orbits, a nested list giving the orbits of3914the points under the automorphism group of the graph.39153916* leg_dict : a (bijective!) dictionary mapping the leg numbers of a graph3917to the corresponing tuple (i,j), i.e. the point j on the component i.39183919* res_cond : a (nested) list of residue conditions given by the r-GRC when3920extracting a level.39213922"""3923def __init__(self,sig_list,res_cond=None,leg_dict=None):3924super(LevelStratum,self).__init__(sig_list,res_cond)3925if leg_dict is None:3926# assume the points were numbered 1...n3927self._leg_dict = {}3928for i in range(len(sig_list)):3929for j in range(sig_list[i].n):3930self._leg_dict[i+j+1] = (i,j)3931else:3932self._leg_dict = leg_dict3933# build inverse dictonary3934self._inv_leg_dict = dict([(v,k) for k,v in self._leg_dict.items()])39353936def __repr__(self):3937return "LevelStratum(sig_list=%r,res_cond=%r,leg_dict=%r)" % (self._sig_list,self._res_cond,self.leg_dict)3938def __str__(self):3939rep = ''3940if self._h0 > 1:3941rep += 'Product of Strata:\n'3942else:3943rep += 'Stratum: '3944for sig in self._sig_list:3945rep += repr(sig) + '\n'3946rep += 'with residue conditions: '3947for res in self._res_cond:3948rep += repr(res) + ' '3949rep += '\n'3950rep += 'dimension: ' + repr(self.dim()) + '\n'3951rep += 'leg dictionary: ' + repr(self._leg_dict) + '\n'3952try:3953rep += 'leg orbits: ' + repr(self.leg_orbits) + '\n'3954except AttributeError:3955pass3956return rep39573958@cached_method3959def dict_key(self):3960"""3961The hash-key for the cache of top-xi-powers.39623963More precisely, we sort each signature, sort this list and renumber3964the residue conditions accordingly. Finally, everything is made into a tuple.39653966Returns:3967tuple: nested tuple.3968"""3969rc_dict = {}3970sig = []3971for new_i, new_sign in enumerate(sorted(enumerate(self._sig_list), key=lambda k: k[1].sig)):3972i, sign = new_sign3973curr_sig = []3974for new_j, s in enumerate(sorted(enumerate(sign.sig), key=lambda k: k[1])):3975j, a = s3976curr_sig.append(a)3977rc_dict[(i,j)] = (new_i, new_j)3978sig.append(tuple(curr_sig))3979sig = tuple(sig)3980rc = sorted([sorted([rc_dict[cond] for cond in conds]) for conds in self._res_cond])3981rc = tuple(tuple(c) for c in rc)3982return (sig, rc)39833984@property3985def leg_dict(self):3986return self._leg_dict39873988@property3989def inv_leg_dict(self):3990return self._inv_leg_dict39913992## Psi classes are numbered according to the points of the stratum, but we want3993## to use them for the points of the graph. The leg_dicts translate between these,3994## we make this a little more user friendly.3995def stratum_number(self,n):3996"""3997Returns a tuple (i,j) for the point j on the component i that corresponds3998to the leg n of the graph.3999"""4000return self._leg_dict[n]40014002def leg_number(self,n):4003"""4004Returns the leg number (of the graph G) that corresponds to the psi class4005number n.4006"""4007return self._inv_leg_dict[n]40084009#################################################################4010#################################################################4011#################################################################4012#################################################################40134014class EmbeddedLevelGraph(object):4015"""4016LevelGraph inside a generalised stratum.40174018Note that the points of the enveloping GeneralisedStratum are of the form4019(i,j) where i is the component and j the index of sig of that component,4020while the points of the level graph are numbers 1,...,n.40214022Thus, dmp is a dictionary mapping integers to tuples of integers.40234024Attributes:4025LG (LevelGraph): underlying LevelGraph4026X (GeneralisedStratum): enveloping stratum4027dmp (dict): (bijective!) dictionary marked points of LG -> points of stratum4028dmp_inv (dict): inverse of dmp4029dlevels (dict): (bijective!) dictionary levels of LG -> new level numbering4030dlevels_inv (dict): inverse of dlevels4031top (GeneralisedStratum): (if self is a BIC) top component4032bot (GeneralisedStratum): (if self is a BIC) bottom component4033clutch_dict (dict): (if self is a BIC) dictionary mapping points of top4034stratum to points of bottom stratum where there is an edge in self.4035emb_top (dict): (if self is a BIC) dictionary mapping points of stratum top4036to the corresponding points of the enveloping stratum.4037emb_bot (dict): (if self is a BIC) dictionary mapping points of stratum bot4038to the corresponding points of the enveloping stratum.4039automorphisms (list of list of dicts): automorphisms4040codim (int): codimension of LevelGraph in stratum4041number_of_levels (int): Number of levels of self.40424043Note that attempting to access any of the attributes top, bot, clutch_dict,4044emb_top or emb_bot will raise a ValueError if self is not a BIC.4045"""4046def __init__(self,X,LG,dmp,dlevels):4047"""4048Initialises EmbeddedLevelGraph.40494050Args:4051LG (LevelGraph): underlying LevelGraph4052X (GeneralisedStratum): enveloping stratum4053dmp (dictionary): (bijective!) dictionary marked points of LG -> points of stratum4054dlevels (dictionary): (bijective!) dictionary levels of LG -> new level numbering4055"""4056self.LG = LG4057self.X = X4058self.dmp = dmp4059self.dmp_inv = {value: key for key, value in dmp.items()}4060self.add_vertices_at_infinity()4061self.dlevels = dlevels4062self.dlevels_inv = {value: key for key, value in dlevels.items()}4063self._top = None4064self._bot = None4065self._clutch_dict = None4066self._emb_top = None4067self._emb_bot = None4068self._automorphisms = None4069self._level = {}4070self._ell = None4071self.codim = self.LG.codim()4072self.number_of_levels = len(set(self.dlevels.keys()))40734074def __repr__(self):4075return "EmbeddedLevelGraph(LG=%r,dmp=%r,dlevels=%r)" % (self.LG, self.dmp, self.dlevels)4076def __str__(self):4077return ("Embedded Level Graph consisting of %s with point dictionary %s and level dictionary %s"4078% (self.LG, self.dmp, self.dlevels))40794080def explain(self):4081"""4082A more user-friendly display of __str__ :-)40834084EXAMPLES ::40854086"""4087def _list_print(L):4088if len(L) > 1:4089s = ['s ']4090for x in L[:-2]:4091s.append('%r, ' % x)4092s.append('%r ' % L[-2])4093s.append('and %r.' % L[-1])4094return ''.join(s)4095else:4096return ' %r.' % L[0]4097def _num(i):4098if i == 1:4099return 'one edge'4100else:4101return '%r edges' % i4102print("LevelGraph embedded into stratum %s with:" % self.X)4103LG = self.LG4104for l in range(LG.numberoflevels()):4105internal_l = LG.internal_level_number(l)4106print("On level %r:" % l)4107for v in LG.verticesonlevel(internal_l):4108print("* A vertex (number %r) of genus %r" % (v, LG.genus(v)))4109levels_of_mps = list(set([LG.level_number(LG.levelofleg(leg)) for leg in self.dmp]))4110print("The marked points are on level%s" % _list_print(sorted(levels_of_mps)))4111print("More precisely, we have:")4112for leg in self.dmp:4113print("* Marked point %r of order %r on vertex %r on level %r" % \4114(self.dmp[leg], LG.orderatleg(leg), LG.vertex(leg), LG.level_number(LG.levelofleg(leg))))4115print("Finally, we have %s. More precisely:" % _num(len(LG.edges)))4116edge_dict = {e : (LG.vertex(e[0]), LG.vertex(e[1])) for e in LG.edges}4117edge_dict_inv = {}4118for k, v in edge_dict.items():4119if v in edge_dict_inv:4120edge_dict_inv[v].append(k)4121else:4122edge_dict_inv[v] = [k]4123for e in edge_dict_inv:4124print("* %s between vertex %r (on level %r) and vertex %r (on level %r) with prong%s" %4125(_num(len(edge_dict_inv[e])),4126e[0], LG.level_number(LG.levelofvertex(e[0])),4127e[1], LG.level_number(LG.levelofvertex(e[1])),4128# _write_prongs()4129_list_print([LG.prong(ee) for ee in edge_dict_inv[e]])))41304131def __eq__(self,other):4132if not isinstance(other, EmbeddedLevelGraph):4133return False4134return self.LG == other.LG and self.dmp == other.dmp and self.dlevels == other.dlevels41354136@cached_method4137def is_bic(self):4138return self.LG.is_bic()41394140@property4141def ell(self):4142"""4143If self is a BIC: the lcm of the prongs.41444145Raises:4146RuntimeError: raised if self is not a BIC.41474148Returns:4149int: lcm of the prongs.4150"""4151if self._ell is None:4152if not self.is_bic():4153raise RuntimeError("ell only defined for BICs!")4154self._ell = lcm(self.LG.prongs.values())4155return self._ell41564157@property4158def top(self):4159if self._top is None:4160self.split()4161return self._top41624163@property4164def bot(self):4165if self._bot is None:4166self.split()4167return self._bot41684169@property4170def clutch_dict(self):4171if self._clutch_dict is None:4172self.split()4173return self._clutch_dict41744175@property4176def emb_bot(self):4177if self._emb_bot is None:4178self.split()4179return self._emb_bot41804181@property4182def emb_top(self):4183if self._emb_top is None:4184self.split()4185return self._emb_top41864187def add_vertices_at_infinity(self):4188"""4189We add the vertices at infinity to the underlying_graph of self.LG.41904191These are given by the residue conditions.41924193More precisely: Recall that the underlying_graph of self.LG has vertices4194and edges of self.LG stored in the form UG_vertex = (vertex number, genus, 'LG')4195and edges of the underlying graph are of the form:4196(UG_vertex, UG_vertex, edge name)4197We now add vertices 'at level infinity' by adding, for each res_cond of self.X4198* a UG_vertex calles (i, 0, 'res') (where i is the index of the condition in res_cond4199we are currently considering)4200and edges so that each residue condition corresponds to an edge from the corresponding4201pole to some residue at 'level infinity'. We store these in the form:4202* (res_vertex, UG_vertex, resiedgej)4203Here UG_vertex is the vertex of self.LG, in the form (vertex number, genus, 'LG'),4204that res_vertex is attached to and j is the leg of that vertex (as a leg of self.LG!)4205corresponding to the pole that resi should be attached to.4206"""4207# remove any that might already be there:4208existing_residues = [v for v in self.LG.underlying_graph.vertices()4209if v[2] == 'res']4210for v in existing_residues:4211self.LG.underlying_graph.delete_vertex(v)4212# add a vertex for every residue condition:4213# TODO: remove duplicates?4214edges = []4215for i, rc in enumerate(self.X.res_cond):4216v_name = (i, 0, 'res')4217for p in rc:4218e_name = 'res%redge%r' % (i,self.dmp_inv[p])4219v_on_graph = self.LG.vertex(self.dmp_inv[p])4220edges.append((self.LG.UG_vertex(v_on_graph),v_name,e_name))4221self.LG.underlying_graph.add_edges(edges)42224223@property4224@cached_method4225def residue_matrix_from_RT(self):4226"""4227The matrix associated to the residue conditions imposed by the residue theorem4228on each vertex of self.42294230Returns:4231SAGE Matrix: matrix of residue conditions given by RT4232"""4233poles_by_vertex = {}4234for p in self.X._polelist:4235vertex = self.LG.vertex(self.dmp_inv[p])4236try:4237poles_by_vertex[vertex].append(p)4238except KeyError:4239poles_by_vertex[vertex] = [p]4240rows = []4241for v in poles_by_vertex:4242rows.append([int(p in poles_by_vertex[v]) for p in self.X._polelist])4243return matrix(QQ,rows)42444245@property4246@cached_method4247def full_residue_matrix(self):4248"""4249Residue matrix with GRC conditions and RT conditions (for each vertex).42504251Returns:4252matrix: Matrix with # of poles columns and a row for each condition.4253"""4254M = self.X.residue_matrix()4255if M:4256M = M.stack(self.residue_matrix_from_RT)4257else:4258M = self.residue_matrix_from_RT4259return M42604261def residue_zero(self,pole):4262"""4263Check if the residue at pole is forced zero by residue conditions.42644265NOTE: We DO include the RT on the vertices in this check!42664267Args:4268pole (tuple): pole (as a point (i,j) of self.X)42694270Returns:4271bool: True if forced zero, False otherwise.4272"""4273# add the equation corresponding to the residue at pole to the residue matrix4274# and see if the rank changes:4275i = self.X._polelist.index(pole)4276res_vec = [[int(i==j) for j in range(len(self.X._polelist))]]4277RM = self.full_residue_matrix4278# RM = self.X.residue_matrix()4279if RM:4280stacked = RM.stack(matrix(res_vec))4281return stacked.rank() == self.full_residue_matrix.rank()4282# return stacked.rank() == self.X.residue_matrix().rank()4283else:4284return False42854286def level(self,l):4287"""4288The generalised stratum on level l.42894290Note that this is cached, i.e. on first call, it is stored in the dictionary4291_level.42924293Args:4294l (int): relative level number (0,...,codim)42954296Returns:4297LevelStratum: the LevelStratum, i.e.4298* a list of Signatures (one for each vertex on the level)4299* a list of residue conditions, i.e. a list [res_1,...,res_r]4300where each res_k is a list of tuples [(i_1,j_1),...,(i_n,j_n)]4301where each tuple (i,j) refers to the point j (i.e. index) on the4302component i and such that the residues at these points add up4303to 0.4304* a dictionary of legs, i.e. n -> (i,j) where n is the original4305number of the point (on the LevelGraph self) and i is the4306number of the component, j the index of the point in the signature tuple.4307Note that LevelStratum is a GeneralisedStratum together with4308a leg dictionary.4309Here, we provide an additional attribute:4310* leg_orbits, a nested list giving the orbits of the points on the level4311under the automorphism group of self.43124313EXAMPLES ::431443154316For the banana graph, the automorphisms fix the marked points but permute4317the edges. (ALL CONCRETE EXAMPLES REMOVED DUE TO INCONSISTENT BIC NUMBERING BETWEEN SAGE VERSIONS!!!)431843194320For the V-graph, the automorphisms permute edges on different components.432143224323In the stratum (4), there are more complicated examples.43244325"""4326try:4327LS = self._level[l]4328except KeyError:4329# for the residue conditions: We add the residue conditions from4330# the enveloping stratum:4331# We do this by passing those poles with residue forced zero4332# as those to be ignored in the residue calcuations performed by the4333# LevelGraph:4334# We have to translate them to points on self:4335# Note that self.LG knows the "level at infinity"4336excluded_poles = tuple(self.dmp_inv[p] for p in flatten(self.X.res_cond,max_level=1))4337LS = self.LG.stratum_from_level(l,excluded_poles=excluded_poles)4338# add automorphism info4339LS.leg_orbits = []4340seen = set()4341for leg in LS._leg_dict:4342if leg in seen:4343continue4344curr_orbit = [LS._leg_dict[leg]]4345for _v_map, l_map in self.automorphisms:4346curr_orbit.append(LS._leg_dict[l_map[leg]])4347seen.update([l_map[leg]])4348LS.leg_orbits.append(list(set(curr_orbit))) # remove duplicates4349self._level[l] = LS4350return LS43514352def legs_are_isomorphic(self,leg,other_leg):4353"""4354Check if leg and other_leg are in the same Aut-orbit of self.43554356Args:4357leg (int): leg on self.LG4358other_leg (int): leg on self.LG43594360Raises:4361RuntimeError: If leg is not in any aut-orbit of the level it should be on.43624363Returns:4364bool: True if they are in the same orbit of self.level(levelofleg),4365False, otherwise.43664367EXAMPLES ::436843694370Note the assymetric banana graph.437143724373The symmetric one has isomorphisms.437443754376Legs are isomorphic to themselves.43774378It's symmetric.43794380"""4381level = self.LG.level_number(self.LG.levelofleg(leg))4382other_level = self.LG.level_number(self.LG.levelofleg(other_leg))4383if level != other_level:4384return False4385assert(level == other_level)4386emb_leg = self.level(level)._leg_dict[leg]4387emb_other_leg = self.level(level)._leg_dict[other_leg]4388for orbit in self.level(level).leg_orbits:4389if emb_leg in orbit:4390if emb_other_leg in orbit:4391return True4392else:4393return False4394else:4395raise RuntimeError ("leg %r not in any orbit %r of %r" %4396(leg,self.level(level).leg_orbits,self.level(level)))43974398@cached_method4399def edge_orbit(self,edge):4400"""4401The edge orbit of edge in self.44024403Args:4404edge (tuple): edge of self.LG, i.e. tuple (start leg, end leg), where4405start leg should not be on a lower level than end leg.44064407Raises:4408ValueError: if edge is not an edge of self.LG44094410Returns:4411set: set of edges in aut-orbit of edge.44124413EXAMPLES ::44144415"""4416if not edge in self.LG.edges:4417raise ValueError("%r is not an edge of %r!" % (edge, self))4418s = set([edge])4419for v_map, l_map in self.automorphisms:4420new_edge = (l_map[edge[0]], l_map[edge[1]])4421s.add(new_edge)4422return s44234424def len_edge_orbit(self,edge):4425"""4426Length of the edge orbit of edge in self.44274428Args:4429edge (tuple): edge of self.LG, i.e. tuple (start leg, end leg), where4430start leg should not be on a lower level than end leg.44314432Raises:4433ValueError: if edge is not an edge of self.LG44344435Returns:4436int: length of the aut-orbit of edge.44374438EXAMPLES ::443944404441Prongs influence the orbit length.44424443"""4444return len(self.edge_orbit(edge))44454446def automorphisms_stabilising_legs(self,leg_tuple):4447stabs = []4448for v_map, l_map in self.automorphisms:4449for l in leg_tuple:4450if l_map[l] != l:4451break4452else: # no break4453stabs.append(l_map)4454return stabs44554456def delta(self,i):4457"""4458Squish all levels except for i.44594460Note that delta(1) contracts everything except top-level and that the4461argument is interpreted via internal_level_number (i.e. a relative level number).44624463Moreover, dlevels is set to map to 0 and -1(!).44644465Args:4466i (int): Level not to be squished.44674468Returns:4469EmbeddedLevelGraph: Embedded BIC (result of applying delta to the4470underlying LevelGraph)4471"""4472newLG = self.LG.delta(i,quiet=True)4473newdmp = self.dmp.copy()4474# level_number is (positive!) relative level number.4475newdlevels = {l:-newLG.level_number(l) for l in newLG.levels}4476return EmbeddedLevelGraph(self.X,newLG,newdmp,newdlevels)44774478def squish_vertical(self,level):4479"""4480Squish level crossing below level 'level'.44814482Note that in contrast to the levelgraph method, we work with relative4483level numbers here!44844485Args:4486level (int): relative (!) level number.44874488Returns:4489EmbeddedLevelGraph: Result of squishing.44904491EXAMPLES ::44924493sage: from admcycles.diffstrata import *4494sage: X=GeneralisedStratum([Signature((4,))])4495sage: p = X.enhanced_profiles_of_length(4)[0][0]4496sage: g = X.lookup_graph(p)44974498lookup_graph uses the sorted profile (note that these do not have to be reduced!):44994500sage: assert any(g.squish_vertical(0).is_isomorphic(G) for G in X.lookup(p[1:]))4501sage: assert any(g.squish_vertical(1).is_isomorphic(G) for G in X.lookup(p[:1]+p[2:]))4502sage: assert any(g.squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]+p[3:]))4503sage: assert any(g.squish_vertical(3).is_isomorphic(G) for G in X.lookup(p[:3]))45044505Squishing outside the range of levels does nothing:45064507sage: assert g.squish_vertical(4) == g45084509Recursive squishing removes larger parts of the profile:45104511sage: assert any(g.squish_vertical(3).squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]))4512"""4513newLG = self.LG.squish_vertical(self.LG.internal_level_number(level),quiet=True)4514newdmp = self.dmp.copy()4515# level_number is (positive!) relative level number.4516newdlevels = {l:-newLG.level_number(l) for l in newLG.levels}4517return EmbeddedLevelGraph(self.X,newLG,newdmp,newdlevels)45184519def split(self):4520"""4521Splits embedded BIC self into top and bottom component.45224523Raises:4524ValueError: Raised if self is not a BIC.45254526Returns:4527dict: dictionary consising of4528* X: GeneralisedStratum self.X4529* top: LevelStratum: top component4530* bottom: LevelStratum: bottom component4531* clutch_dict: clutching dictionary mapping ex-half-edges on4532top to their partners on bottom (both as points in the4533respective strata!)4534* emb_dict_top: a dictionary embedding top into the stratum of self4535* emb_dict_bot: a dictionary embedding bot into the stratum of self45364537Note that clutch_dict, emb_top and emb_bot are dictionaries between4538points of strata, i.e. after applying dmp to the points!45394540EXAMPLES ::45414542"""4543if not self.is_bic():4544raise ValueError(4545"Error: %s is not a BIC! Cannot be split into Top and Bottom component!"4546% self)4547self._top = self.level(0)4548self._bot = self.level(1)4549# To construct emb_top and emb_bot, we have to combine self.dmp with the4550# the leg_dicts of top and bot.4551# More precisely: emb_top is the composition of the inverse of the leg_dict4552# of top, i.e. top.stratum_number, and self.dmp4553# (giving a map from the points of top to the points of the enveloping4554# stratum of self) and the same for bot.4555# We implement this by iterating over the marked points of self on top level,4556# which are exactly the keys of self.dmp that are on top level.4557# Note that we make extra sure that we didn't mess up the level numbering by4558# using the relative level numbering (where the top level is guaranteed to be 04559# and the bottom level is 1 (positive!)).4560self._emb_top = {self._top.stratum_number(l) : self.dmp[l]4561for l in iter(self.dmp)4562if self.LG.level_number(self.LG.levelofleg(l)) == 0}4563self._emb_bot = {self._bot.stratum_number(l) : self.dmp[l]4564for l in iter(self.dmp)4565if self.LG.level_number(self.LG.levelofleg(l)) == 1}4566# Because this is a BIC, all edges of self are cut in this process4567# and this is exactly the dictionary we must remember4568# WARNING: Here we assume that e[0] is on top level and e[1] is on bottom4569# This is assured by tidy_up, e.g. after initialisation!4570# Note that all these dictionaries map points of GeneralisedStrata to each4571# other so we must take the corresponding stratum_number!4572self._clutch_dict = {self._top.stratum_number(e[0]) : self._bot.stratum_number(e[1])4573for e in self.LG.edges}4574return {'X': self.X, 'top': self._top, 'bottom': self._bot,4575'clutch_dict': self._clutch_dict,4576'emb_dict_top': self._emb_top, 'emb_dict_bot': self._emb_bot,}45774578def is_legal(self):4579"""4580Check the R-GRC for self.45814582Returns:4583bool: result of R-GRC.4584"""4585# Check if any levels are empty:4586# Note that this can only happen if self.X has simple poles (as we never4587# have horizontal edges)4588if self.X.simple_poles():4589if any(self.level(l).is_empty() for l in range(self.number_of_levels)):4590return False4591# poles are excluded if they are contained in _any_ residue condition of the stratum.4592# In particular, they are _not_ excluded if they are only restrained by the RT on some component!4593poles_in_rc_stratum = flatten(self.X.res_cond, max_level=1)4594poles_in_rc_graph = tuple(self.dmp_inv[p] for p in poles_in_rc_stratum)4595return self.LG.is_legal(excluded_poles=poles_in_rc_graph, quiet=True)45964597def is_isomorphic(self,other):4598"""4599Check if self and other are isomorphic (as EmbeddedLevelGraphs).46004601Args:4602other (EmbeddedLevelGraph): Graph to check isomorphism.46034604Returns:4605bool: True if there exists at least one isomorphism.4606"""4607# TODO: Maybe include a way to check against unembedded LGs4608# TODO: Check embedding!4609if not isinstance(other, EmbeddedLevelGraph):4610return False4611try:4612next(self.isomorphisms(other))4613return True4614except StopIteration:4615return False46164617@property4618def automorphisms(self):4619"""4620The automorphisms of self (as automorphisms of the underlying LevelGraph,4621respecting the embedding, see doc of isomorphisms).46224623Returns:4624list: list of tuples:4625dict: map of vertices4626dict: map of legs46274628EXAMPLES ::46294630"""4631if not self._automorphisms:4632self._automorphisms = list(self.isomorphisms(self))4633return self._automorphisms46344635def isomorphisms(self,other):4636"""4637Generator yielding the "next" isomorphism of self and other.46384639Note that while this gives an "isomorphism" from self.LG to other.LG, this4640is not necessarily an isomorphism of the LevelGraphs (the numbered points may4641be permuted if this is "fixed" by the embedding).46424643Args:4644other (EmbeddedLevelGraph): The (potentially) isomorphic EmbeddedLevelGraph.46454646Yields:4647tuple: The next compatible isomorphism:4648dict: vertices of self.LG -> vertices of other.LG4649dict: legs of self.LG -> legs of other.LG4650"""4651#### Isomorphisms of EmbeddedLevelGraphs:4652## An isomorphism of EmbeddedLevelGraph is a set of compatible level isomorphisms.4653## We iterate through the isomorphisms on each level and yield whenever we find4654## compatible levelisomorphisms for all levels.4655## Note that we use dlevels for this, as these should be compatible.4656# There are (at least) two ways in which this can be optimised:4657# * don't go through the entire product before checking edge compatibility!4658# * choose a smart ordering of levels (e.g. number of vertices)4659isom_vertices = {}4660isom_legs = {}4661for level_isos in itertools.product(*[self._level_iso(other,l) for l in self.dlevels.values()]):4662for level_iso_v, level_iso_l in level_isos:4663isom_vertices.update(level_iso_v)4664isom_legs.update(level_iso_l)4665# check edge compatibility4666for e in self.LG.edges:4667if (isom_legs[e[0]],isom_legs[e[1]]) not in other.LG.edges:4668break4669else: # no break4670yield isom_vertices.copy(), isom_legs.copy()46714672def _level_iso(self,other,l):4673"""4674Generator yielding the "next" isomorphism of level l of self and other.46754676Here, l is a value of dlevels (this should be compatible).46774678Note that we require the graph to have no horizontal edges, i.e. the level4679contains no edges!46804681TODO: * Maybe add future horizontal support?4682* Maybe use relative level number instead? (this seems to give weird behaviour4683right now...)46844685Args:4686other (EmbeddedLevelGraph): The embedded graph we are checking for isomorphism.4687l (int): Level number (embedded into the stratum, i.e. value of dlevels).46884689Yields:4690tuple: the next isomorphism of levels:4691dict: vertices of self.LG -> vertices of other.LG4692dict: legs of self.LG -> legs of other.LG4693"""4694#### Isomorphisms of levels:4695## An isomorphism of levels consist of4696## * a map: vertices to vertices4697## * a map: legs to legs4698## respecting:4699## * the genus,4700## * the number of legs on every vertex,4701## * the order at every leg,4702## * the marked points of the stratum (via dmp).4703####4704# First, we extract the data for level l from self and other.4705# Note that we do not use stratum_from_level to avoid all the overhead.4706# TODO: All this should be cached!!4707l_self = self.LG.internal_level_number(l)4708l_other = other.LG.internal_level_number(l)4709# we need to be careful to distinguish the indices in the list of genera4710# of the LevelGraph from the actual genera.4711vv_self_idx = self.LG.verticesonlevel(l_self) # list of indices4712vv_other_idx = other.LG.verticesonlevel(l_other) # list of indices4713if len(vv_self_idx) != len(vv_other_idx):4714return4715vv_self = [self.LG.genus(i) for i in vv_self_idx] # list of genera4716vv_other = [other.LG.genus(i) for i in vv_other_idx] # list of genera4717# extract the legs: (nested lists)4718legs_self = [self.LG.legsatvertex(v) for v in vv_self_idx]4719legs_other = [other.LG.legsatvertex(v) for v in vv_other_idx]4720# build dictionary: leg -> index in vv4721leg_dict_self = {l:i for i,legs in enumerate(legs_self) for l in legs}4722leg_dict_other = {l:i for i, legs in enumerate(legs_other) for l in legs}4723if len(leg_dict_self) != len(leg_dict_other):4724return4725# for quick checks, we create sorted lists of the orders at each vertex4726order_sorted_self = [sorted([self.LG.orderatleg(l) for l in legs])4727for legs in legs_self]4728order_sorted_other = [sorted([other.LG.orderatleg(l) for l in legs])4729for legs in legs_other]4730# We create the two maps as dictionaries:4731# index of vv_self -> index of vv_other4732isom_vert = {}4733# leg number (on self.LG) -> leg number (on other.LG)4734isom_legs = {}4735# We also want to keep track of whom we've already mapped:4736# source is a dictionary: genus -> list of indices of vv_self4737source = {}4738for i, g in enumerate(vv_self):4739try:4740source[g].append(i)4741except KeyError:4742source[g] = [i]4743# target is a dictionary: genus -> list of indices of vv_other4744target = {}4745for i, g in enumerate(vv_other):4746try:4747target[g].append(i)4748except KeyError:4749target[g] = [i]4750# for the legs we build copies of the nested lists to manipulate4751legs_source = [legs[:] for legs in legs_self]4752legs_target = [legs[:] for legs in legs_other]4753# Next, we exclude some deal-breakers:4754# * The same genera must appear.4755if sorted(vv_self) != sorted(vv_other):4756return4757# * The same embedded points have to be on this level (they have to be4758# mapped to each other!)4759# In particular, this gives a part of the leg map (and thus also of the4760# vertex map).4761for p_self, p in self.dmp.items(): # p is the "shared" point of the stratum4762p_other = other.dmp_inv[p]4763# If neither point is on this level, we continue:4764if not (p_self in leg_dict_self or p_other in leg_dict_other):4765continue4766# The vertex of p_self must map to that of p_other.4767# Three things can fail here:4768# * only one of the two points is on this level.4769if ((p_self in leg_dict_self and p_other not in leg_dict_other) or4770(p_self not in leg_dict_self and p_other in leg_dict_other)):4771return4772v_self = leg_dict_self[p_self]4773v_other = leg_dict_other[p_other]4774# * the points are on incompatible vertices (genus or numbers/orders of legs!)4775if (vv_self[v_self] != vv_other[v_other] or4776len(legs_self[v_self]) != len(legs_other[v_other]) or4777order_sorted_self[v_self] != order_sorted_other[v_other]):4778return4779# * two points are on the same vertex in one case, but on different vertices4780# in the other. I.e. v_self is already being mapped somewhere other than v_other4781# or v_other is already being mapped to (by someone else)4782try:4783if isom_vert[v_self] != v_other:4784return4785except KeyError: # v_self not being mapped yet, i.e. still in source4786g = vv_other[v_other]4787if v_other in target[g]: # make sure v_other is still a possible target4788isom_vert[v_self] = v_other4789source[g].remove(v_self)4790target[g].remove(v_other)4791else:4792return4793# now we can safely map the legs:4794isom_legs[p_self] = p_other4795# and remove them from source and target (so they won't be reassigned later)4796legs_source[v_self].remove(p_self)4797legs_target[v_other].remove(p_other)4798# Next, we construct maps of the remaining vertices.4799# For this, we use a small recursive function:4800curr_v_map = {}4801legal_v_maps = []4802def vertex_maps(sl,tl):4803if not sl:4804# all entries of tl should be None at this point:4805assert(all(tv is None for tv in tl))4806legal_v_maps.append(curr_v_map.copy())4807return4808curr_v = sl.pop()4809curr_legs = len(legs_self[curr_v])4810# try to map curr_v to tl:4811for i,tv in enumerate(tl):4812# we temporarily set "hit" targets to None so we don't have to worry4813# about indexing...4814if tv is None:4815continue4816# check if legs _can_ be compatible:4817if (curr_legs != len(legs_other[tv]) or4818order_sorted_self[curr_v] != order_sorted_other[tv]):4819continue4820tl[i] = None4821curr_v_map[curr_v] = tv4822vertex_maps(sl,tl)4823# undo4824del curr_v_map[curr_v]4825tl[i] = tv4826# undo4827sl.append(curr_v)4828# the function for the legs is almost the same, just the condition is different:4829curr_l_map = {}4830legal_l_maps = []4831def leg_maps(sl,tl):4832if not sl:4833# all entries of tl should be None at this point:4834assert(all(tleg is None for tleg in tl))4835legal_l_maps.append(curr_l_map.copy())4836return4837curr_l = sl.pop()4838# try to map curr_l to tl:4839for i, tleg in enumerate(tl):4840# we temporarily set "hit" targets to None so we don't have to worry4841# about indexing...4842if tleg is None:4843continue4844# check if orders are compatible:4845if self.LG.orderatleg(curr_l) == other.LG.orderatleg(tleg):4846tl[i] = None4847curr_l_map[curr_l] = tleg4848leg_maps(sl,tl)4849# undo4850del curr_l_map[curr_l]4851tl[i] = tleg4852# undo4853sl.append(curr_l)4854# Now we build the list of all vertex isomorphisms going through the vertices by genus4855v_isom_list = []4856for g in source:4857legal_v_maps = [] # will get filled by vertex_maps4858vertex_maps(source[g],target[g])4859v_isom_list.append(legal_v_maps[:]) # copy!4860# v_isom_list is now a nested list of maps for each genus.4861# the product consists of tuples, one map for every genus.4862for v_maps in itertools.product(*v_isom_list):4863for v_map in v_maps:4864# this overwrites exactly the vertices in source.4865isom_vert.update(v_map)4866# Finally, the returned vertex map should use the indexing of the4867# LevelGraph, not of the level:4868return_isom_vert = {vv_self_idx[k] : vv_other_idx[v]4869for k,v in isom_vert.items()}4870# Now we build all leg maps, again as a product of all maps at vertices.4871# Note: This also included the previously assigned vertices (with marked points...)4872l_isom_list = []4873for v in isom_vert:4874# Construct leg maps:4875# We work with legs_source and legs_target, i.e. the list4876# of legs with the marked points removed.4877legal_l_maps = []4878leg_maps(legs_source[v],legs_target[isom_vert[v]])4879l_isom_list.append(legal_l_maps[:]) # copy!4880for l_maps in itertools.product(*l_isom_list):4881for l_map in l_maps:4882isom_legs.update(l_map)4883yield return_isom_vert.copy(), isom_legs.copy()48844885#################################################################4886#################################################################4887#################################################################4888#################################################################48894890class AdditiveGenerator (SageObject):4891"""4892Product of Psi classes on an EmbeddedLevelGraph (of a stratum X).48934894The information of a product of psi-class on an EmbeddedLevelGraph, i.e. a4895leg_dict and an enhanced_profile, where leg_dict is a dictionary on the legs4896leg -> exponent of the LevelGraph associated to the enhanced profile, i.e.4897(profile,index) or None if we refer to the class of the graph.48984899We (implicitly) work inside some stratum X, where the enhanced profile4900makes sense.49014902This class should be considered constant (hashable)!4903"""4904def __init__ (self,X,enh_profile,leg_dict=None):4905"""4906AdditiveGenerator for psi polynomial given by leg_dict on graph4907corresponding to enh_profile in X.49084909Args:4910X (GeneralisedStrataum): enveloping stratum4911enh_profile (tuple): enhanced profile (in X)4912leg_dict (dict, optional): dictionary leg of enh_profile -> exponent4913encoding a psi monomial. Defaults to None.4914"""4915self._X = X4916self._hash = hash_AG(leg_dict, enh_profile)4917self._enh_profile = (tuple(enh_profile[0]),enh_profile[1])4918self._leg_dict = leg_dict4919self._G = self._X.lookup_graph(*enh_profile)4920# dictionary leg -> level4921# Careful! These are leg numbers on the whole graph, not on4922# the graphs inside the LevelStrata!!4923self._level_dict = {}4924if not leg_dict is None:4925for l in leg_dict:4926self._level_dict[l] = self._G.LG.level_number(self._G.LG.levelofleg(l))4927self._inv_level_dict = {}4928for leg in self._level_dict:4929try:4930self._inv_level_dict[self._level_dict[leg]].append(leg)4931except KeyError:4932self._inv_level_dict[self._level_dict[leg]] = [leg]49334934@classmethod4935def from_hash(cls,X,hash):4936"""4937AdditiveGenerator from a hash generated with hash_AG.49384939Args:4940X (GeneralisedStratum): Enveloping stratum.4941hash (tuple): hash from hash_AG49424943Returns:4944AdditiveGenerator: AG from hash.4945"""4946if hash[0] is None:4947leg_dict = None4948else:4949leg_dict = dict(hash[0])4950return cls(X,(hash[1],hash[2]),leg_dict)49514952def __hash__(self):4953return hash(self._hash)4954def __eq__(self,other):4955try:4956return self._hash == other._hash4957except AttributeError:4958return NotImplemented49594960def __repr__(self):4961return "AdditiveGenerator(X=%r,enh_profile=%r,leg_dict=%r)"\4962% (self._X, self._enh_profile, self._leg_dict)4963# Better, but destroys tests:4964# return "AdditiveGenerator(enh_profile=%r,leg_dict=%r)"\4965# % (self._enh_profile, self._leg_dict)4966def __str__(self):4967str = ""4968if not self._leg_dict is None:4969for l in self._leg_dict:4970str += "Psi class %r with exponent %r on level %r * "\4971% (l, self._leg_dict[l], self._level_dict[l])4972str += "Graph %r" % (self._enh_profile,)4973return str49744975def __mul__(self,other):4976"""4977Multiply to psi products on the same graph (add dictioaries).49784979Args:4980other (AdditiveGenerator): Product of psi classes on same graph.49814982Returns:4983AdditiveGenerator: Product of psi classes on same graph.49844985EXAMPLES ::498649874988Also works without legs.49894990"""4991# Check that other is an AdditiveGenerator for the same graph:4992try:4993if self._X != other._X or self._enh_profile != other._enh_profile:4994return NotImplemented4995other_leg_dict = other._leg_dict4996except AttributeError:4997return NotImplemented4998# "unite" the leg_dicts:4999if self._leg_dict is None:5000self_leg_dict = {}5001else:5002self_leg_dict = self._leg_dict5003if other_leg_dict is None:5004other_leg_dict = {}5005new_leg_dict = {l:self_leg_dict.get(l,0) + other_leg_dict.get(l,0)5006for l in set(self_leg_dict) | set(other_leg_dict)}5007return self._X.additive_generator(self._enh_profile,new_leg_dict)5008def __rmul__(self,other):5009self.__mul__(other)5010def __pow__(self, n):5011return self.pow(n)50125013@property5014def enh_profile(self):5015return self._enh_profile50165017@property5018def psi_degree(self):5019"""5020Sum of powers of psi classes of self.5021"""5022if self._leg_dict is None:5023return 05024else:5025return sum(self._leg_dict.values())50265027@cached_method5028def dim_check(self):5029"""5030Check if, on any level, the psi degree is higher than the dimension.50315032Returns:5033bool: False if the class is 0 for dim reasons, True otherwise.5034"""5035# remove if degree > dim(X)5036if self.degree > self._X.dim():5037return False5038if self.codim == 0:5039# Avoid crazy infinite recursion for smooth graph :-)5040return True5041# for each level, check if psi product on level exceeds level dimension5042for level_number in range(self.codim + 1):5043assert self.level_dim(level_number) >= 05044if self.degree_on_level(level_number) > self.level_dim(level_number):5045return False5046return True50475048@property5049def codim(self):5050"""5051The codimension of the graph (number of levels)50525053Returns:5054int: length of the profile5055"""5056return len(self._enh_profile[0])50575058@property5059def degree(self):5060"""5061Degree of class, i.e. codimension of graph + psi-degree50625063Returns:5064int: codim + psi_degree5065"""5066# degree = codim of graph + powers of psi classes5067return self.codim + self.psi_degree50685069@property5070def leg_dict(self):5071return self._leg_dict50725073@property5074def level_dict(self):5075"""5076The dictionary mapping leg -> level5077"""5078return self._level_dict50795080@property5081def inv_level_dict(self):5082"""5083The dictionary mapping level -> list of legs on level.50845085Returns:5086dict: level -> list of legs.5087"""5088return self._inv_level_dict50895090@cached_method5091def degree_on_level(self,level):5092"""5093Total degree of psi classes on level.50945095Args:5096level (int): (relative) level number (i.e. 0...codim)50975098Raises:5099RuntimeError: Raised for level number out of range.51005101Returns:5102int: sum of exponents of psis appearing on this level.5103"""5104if level not in range(self.codim + 1):5105raise RuntimeError("Illegal level number: %r on %r" % (level, self))5106try:5107return sum(self._leg_dict[leg] for leg in self._inv_level_dict[level])5108except KeyError:5109# no psis on this level5110return 051115112def level(self,level_number):5113"""5114Level of underlying graph.51155116Args:5117level_number (int): (relative) level number (0...codim)51185119Returns:5120LevelStratum: Stratum at level level_number of self._G.5121"""5122return self._G.level(level_number)51235124@cached_method5125def level_dim(self,level_number):5126"""5127Dimension of level level_number.51285129Args:5130level_number (int): (relative) level number (i.e. 0...codim)51315132Returns:5133int: dimension of GeneralisedLevelStratum5134"""5135level = self._G.level(level_number)5136return level.dim()51375138@property5139def stack_factor(self):5140"""5141The stack factor, that is the product of the prongs of the underlying graph5142divided by the product of the ells of the BICs and the automorphisms.51435144Returns:5145QQ: stack factor5146"""5147try:5148return self._stack_factor5149except AttributeError:5150# to get g_Gamma, we have to take the product of prongs/lcm for each bic:5151prod = 15152for k in self._G.LG.prongs.values():5153prod *= k51545155p, _ = self.enh_profile51565157bic_contr = 15158for i in p:5159bic_contr *= self._X.bics[i].ell51605161stack_factor = QQ(prod) / QQ(bic_contr * len(self._G.automorphisms))51625163self._stack_factor = stack_factor5164return self._stack_factor51655166@cached_method5167def as_taut(self):5168"""5169Helper method, returns [(1,self)] as default input to ELGTautClass.5170"""5171return ELGTautClass(self._X,[(1,self)])51725173@cached_method5174def is_in_ambient(self,ambient_enh_profile):5175"""5176Check if ambient_enh_profile is an ambient graph, i.e. self is a degeneration5177of ambient_enh_profile.51785179Args:5180ambient_enh_profile (tuple): enhanced profile.51815182Returns:5183bool: True if there exists a leg map, False otherwise.51845185EXAMPLES ::51865187"""5188return self._X.is_degeneration(self._enh_profile,ambient_enh_profile)51895190@cached_method5191def pow(self, n, amb=None):5192"""5193Recursively calculate the n-th power of self (in amb), caching all results.51945195Args:5196n (int): exponent5197amb (tuple, optional): enhanced profile. Defaults to None.51985199Returns:5200ELGTautClass: self^n in CH(amb)5201"""5202if amb is None:5203ONE = self._X.ONE5204amb = ((),0)5205else:5206ONE = self._X.taut_from_graph(*amb)5207if n == 0:5208return ONE5209return self._X.intersection(self.as_taut(), self.pow(n-1, amb), amb)52105211@cached_method5212def exp(self, c, amb=None, stop=None):5213"""5214exp(c * self) in CH(amb), calculated via exp_list.52155216Args:5217c (QQ): coefficient5218amb (tuple, optional): enhanced profile. Defaults to None.5219stop (int, optional): cut-off. Defaults to None.52205221Returns:5222ELGTautClass: the tautological class associated to the5223graded list exp_list.5224"""5225# graded pieces are already reduced:5226new_taut_list = []5227for T in self.exp_list(c, amb, stop):5228new_taut_list.extend(T.psi_list)5229return ELGTautClass(self._X, new_taut_list, reduce=False)52305231@cached_method5232def exp_list(self, c, amb=None, stop=None):5233"""5234Calculate exp(c * self) in CH(amb).52355236We calculate exp as a sum of powers (using self.pow, i.e. cached)5237and check at each step if the power vanishes (if yes, we obviously stop).52385239The result is returned as a list consisting of the graded pieces.52405241Optionally, one may specify the cut-off degree using stop (by5242default this is dim + 1).52435244Args:5245c (QQ): coefficient5246amb (tuple, optional): enhanced profile. Defaults to None.5247stop (int, optional): cut-off. Defaults to None.52485249Returns:5250list: list of ELGTautClasses5251"""5252c = QQ(c)5253if amb is None:5254ONE = self._X.ONE5255amb = ((),0)5256else:5257ONE = self._X.taut_from_graph(*amb)5258e = [ONE]5259f = ONE5260coeff = QQ(1)5261k = QQ(0)5262if stop is None:5263stop = self._X.dim() + 15264while k < stop and f != self._X.ZERO:5265k += 15266coeff *= c/k5267f = self.pow(k, amb)5268e.append(coeff * f)5269return e52705271def pull_back(self,deg_enh_profile):5272"""5273Pull back self to the graph associated to deg_enh_profile.52745275Note that this returns an ELGTautClass as there could be several maps.52765277More precisely, we return the sum over the pulled back classes divided5278by the number of undegeneration maps.52795280Args:5281deg_enh_profile (tuple): enhanced profile of graph to pull back to.52825283Raises:5284RuntimeError: raised if deg_enh_profile is not a degeneration of the5285underlying graph of self.52865287Returns:5288ELGTautClass: sum of pullbacks of self to deg_enh_profile for each5289undegeneration map divided by the number of such maps.52905291"""5292if self._leg_dict is None:5293# trivial pullback5294return ELGTautClass(self._X,[(1,self._X.additive_generator(deg_enh_profile))])5295else:5296leg_maps = self._X.explicit_leg_maps(self._enh_profile,deg_enh_profile)5297if leg_maps is None:5298raise RuntimeError ("Pullback failed: %r is not a degeneration of %r")\5299% (deg_enh_profile,self._enh_profile)5300psi_list = []5301aut_factor = QQ(1) / QQ(len(leg_maps))5302for leg_map in leg_maps:5303new_leg_dict = {leg_map[l]:e for l, e in self._leg_dict.items()}5304psi_list.append((aut_factor,self._X.additive_generator(deg_enh_profile,new_leg_dict)))5305return ELGTautClass(self._X,psi_list)53065307def psis_on_level(self, l):5308"""5309The psi classes on level l of self.53105311Args:5312l (int): level, i.e. 0,...,codim53135314Returns:5315dict: psi dictionary on self.level(l).smooth_LG5316"""5317L = self.level(l)5318# The psi classes on this level should be expressed in terms of the legs5319# of the smooth_LG of L:5320EG = L.smooth_LG5321try:5322# Careful: the legs of the smooth_LG are numbered 1,...,n5323# The psi classes are still numbered inside the whole graph5324# The conversion runs through the embedding of the LevelStratum5325# and back through the embedding of smooth_LG (dmp_inv)5326psis = {EG.dmp_inv[L.leg_dict[leg]] : self.leg_dict[leg]5327for leg in self.inv_level_dict[l]}5328except KeyError:5329# no psis on this level5330psis = {}5331return psis53325333def evaluate(self,quiet=False,warnings_only=False,admcycles_output=False):5334"""5335Evaluate self (cap with the fundamental class of self._X).53365337Note that this gives 0 if self is not a top-degree class.53385339Evaluation works by taking the product of the evaluation of each level5340(i.e. evaluating, for each level, the psi monomial on this level) and5341multiplying this with the stack factor.53425343The psi monomials on the levels are evaluated using admcycles (after5344removing residue conditions).53455346Args:5347quiet (bool, optional): No output. Defaults to False.5348warnings_only (bool, optional): Output warnings. Defaults to False.5349admcycles_output (bool, optional): Output admcycles debugging info5350(used when evaluating levels). Defaults to False.53515352Raises:5353RuntimeError: raised if there are inconsistencies with the psi5354degrees on the levels.53555356Returns:5357QQ: integral of self on X.5358"""5359if self.degree < self._X.dim():5360if not quiet or warnings_only:5361print("Warning: %r is not of top degree: %r (instead of %r)" % (self,self.degree,self._X.dim()))5362return 05363level_list = []5364for l in range(self.codim + 1):5365if self.degree_on_level(l) < self.level_dim(l):5366raise RuntimeError("%r is of top degree, but not on level %r" % (self,l))5367L = self.level(l)5368value = L.evaluate(psis=self.psis_on_level(l),quiet=quiet,warnings_only=warnings_only,admcycles_output=admcycles_output)5369if value == 0:5370return 05371level_list.append(value)5372# product over levels:5373prod = 15374for p in level_list:5375prod *= p5376if not quiet:5377print("----------------------------------------------------")5378print("Contribution of Additive generator:")5379print(self)5380print("Product of level-wise integrals: %r" % prod)5381print("Stack factor: %r" % self.stack_factor)5382print("Total: %r" % (prod*self.stack_factor))5383print("----------------------------------------------------")5384return self.stack_factor * prod53855386def to_prodtautclass(self):5387"""5388Transform self into an admcycles prodtautclass on the underlying stgraph of self.53895390Note that this gives the pushforward to M_g,n in the sense that we multiply with5391Strataclass and remove all residue conditions.53925393Returns:5394prodtautclass: the prodtautclass of self, multiplied with the Strataclasses of5395the levels and all residue conditions removed.53965397EXAMPLES ::53985399sage: from admcycles.diffstrata import *5400sage: X=Stratum((2,))5401sage: X.additive_generator(((),0)).to_prodtautclass()5402Outer graph : [2] [[1]] []5403Vertex 0 :5404Graph : [2] [[1]] []5405Polynomial : (-7/24)*(kappa_1^1 )_05406<BLANKLINE>5407<BLANKLINE>5408Vertex 0 :5409Graph : [2] [[1]] []5410Polynomial : 79/24*psi_1^15411<BLANKLINE>5412<BLANKLINE>5413Vertex 0 :5414Graph : [1, 1] [[8], [1, 9]] [(8, 9)]5415Polynomial : (-19/24)*5416<BLANKLINE>5417<BLANKLINE>5418Vertex 0 :5419Graph : [1] [[1, 8, 9]] [(8, 9)]5420Polynomial : (-1/48)*5421sage: from admcycles.stratarecursion import Strataclass5422sage: X=GeneralisedStratum([Signature((4,-2,-2))], res_cond=[[(0,1)], [(0,2)]])5423sage: (X.additive_generator(((),0)).to_prodtautclass().pushforward() - Strataclass(1, 1, [4,-2,-2], res_cond=[2])).is_zero()5424True5425"""5426LG = self._G.LG5427stgraph = LG.stgraph5428if any(self.level(l).zeroStratumClass() for l in range(self.codim + 1)):5429return admcycles.admcycles.prodtautclass(stgraph, terms=[]) # ZERO5430alpha = [] # prodtautclasses on levels5431vertices = [] # embedding of level into stgraph5432for l in range(self.codim + 1):5433# we pass the psis on this level and hope that in-between terms vanish5434# for dimension reasons:5435psis = self.psis_on_level(l)5436T = self.level(l).remove_res_cond(psis) # ELGTautClass on self.level(l) with possibly less RCs5437if T._X.res_cond or len(T.psi_list) > 1:5438alpha.append(T.to_prodtautclass())5439else:5440# Now T is at most a psi product on T._X:5441psis = T.psi_list[0][1].leg_dict5442# self.level(l) has no residue conditions: take prodtautclass of Strataclass5443tautlist = [admcycles.stratarecursion.Strataclass(sig.g, 1, sig.sig) for sig in self.level(l)._sig_list]5444# as it is not ZERO it must be connected!5445assert len(tautlist) == 15446# we have to include the psi contribution:5447stgraph_level = T._X.smooth_LG.LG.stgraph5448adm_psis = admcycles.admcycles.decstratum(stgraph_level, psi=psis)5449adm_psis_taut = admcycles.admcycles.tautclass([adm_psis])5450tautlist = [tautlist[0] * adm_psis_taut]5451ptc = admcycles.admcycles.prodtautclass(stgraph_level, protaut=tautlist)5452alpha.append(ptc)5453# Finally, we save the vertices of this level (as vertices of stgraph)5454vertices.append(LG.verticesonlevel(LG.internal_level_number(l)))5455# now we pull back all the alphas to a prodtautclass on stgraph:5456prod = self.stack_factor * admcycles.admcycles.prodtautclass(stgraph)5457for l, ptc in enumerate(alpha):5458prod = prod.factor_pullback(vertices[l], ptc) # returns product (!)5459return prod54605461#################################################################5462#################################################################5463#################################################################5464#################################################################54655466class ELGTautClass (SageObject):5467"""5468A Tautological class of a stratum X, i.e. a formal sum of of psi classes on5469EmbeddedLevelGraphs.54705471This is encoded by a list of summands.54725473Each summand corresponds to an AdditiveGenerator with coefficient.54745475Thus an ELGTautClass is a list with entries tuples (coefficient, AdditiveGenerator).54765477These can be added, multiplied and reduced (simplified).54785479INPUT :54805481* X : GeneralisedStratum that we are on5482* psi_list : list of tuples (coefficient, AdditiveGenerator) as5483described above.5484* reduce=True : call self.reduce() on initialisation5485"""5486def __init__(self,X,psi_list,reduce=True):5487self._psi_list = psi_list5488self._X = X5489if reduce:5490self.reduce()54915492@classmethod5493def from_hash_list(cls,X,hash_list):5494# This does not reduce!5495return cls(X,[(c, X.additive_generator_from_hash(h)) for c,h in hash_list], reduce=False)54965497@property5498def psi_list(self):5499return self._psi_list55005501def __repr__(self):5502return "ELGTautClass(X=%r,psi_list=%r)"\5503% (self._X,self._psi_list)5504def __str__(self):5505str = "Tautological class on %s\n" % self._X5506for coeff, psi in self._psi_list:5507str += "%s * %s + \n" % (coeff, psi)5508return str55095510def __eq__(self,other):5511if isinstance(other, AdditiveGenerator):5512return self == other.as_taut()5513try:5514return self._psi_list == other._psi_list5515except AttributeError:5516return False55175518def __add__(self,other):5519# for sum, we need to know how to add '0':5520if other == 0:5521return self5522try:5523if not self._X == other._X:5524return NotImplemented5525new_psis = self._psi_list + other._psi_list5526return ELGTautClass(self._X,new_psis)5527except AttributeError:5528return NotImplemented5529def __iadd__(self,other):5530return self.__add__(other)5531def __radd__(self,other):5532return self.__add__(other)55335534def __neg__(self):5535return (-1)*self55365537def __sub__(self,other):5538return self + (-1)*other55395540def __mul__ (self, other):5541if 0 == other:5542return 05543elif self._X.ONE == other:5544return self5545# convert AdditiveGenerators to Tautclasses:5546if isinstance(other, AdditiveGenerator):5547return self * other.as_taut()5548try:5549# check if other is a tautological class5550_other_psi_list = other._psi_list5551except AttributeError:5552# attempt scalar multiplication:5553new_psis = [(coeff * other, psi) for coeff, psi in self._psi_list]5554return ELGTautClass(self._X,new_psis,reduce=False)5555if not self._X == other._X:5556return NotImplemented5557else:5558return self._X.intersection(self,other)5559def __rmul__(self,other):5560return self.__mul__(other)5561def __pow__(self,exponent):5562if exponent == 0:5563return self._X.ONE5564# TODO: quick check for going over top degree?5565prod = self5566for _ in range(1,exponent):5567prod = self * prod5568return prod55695570@cached_method5571def is_equidimensional(self):5572"""5573Determine if all summands of self have the same degree.55745575Note that the empty empty tautological class (ZERO) gives True.55765577Returns:5578bool: True if all AdditiveGenerators in self.psi_list are of same degree,5579False otherwise.5580"""5581if self.psi_list:5582first_deg = self.psi_list[0][1].degree5583return all(AG.degree == first_deg for _c, AG in self.psi_list)5584return True55855586def reduce(self):5587"""5588Reduce self.psi_list by combining summands with the same AdditiveGenerator5589and removing those with coefficient 0 or that die for dimension reasons.5590"""5591# we use the hash of the AdditiveGenerators to group:5592hash_dict = Counter()5593for c, AG in self._psi_list:5594hash_dict[AG] += c5595self._psi_list = [(c, AG) for AG, c in hash_dict.items() if c != 0 and AG.dim_check()]55965597# To evaluate, we go through the AdditiveGenerators and5598# take the (weighted) sum of the AdditiveGenerators.5599def evaluate(self,quiet=True,warnings_only=False,admcycles_output=False):5600"""5601Evaluation of self, i.e. cap with fundamental class of X.56025603This is the sum of the evaluation of the AdditiveGenerators in psi_list5604(weighted with their coefficients).56055606Each AdditiveGenerator is (essentially) the product of its levels,5607each level is (essentially) evaluated by admcycles.56085609Args:5610quiet (bool, optional): No output. Defaults to True.5611warnings_only (bool, optional): Only warnings output. Defaults to False.5612admcycles_output (bool, optional): admcycles debugging output. Defaults to False.56135614Returns:5615QQ: integral of self on X56165617EXAMPLES ::56185619sage: from admcycles.diffstrata import *5620sage: X=GeneralisedStratum([Signature((0,0))])5621sage: assert (X.xi^2).evaluate() == 056225623sage: X=GeneralisedStratum([Signature((1,1,1,1,-6))])5624sage: assert set([(X.cnb(((i,),0),((i,),0))).evaluate() for i in range(len(X.bics))]) == {-2, -1}5625"""5626if warnings_only:5627quiet = True5628DS_list = []5629for c, AG in self.psi_list:5630value = AG.evaluate(quiet=quiet,warnings_only=warnings_only,admcycles_output=admcycles_output)5631DS_list.append(c * value)5632if not quiet:5633print("----------------------------------------------------")5634print("In summary: We sum")5635for i, summand in enumerate(DS_list):5636print("Contribution %r from AdditiveGenerator" % summand)5637print(self.psi_list[i][1])5638print("(With coefficient %r)" % self.psi_list[i][0])5639print("To obtain a total of %r" % sum(DS_list))5640print("----------------------------------------------------")5641return sum(DS_list)56425643def extract(self,i):5644"""5645Return the i-th component of self.56465647Args:5648i (int): index of self._psi_list56495650Returns:5651ELGTautClass: coefficient * AdditiveGenerator at position i of self.5652"""5653return ELGTautClass(self._X,[self._psi_list[i]],reduce=False)56545655@cached_method5656def degree(self, d):5657"""5658The degree d part of self.56595660Args:5661d (int): degree56625663Returns:5664ELGTautClass: degree d part of self5665"""5666new_psis = []5667for c, AG in self.psi_list:5668if AG.degree == d:5669new_psis.append((c, AG))5670return ELGTautClass(self._X, new_psis, reduce=False)56715672@cached_method5673def list_by_degree(self):5674"""5675A list of length X.dim with the degree d part as item d56765677Returns:5678list: list of ELGTautClasses with entry i of degree i.5679"""5680deg_psi_list = [[] for _ in range(self._X.dim() + 1)]5681for c, AG in self.psi_list:5682deg_psi_list[AG.degree].append((c, AG))5683return [ELGTautClass(self._X, piece, reduce=False) for piece in deg_psi_list]56845685def is_pure_psi(self):5686"""5687Check if self is ZERO or a psi-product on the stratum.56885689Returns:5690boolean: True if self has at most one summand and that is of the form5691AdditiveGenerator(((), 0), psis).56925693EXAMPLES ::56945695sage: from admcycles.diffstrata import *5696sage: X=Stratum((2,))5697sage: X.ZERO.is_pure_psi()5698True5699sage: X.ONE.is_pure_psi()5700True5701sage: X.psi(1).is_pure_psi()5702True5703sage: X.xi.is_pure_psi()5704False5705"""5706if not self.psi_list:5707return True5708return len(self.psi_list) == 1 and self.psi_list[0][1].enh_profile == ((), 0)57095710def to_prodtautclass(self):5711"""5712Transforms self into an admcycles prodtautclass on the stable graph of the smooth5713graph of self._X.57145715Note that this is essentially the pushforward to M_g,n, i.e. we resolve residues5716and multiply with the correct Strataclasses along the way.57175718Returns:5719prodtautclass: admcycles prodtautclass corresponding to self pushed forward5720to the stable graph with one vertex.57215722EXAMPLES ::57235724sage: from admcycles.diffstrata import *5725sage: X=Stratum((2,))5726sage: X.ONE.to_prodtautclass()5727Outer graph : [2] [[1]] []5728Vertex 0 :5729Graph : [2] [[1]] []5730Polynomial : (-7/24)*(kappa_1^1 )_05731<BLANKLINE>5732<BLANKLINE>5733Vertex 0 :5734Graph : [2] [[1]] []5735Polynomial : 79/24*psi_1^15736<BLANKLINE>5737<BLANKLINE>5738Vertex 0 :5739Graph : [1, 1] [[6], [1, 7]] [(6, 7)]5740Polynomial : (-19/24)*5741<BLANKLINE>5742<BLANKLINE>5743Vertex 0 :5744Graph : [1] [[1, 6, 7]] [(6, 7)]5745Polynomial : (-1/48)*5746sage: (X.xi^X.dim()).evaluate() == (X.xi^X.dim()).to_prodtautclass().pushforward().evaluate()5747True5748"""5749G = self._X.smooth_LG5750stgraph = G.LG.stgraph5751total = 05752for c, AG in self.psi_list:5753ptc = AG.to_prodtautclass()5754# sort vertices by connected component:5755vertex_map = {}5756# note that every vertex of G has at least one leg (that is a marked point of _X):5757for v, _ in enumerate(G.LG.genera):5758mp_on_stratum = G.dmp[G.LG.legs[v][0]]5759# find this marked point on AG:5760leg_on_ag = AG._G.dmp_inv[mp_on_stratum]5761LG = AG._G.LG5762# we use the underlying graph:5763UG_v = LG.UG_vertex(v)5764for w, g, kind in LG.underlying_graph.connected_component_containing_vertex(UG_v):5765if kind != 'LG':5766continue5767vertex_map[w] = v5768# map legs of AG._G to smooth_LG5769# CAREFUL: This goes in the OTHER direction!5770leg_map = {G.dmp_inv[mp] : ldeg for ldeg, mp in AG._G.dmp.items()}5771pf = ptc.partial_pushforward(stgraph, vertex_map, leg_map)5772total += c * pf5773return total57745775#################################################################5776#################################################################5777#################################################################5778#################################################################57795780#################################################################5781#################################################################5782### Auxillary functions:5783#################################################################5784#################################################################57855786def unite_embedded_graphs(gen_LGs):5787"""5788Create a (disconnected) EmbeddedLevelGraph from a tuple of tuples that generate EmbeddedLevelGraphs.57895790(The name is slightly misleading, but of course it does not make sense to actually unite two complete5791EmbeddedLevelGraphs, as the checks would (and do!) throw errors otherwise! Therefore, this essentially5792takes the data of a LevelGraph embedded into each connected componenent of a GeneralisedStratum and5793returns an EmbeddedLevelGraph on the product.)57945795This should be used on (products) of BICs in generalised strata.57965797Args:5798gen_LGs (tuple): tuple of tuples that generate EmbeddedLevelGraphs.5799More precisely, each tuple is of the form:5800* X (GeneralisedStratum): Enveloping stratum (should be the same for all tuples!)5801* LG (LevelGraph): Underlying LevelGraph5802* dmp (dict): (partial) dictionary of marked points5803* dlevels (dict): (partial) dictionary of levels58045805Returns:5806EmbeddedLevelGraph: The (disconnected) LevelGraph obtained from the input with5807the legs renumbered (continuously, starting with 1), and the levels numbered5808according to the embedding.5809"""5810newgenera = []5811newlevels = []5812newlegs = []5813newpoleorders = {}5814newedges = []5815newdmp = {}5816newdlevels = {}5817max_leg_number = 05818oldX = gen_LGs[0][0] # for check that all belong to the same stratum:5819for emb_g in gen_LGs:5820# Unpack tuple:5821X, LG, dmp, dlevels = emb_g5822if X != oldX:5823raise RuntimeError("Can't unite graphs on different Strata! %r" % gen_LGs)5824# the genera are just appended5825newgenera += LG.genera5826# same for the levels, but while we're at it, we might just as well5827# replace them by their embedding (then newdlevels will be trivial)5828# and these should be consistens for all graphs in the tuple.5829# Thus, newdlevels will be the identity.5830newlevels += [dlevels[l] for l in LG.levels]5831# the legs will have to be renumbered5832leg_dict = {} # old number -> new number5833legs = 05834for i, l in enumerate(flatten(LG.legs)):5835newlegnumber = max_leg_number + i + 15836leg_dict[l] = newlegnumber5837# while we're at it, we add the pole orders:5838newpoleorders[newlegnumber] = LG.poleorders[l]5839# For the dictionary of marked points (for the embedding), we5840# must distinguish if this is a marked point or a half-edge.5841# Marked points are simply the ones for which we have a key5842# in dmp :-)5843try:5844newdmp[newlegnumber] = dmp[l]5845except KeyError:5846pass5847legs += 15848max_leg_number += legs5849# append (nested) list of legs:5850newlegs += [[leg_dict[l] for l in comp] for comp in LG.legs]5851# finally, the edges are renumbered accordingly:5852newedges += [(leg_dict[e[0]], leg_dict[e[1]]) for e in LG.edges]5853# the levels are already numbered according to the embedding dict5854newdlevels = {l:l for l in newlevels}5855newLG = admcycles.diffstrata.levelgraph.LevelGraph(5856newgenera, newlegs, newedges, newpoleorders, newlevels5857)5858return EmbeddedLevelGraph(X, newLG, newdmp, newdlevels)58595860def sort_with_dict(l):5861"""5862Sort a list and provide a dictionary relating old and new indices.58635864If x had index i in l, then x has index sorted_dict[i] in the sorted l.58655866Args:5867l (list): List to be sorted.58685869Returns:5870tuple: A tuple consisting of:5871list: The sorted list l.5872dict: A dictionary old index -> new index.5873"""5874sorted_list = []5875sorted_dict = {}5876for i,(j,v) in enumerate(sorted(enumerate(l),key=lambda w:w[1])):5877sorted_list.append(v)5878sorted_dict[j] = i5879return sorted_list, sorted_dict58805881def get_squished_level(deg_ep,ep):5882"""5883Get the (relative) level number of the level squished in ep.58845885This is the index of the corresponding BIC in the profile.58865887Args:5888deg_ep (tuple): enhanced profile5889ep (tuple): enhanced profile58905891Raises:5892RuntimeError: raised if deg_ep is not a degeneration of ep58935894Returns:5895int: relative level number5896"""5897deg_p = deg_ep[0]5898p = set(ep[0])5899for i, b in enumerate(deg_p):5900if not b in p:5901break5902else:5903raise RuntimeError("%r is not a degeneration of %r!" % (deg_ep, p))5904return i59055906def _graph_word(n):5907if n == 1:5908return "graph"5909else:5910return "graphs"59115912#################################################################5913#################################################################5914### Auxillary functions for caching:5915#################################################################5916#################################################################59175918def hash_AG(leg_dict, enh_profile):5919"""5920The hash of an AdditiveGenerator, built from the psis and the enhanced profile.59215922The hash-tuple is (leg-tuple,profile,index), where profile is5923changed to a tuple and leg-tuple is a nested tuple consisting of5924tuples (leg,exponent) (or None).59255926Args:5927leg_dict (dict): dictioary for psi powers (leg -> exponent)5928enh_profile (tuple): enhanced profile59295930Returns:5931tuple: nested tuple5932"""5933if leg_dict is None:5934leg_hash = ()5935else:5936leg_hash = tuple(sorted(leg_dict.items()))5937return (leg_hash,tuple(enh_profile[0]),enh_profile[1])59385939def adm_key(sig, psis):5940"""5941The hash of a psi monomial on a connected stratum without residue conditions.59425943This is used for caching the values computed using admcycles (using5944GeneralisedStratum.adm_evaluate)59455946The signature is sorted, the psis are renumbered accordingly and also5947sorted (with the aim of computing as few duplicates as possible).59485949Args:5950sig (tuple): signature tuple5951psis (dict): psi dictionary59525953Returns:5954tuple: nested tuple5955"""5956sorted_psis = {}5957sorted_sig = []5958psi_by_order = defaultdict(list)5959# sort signature and relabel psis accordingly:5960# NOTE: Psis are labelled 'mathematically', i.e. 1,...,len(sig)5961for new_i, (old_i, order) in enumerate(sorted(enumerate(sig), key=lambda k: k[1])):5962psi_new_i = new_i + 15963psi_old_i = old_i + 15964sorted_sig.append(order)5965if psi_old_i in psis:5966assert not (psi_new_i in sorted_psis)5967psi_exp = psis[psi_old_i]5968sorted_psis[psi_new_i] = psi_exp5969psi_by_order[order].append(psi_exp)5970# sort psis for points of same order:5971ordered_sorted_psis = {}5972i = 05973assert len(sig) == len(sorted_sig)5974while i < len(sig):5975order = sorted_sig[i]5976for j, psi_exp in enumerate(sorted(psi_by_order[order])):5977assert sorted_sig[i+j] == order5978ordered_sorted_psis[i+j+1] = psi_exp5979while i < len(sig) and sorted_sig[i] == order:5980i += 15981return (tuple(sorted_sig), tuple(sorted(ordered_sorted_psis.items())))598259835984