unlisted
ubuntu2004# -*- coding: utf-8 -*-12import itertools3import sys4from sympy.utilities.iterables import partitions56# pylint does not know sage7from sage.structure.sage_object import SageObject # pylint: disable=import-error8from sage.matrix.constructor import matrix # pylint: disable=import-error9from sage.misc.flatten import flatten # pylint: disable=import-error10from sage.misc.cachefunc import cached_method # pylint: disable=import-error11from sage.rings.rational_field import QQ # pylint: disable=import-error12from sage.functions.other import factorial # pylint: disable=import-error13from sage.combinat.integer_vector_weighted import WeightedIntegerVectors # pylint: disable=import-error14from sage.functions.other import binomial # pylint: disable=import-error15from sage.symbolic.constants import pi, I # pylint: disable=import-error16from sage.misc.misc_c import prod # pylint: disable=import-error1718from copy import deepcopy1920import admcycles.admcycles21import admcycles.stratarecursion2223import admcycles.diffstrata.levelgraph24import admcycles.diffstrata.bic25import admcycles.diffstrata.sig26import admcycles.diffstrata.stratatautring27import admcycles.diffstrata.embeddedlevelgraph28import admcycles.diffstrata.additivegenerator29import admcycles.diffstrata.elgtautclass3031from admcycles.diffstrata.auxiliary import (hash_AG, unite_embedded_graphs,32adm_key, get_squished_level)3334#######################################################################35#######################################################################36# Recursive Calculations and Degeneration Graphs37#######################################################################38# The idea is to do all calculations recursively.39# In particular, the Degeneration Graph is itself a recursive object.40##41# The key observation is that:42# * Each 3-level graph arises by either clutching a top component of43# a BIC to a BIC of its bottom component of a BIC of the top44# component to the bottom component.45# * On the other hand, each 3-level graph is the intersection of46# two (different) BICs of the Stratum.47# * Therefore, for each BIC B of the Stratum, every BIC Bt in the top48# component corresponds to a unique BIC B' of the stratum, so49# that the 3-level graph (Bt clutched to the bottom component of B)50# is B*B' (i.e. delta_1 of this graph is B', delta_2 is B).51# The same is true for the BICs of the bottom component.52# * We thus obtain two maps, for each BIC B of the stratum:53# * top_to_bic mapping BICs of the top component to BICs of54# the stratum55# * bot_to_bic mapping BICs of the bottom component to BICs of56# the stratum57# * These maps have disjoint images.58# * These maps fail to be embeddings precisely when the intersection59# of two BICs is not irreducible (i.e. clutching different BICs60# to a component results in the intersection with the same divisor)61# or when there are automorphisms involved (i.e. several ways of62# undegenerating to the same BIC).63# We can thereby express the clutching of a product of BICs in the top64# and bottom components of a BIC in our stratum as a product of BICs of65# our stratum. Hence the procedure is recursive.66##67# Therefore, the GenDegenerationGraph needs to remember only the BICs68# together with, for each BIC, top and bottom components and the two maps.69##70# More precisely, the Degeneration Graph of a GeneralisedStratum71# consists of the following information:72# * The BICs inside the Stratum.73# * For each BIC, its top and bottom component (GeneralisedStratum74# together with a dictionary Stratum points -> LevelGraph points)75# * For each BIC, the BICs of its top and bottom component, together76# with the maps top_to_bic and bot_to_bic.77##78# We can now calculate the GenDegenerationGraph:79# * Step 1: Calculate all BICs in a GeneralisedStratum.80# * Step 2: Separate these into top an bottom component.81# * Step 3: Calculate all BICs in every top and bottom component.82# * Step 4: Calculate top_to_bic and bot_to_bic for each BIC in the83# Stratum (as dictionaries: index of BIC in top/bottom ->84# index of BIC in stratum)85##86# In particular, we this implies the following recursive algorithm for87# the EmbeddedLevelGraph of an arbitrary product of BICs in the stratum:88# INPUT: Product of BICs.89# OUTPUT: EmbeddedLevelGraph.90# * Step 1: Choose a BIC B from the product (e.g. the first).91# * Step 2: Find the preimages of the other BICs in the product under92# top_to_bic and bot_to_bic of B.93# * This gives (possibly multiple) products of BICs in the top and bottom94# stratum of B.95# * Step 3: Apply to product in top an bottom to get two EmbeddedLevelGraphs96# * Step 4: Return the clutching of the top and bottom graph.97##98# Moreover, we can generate the "lookup list", consisting of the non-empty99# products of BICs in each stratum.100# For this, we record all intersections that give 3-level graphs in each101# GenDegenerationGraph (i.e. (2,1) means that there exists a 3-level graph102# C such that delta(1) of C is bics[2] and delta(2) of C is bics[1]).103# Note that this is equivalent to 2 being in the image of top_to_bic(1).104##105# The key observation here is that any profile (i_1,...,i_n) can be106# written as a "domino" of 3-level graphs, i.e. (i_1,i_2)(i_2,_3)...(i_n-1,i_n).107##108# However, for the recursive generation of the lookup list, it is enough109# to take a profile and add the top generations of the first bic and the110# bottom degenerations of the last bic to obtain a profile with length111# increased by one (see the implementation below for more details.)112##113#######################################################################114#######################################################################115116117class 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,125* res_cond : list of residue conditions, i.e. [R_1,...,R_n] where each R_l is126a list of tuples (i,j), corresponding to the j-th component of sig_i, that127share a residue condition (i.e. the residues at these poles add up to 0).128129Note that the residue theorem for each component will be added automatically.130"""131def __init__(self, sig_list, res_cond=None):132assert all(sig.k == 1 for sig in sig_list)133self._h0 = len(sig_list)134self._sig_list = sig_list135self._n = sum([sig.n for sig in sig_list]) # total number of points136self._g = [sig.g for sig in sig_list]137# remember poles as (i,j) where i is the component and j is the index138# in sig_i139self._polelist = [(i, j) for i, sig in enumerate(sig_list)140for j in sig.pole_ind]141self._p = len(self._polelist)142if res_cond is None:143res_cond = []144self._res_cond = res_cond145self.init_more()146147def init_more(self):148self._bics = None149self._smooth_LG = None150self._all_graphs = None151self._lookup_list = None152self._lookup = {}153self._DG = None154# cache AdditiveGenerators:155self._AGs = {}156# tautological class of self:157self._ONE = None158# tautological class of zero:159self._ZERO = None160161def __repr__(self):162return "GeneralisedStratum(sig_list=%r,res_cond=%r)" % (163self._sig_list, self._res_cond)164165def __str__(self):166rep = ''167if self._h0 > 1:168rep += 'Product of Strata:\n'169else:170rep += 'Stratum: '171for sig in self._sig_list:172rep += repr(sig.sig) + '\n'173rep += 'with residue conditions: '174if not self._res_cond:175rep += repr([]) + '\n'176for res in self._res_cond:177rep += repr(res) + '\n'178return rep179180def info(self):181"""182Print facts about self.183184This calculates everything, so could take long(!)185186EXAMPLES::187188sage: from admcycles.diffstrata import *189sage: X=GeneralisedStratum([Signature((1,1))])190sage: X.info()191Stratum: (1, 1)192with residue conditions: []193Genus: [2]194Dimension: 4195Boundary Graphs (without horizontal edges):196Codimension 0: 1 graph197Codimension 1: 4 graphs198Codimension 2: 4 graphs199Codimension 3: 1 graph200Total graphs: 10201202sage: X=GeneralisedStratum([Signature((4,))])203sage: X.info()204Stratum: (4,)205with residue conditions: []206Genus: [3]207Dimension: 5208Boundary Graphs (without horizontal edges):209Codimension 0: 1 graph210Codimension 1: 8 graphs211Codimension 2: 19 graphs212Codimension 3: 16 graphs213Codimension 4: 4 graphs214Total graphs: 48215"""216def _graph_word(n):217if n == 1:218return "graph"219else:220return "graphs"221print(self)222print("Genus: %s" % self._g)223print("Dimension: %s" % self.dim())224print("Boundary Graphs (without horizontal edges):")225tot = 0226for c, graphs in enumerate(self.all_graphs):227n = len(graphs)228print("Codimension %s: %s %s" % (c, n, _graph_word(n)))229tot += n230print("Total graphs: %s" % tot)231232def additive_generator(self, enh_profile, leg_dict=None):233"""234The AdditiveGenerator for the psi-polynomial given by leg_dict on enh_profile.235236For example, if psi_2 is the psi-class at leg 2 of enh_profile,237the polynomial psi_2^3 would be encoded by the leg_dict {2 : 3}.238239This method should always be used instead of generating AdditiveGenerators240directly, as the objects are cached here, i.e. the _same_ object is returned241on every call.242243INPUT:244245enh_profile: tuple246The enhanced profile247248leg_dict: dict (optional)249A dictionary mapping legs of the underlying250graph of enh_profile to positive integers, corresponding to251the power of the psi class associated to this leg. Defaults to None.252253OUTPUT:254255The AdditiveGenerator256257EXAMPLES::258259sage: from admcycles.diffstrata import *260sage: X=Stratum((2,))261sage: a = X.additive_generator(((0,),0))262sage: a is X.additive_generator(((0,),0))263True264sage: a is AdditiveGenerator(X,((0,),0))265False266"""267ag_hash = hash_AG(leg_dict, enh_profile)268return self.additive_generator_from_hash(ag_hash)269270def additive_generator_from_hash(self, ag_hash):271if ag_hash not in self._AGs:272self._AGs[ag_hash] = admcycles.diffstrata.additivegenerator.AdditiveGenerator.from_hash(273self, ag_hash)274return self._AGs[ag_hash]275276def simple_poles(self):277"""278Return an iterator over simple poles.279280EXAMPLES::281282sage: from admcycles.diffstrata import *283sage: X = GeneralisedStratum([Signature((1,1))])284sage: list(X.simple_poles())285[]286"""287for p in self._polelist:288if self.stratum_point_order(p) == -1:289yield p290291@cached_method292def is_empty(self):293"""294Checks if self fails to exist for residue reasons (simple pole with residue forced zero).295296Returns:297bool: existence of simple pole with residue zero.298299EXAMPLES::300301sage: from admcycles.diffstrata import *302sage: X = Stratum((1,-1))303sage: X.is_empty()304True305sage: X = Stratum((1,1))306sage: X.is_empty()307False308"""309return any(self.smooth_LG.residue_zero(p)310for p in self.simple_poles())311312def is_disconnected(self):313"""314Return whether self is not connected.315316EXAMPLES::317318sage: from admcycles.diffstrata import *319sage: X = Stratum((1,1))320sage: X.is_disconnected()321False322sage: X = GeneralisedStratum([Signature((5,1)),Signature((1,3))])323sage: X.is_disconnected()324True325"""326return self._h0 > 1327328def stratum_point_order(self, p):329"""330The pole order at the stratum point p.331332Args:333p (tuple): Point (i,j) of self.334335Returns:336int: Pole order of p.337"""338i, j = p339return self._sig_list[i].sig[j]340341@property342def bics(self):343"""344Initialise BIC list on first call.345346Note that _bics is a list of tuples of EmbeddedLevelGraphs347(each tuple consists of one EmbeddedLevelGraph for every348connected component).349"""350if self.is_empty():351return []352if self._bics is None:353return self.gen_bic()354return self._bics355356@property357def res_cond(self):358return self._res_cond359360@property361def lookup_list(self):362"""363The list of all (ordered) profiles.364365Note that starting with SAGE 9.0 profile numbering is no longer deterministic.366367Note that this generates all graphs and can take a long time for large strata!368369Returns:370list: Nested list of tuples.371372EXAMPLES::373374sage: from admcycles.diffstrata import *375sage: X=Stratum((2,))376sage: assert len(X.lookup_list) == 3377sage: X.lookup_list[0]378[()]379sage: X.lookup_list[1]380[(0,), (1,)]381sage: assert len(X.lookup_list[2]) == 1382"""383if self.is_empty():384return []385if self._lookup_list is None:386# First, we build the "lookup-list", i.e. the list of all profiles:387# the non-empty profiles can be found recursively:388# given a profile, we create new profiles by adding top and bottom389# degenerations of the corresponding bic to the beginning and end.390self._lookup_list = [[tuple()]] # only one with 0 levels391n = len(self.bics)392self._lookup_list += [[(i,) for i in range(n)]] # bics393new_profiles = n394while new_profiles:395# we temporarily work with a set to avoid duplicates396self._lookup_list.append(set())397for profile in self._lookup_list[-2]:398first = profile[0]399for i in self.DG.top_to_bic(first).values():400self._lookup_list[-1].add((i,) + profile)401if len(profile) > 1:402last = profile[-1]403for i in self.DG.bot_to_bic(last).values():404self._lookup_list[-1].add(profile + (i,))405self._lookup_list[-1] = list(self._lookup_list[-1])406new_profiles = len(self._lookup_list[-1])407self._lookup_list.pop()408return self._lookup_list409410@property411def DG(self):412assert not self.is_empty()413if self._DG is None:414self._DG = admcycles.diffstrata.stratatautring.GenDegenerationGraph(415self)416return self._DG417418@property419def ONE(self):420if self._ONE is None:421one = self.additive_generator((tuple(), 0))422self._ONE = one.as_taut()423return self._ONE424425@property426def ZERO(self):427if self._ZERO is None:428self._ZERO = admcycles.diffstrata.elgtautclass.ELGTautClass(self, [429])430return self._ZERO431432@property433def all_graphs(self):434"""435Nested list of all EmbeddedLevelGraphs in self.436437This list is built on first call.438439EXAMPLES::440441sage: from admcycles.diffstrata import *442sage: X=GeneralisedStratum([Signature((1,1))])443sage: 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})])444sage: assert comp_list(X.all_graphs[1], \445[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}),\446EmbeddedLevelGraph(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}),\447EmbeddedLevelGraph(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}),\448EmbeddedLevelGraph(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})])449sage: assert comp_list(X.all_graphs[2],\450[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}),\451EmbeddedLevelGraph(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}),\452EmbeddedLevelGraph(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}),\453EmbeddedLevelGraph(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})])454sage: 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})])455"""456if self.is_empty():457return []458if self._all_graphs is None:459# We build the graph list from the lookup list:460# Note that lookup returns a list of graphs.461self._all_graphs = []462for l in self.lookup_list:463self._all_graphs.append(464list(itertools.chain.from_iterable(self.lookup(g)465for g in l))466)467# Ensure that degenerations of top and bottom match up:468assert all(k in self.DG.bot_to_bic(j).values()469for k in range(self.DG.n)470for j in self.DG.top_to_bic(k).values())471return self._all_graphs472473@property474def smooth_LG(self):475"""476The smooth EmbeddedLevelGraph inside a LevelStratum.477478Note that the graph might be disconnected!479480EXAMPLES::481482sage: from admcycles.diffstrata import *483sage: X=GeneralisedStratum([Signature((1,1))])484sage: 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}))485486Note that we get a single disconnected graph if the stratum is487disconnected.488489sage: X=GeneralisedStratum([Signature((0,)), Signature((0,))])490sage: X.smooth_LG491EmbeddedLevelGraph(LG=LevelGraph([1, 1],[[1], [2]],[],{1: 0, 2: 0},[0, 0],True),dmp={1: (0, 0), 2: (1, 0)},dlevels={0: 0})492493Returns:494EmbeddedLevelGraph: The output of unite_embedded_graphs applied to495the (embedded) smooth_LGs of each component of self.496"""497if not self._smooth_LG:498graph_list = []499for i, sig in enumerate(self._sig_list):500g = admcycles.diffstrata.levelgraph.smooth_LG(sig)501dmp = {j: (i, j - 1) for j in range(1, sig.n + 1)}502graph_list.append((self, g, dmp, {0: 0}))503self._smooth_LG = unite_embedded_graphs(tuple(graph_list))504return self._smooth_LG505506@cached_method507def residue_matrix(self):508"""509Calculate the matrix associated to the residue space,510i.e. a matrix with a line for every residue condition and a column for every pole of self.511512The residue conditions consist ONLY of the ones coming from the GRC (in _res_cond)513For inclusion of the residue theorem on each component, use smooth_LG.full_residue_matrix!514"""515return self.matrix_from_res_conditions(self._res_cond)516517def matrix_from_res_conditions(self, res_conds):518"""519Calculate the matrix for a list of residue conditions, i.e.520a matrix with one line for every residue condition and a column for each pole of self.521522Args:523res_conds (list): list of residue conditions, i.e. a nested list R524R = [R_1,R_2,...] where each R_i is a list of poles (stratum points)525whose residues add up to zero.526527Returns:528SAGE matrix: residue matrix (with QQ coefficients)529530EXAMPLES::531532sage: from admcycles.diffstrata import *533sage: X=GeneralisedStratum([Signature((2,-2,-2)),Signature((2,-2,-2))])534sage: X.matrix_from_res_conditions([[(0,1),(0,2),(1,2)],[(0,1),(1,1)],[(1,1),(1,2)]])535[1 1 0 1]536[1 0 1 0]537[0 0 1 1]538"""539res_vec = []540for res_c in res_conds:541# note: int(True)=1, int(False)=0542res_vec += [[int(p in res_c) for p in self._polelist]]543return matrix(QQ, res_vec)544545@cached_method546def residue_matrix_rank(self):547return self.residue_matrix().rank()548549@cached_method550def dim(self):551"""552Return the dimension of the stratum, that is the sum of 2g_i + n_i - 1 - residue conditions -1 for projective.553554The residue conditions are given by the rank of the (full!) residue matrix.555556Empty strata return -1.557558EXAMPLES::559560sage: from admcycles.diffstrata import *561sage: X=GeneralisedStratum([Signature((4,))])562sage: all(B.top.dim() + B.bot.dim() == X.dim()-1 for B in X.bics)563True564"""565if self.is_empty():566return -1567# add residue conditions from RT for every connected component:568M = self.smooth_LG.full_residue_matrix569return (sum([2 * sig.g + sig.n - 1 for sig in self._sig_list])570- M.rank() - 1)571572@property573def N(self):574"""575Unprojectivised dimension of self.576577Returns:578int: dim() + 1579"""580return self.dim() + 1581582def gen_bic(self):583"""584Generates all BICs (using bic) as EmbeddedLevelGraphs.585586Returns:587list: self._bics i.e. a list of (possibly disconnected)588EmbeddedLevelGraphs.589(More precisely, each one is a tuple consisting of one590EmbeddedLevelGraph for every connected component that has591been fed to unite_embedded_graphs).592"""593self._bics = []594if self.is_empty():595return596# The BICs are the products of BICs for each connected component597# (satisfying the residue condition).598# Moreover, if there are several connected components, we also need599# to include the smooth stratum on each level.600emb_bic_list = []601602# First, we establish the dictionaries for the EmbeddedLevelGraphs:603# * the marked points of the stratum are numbered (i,j) where (i,j)604# is the j-th point on the i-th connected component.605# Note that j is the index in sig, i.e. starts at 0.606# * on each BIC, the i-th point of the signature is the point i+1607# mp_dict maps the points on the BIC to the points of the stratum608for i, sig in enumerate(self._sig_list):609mp_dict = {j: (i, j - 1) for j in range(1, sig.n + 1)}610# We can't build the EmbeddedLevelGraph until we have the data for all611# components (otherwise we mess up the legality check, etc.)612# So for now, we just store the generating info for each connected613# component separately.614emb_bic_list_cur = []615for g in admcycles.diffstrata.bic.bic_alt_noiso(sig.sig):616level_dict = {g.internal_level_number(0): 0,617g.internal_level_number(-1): -1}618EG = (self, g, mp_dict, level_dict)619emb_bic_list_cur.append(EG)620if self._h0 > 1:621# we need the smooth component on each level622for l in [0, -1]:623emb_bic_list_cur.append((self, admcycles.diffstrata.levelgraph.smooth_LG(sig), mp_dict,624{0: l}, # one for each level625)626)627emb_bic_list.append(emb_bic_list_cur)628# The elements of _bics are now products of the (embedded) bics of the components629# Careful: The only ones that are not allowed are those, where all630# components are on one level!!631prod_bics = itertools.product(*emb_bic_list)632for prod_graph in prod_bics:633# levels are stored in values of the dict in component 3 of each634# tuple:635if any(0 in g[3].values() for g in prod_graph) and \636any(-1 in g[3].values() for g in prod_graph):637# NOTE: This actually builds the EmbeddedLevelGraphs!638pg = unite_embedded_graphs(prod_graph)639if pg.is_legal(): # check r-GRC640self._bics.append(pg)641# isomorphism classes: (possibly figure out how to check earlier?)642self._bics = admcycles.diffstrata.bic.isom_rep(self._bics)643return self._bics644645# Ideally, we could always work with enhanced profiles, never with graphs.646# Then edge maps could work like this:647# Def: A leg is a tuple consisting of:648# * an enhanced profile (of a levelgraph)649# * the levelstratum inside the profile (e.g. for the profile (1,2,3) this would650# be either 1^top, 3^bot, (12) or (23)). These were computed for the stratum anyway.651# * an orbit of a marked point of this gen levelstratum, which corresponds to an edge652# of the corresponding graph653# i.e. an ordered tuple of marked points equivalent by automorphisms of the corresponding654# BIC or 3-level graph (which should be also an automorphism of the full graph??!!)655##656# Then:657# INPUT: (leg, enhanced profile)658# The enhanced profile should be that of a degeneration of the graph of leg (!)659##660# OUTPUT: leg (on the second profile)661##662# Case 1:663# The levelstratum of the leg is unchanged by the degeneration.664# i.e.: (1,2) and (1,2,3) for an edge on (1,2).665# In this case the output is trivially the same edge embedded into (1,2,3)666# (because (1,2) is still a level of (1,2,3)).667##668# Case 2:669# The levelstratum is degenerated,670# i.e.: (1,2) and (1,3,2) for an leg e on (1,2).671# In this case we know that e (by checking the sign of the order) is either672# a leg on 1^bot or 2^top and the degeneration is given by top_to_bic_inv (or673# bot_to_bic_inv) of 3, where we can then track the marked point associated to e.674####675676# TODO: This should work "smarter", see above.677@cached_method678def explicit_leg_maps(self, enh_profile, enh_deg_profile, only_one=False):679"""680Provide explicit leg maps (as list of dictionaries: legs of LG to legs of LG), from681the graph associated to enh_profile to the one associated to enh_deg_profile.682683If enh_deg_profile is not a degeneration (on the level of profiles), None is684returned.685686Raises a RuntimeError if enh_profile is empty and a UserWarning if687there are no degenerations in the appropriate profile.688689INPUT:690691enh_profile (enhanced profile): tuple (profile, index).692693enh_deg_profile (enhanced profile): tuple (profile, index).694695only_one (bool, optional): Give only one (the 'first') map (or None if none exist).696Defaults to False.697698OUTPUT:699700list of dicts: List of the leg isomorphisms, None if not a degeneration,701only one dict if only_one=True.702"""703profile = enh_profile[0]704deg_profile = enh_deg_profile[0]705# check if deg_profile is actually a (profile) degeneration:706if not set(profile).issubset(set(deg_profile)):707return None708g = self.lookup_graph(*enh_profile)709degen = self.lookup_graph(*enh_deg_profile)710if not degen:711raise RuntimeError("%r is not a graph in %r!" %712(enh_deg_profile, self))713# To obtain g, we have to squish degen at the places in the profile714# of degen that are not in the profile of g.715# We work from right to left to avoid confusion with the level716# numbering.717degen_squish = degen718for level, bic_index in list(enumerate(deg_profile))[::-1]:719if bic_index in profile:720continue721degen_squish = degen_squish.squish_vertical(level)722isoms = (l_iso for v_iso, l_iso in g.isomorphisms(degen_squish))723try:724first_isom = next(isoms)725except StopIteration:726# No isomorphisms found727raise UserWarning("Squish of %r not isomorphic to %r!" %728(enh_deg_profile, enh_profile))729if only_one:730return first_isom731else:732return [first_isom] + list(isoms)733734#####735# Common degenerations:736# This should eat two graphs, given by their "enhanced profile" i.e. things we can737# feed to graph_lookup (a list of BICs and an index) and also return a list of738# enhanced profiles.739740# Naive approach:741# do a (set-wise) degeneration of the profile and just go through the list742# checking which ones are actually degenerations:743# INPUT: Profile + index744# OUTPUT: List of profiles + indices745746# TODO: There should be a smart way! For that one has to understand747# how to correctly encode the irreducible components of the profiles.748749@cached_method750def common_degenerations(self, s_enh_profile, o_enh_profile):751"""752Find common degenerations of two graphs.753754INPUT:755756s_enh_profile (tuple): Enhanced profile, i.e. a tuple (p,k) consisting of757758* a sorted (!) profile p759* an index in self.lookup(p)760thus giving the information of an EmbeddedLevelGraph in self.761762o_enh_profile (tuple): Enhanced profile.763764OUTPUT:765766A list of enhanced profiles, i.e. entries of type [tuple profile, index]767(that undegenerate to the two given graphs).768769EXAMPLES::770771sage: from admcycles.diffstrata import *772sage: X=GeneralisedStratum([Signature((4,))])773774To retrieve the actual EmbeddedLevelGraphs, we must use lookup_graph.775(Because of BIC renumbering between different SAGE versions we can't provide any concrete examples :/)776777Note that the number of components can also go down.778779Providing common graphs works::780781sage: X.common_degenerations(((2,),0),((2,),0))782[((2,), 0)]783784Empty intersection gives empty list.785786"""787s_profile = s_enh_profile[0]788o_profile = o_enh_profile[0]789try:790# merge_profiles returns None if there aren't any...791deg_profile = tuple(self.merge_profiles(s_profile, o_profile))792except TypeError:793return []794return_list = []795# careful with reducible profiles:796for i in range(len(self.lookup(deg_profile))):797if self.is_degeneration(798(deg_profile, i), s_enh_profile) and self.is_degeneration(799(deg_profile, i), o_enh_profile):800return_list.append((deg_profile, i))801return return_list802803# Excess intersection of two additive generators in an ambient graph804def intersection(self, s_taut_class, o_taut_class, amb_enh_profile=None):805"""806Excess intersection of two tautological classes in Chow of ambient_enh_profile.807808Raises a RuntimeError if any summand of any tautological class is not on809a degeneration of ambient_enh_profile.810811INPUT:812813s_taut_class (ELGTautClass): tautological class814815o_taut_class (ELGTautClass): tautological class816817amb_enh_profile (tuple, optional): enhanced profile of ambient graph.818Defaults to None.819820OUTPUT:821822ELGTautClass: Tautological class on common degenerations823"""824# check input:825if amb_enh_profile is None:826amb_enh_profile = ((), 0)827if s_taut_class == 0 or s_taut_class == self.ZERO:828return self.ZERO829if s_taut_class == 1 or s_taut_class == self.ONE:830return o_taut_class831if o_taut_class == 0 or o_taut_class == self.ZERO:832return self.ZERO833if o_taut_class == 1 or o_taut_class == self.ONE:834return s_taut_class835return_list = []836# unpack tautological classes:837for s_coeff, s_add_gen in s_taut_class.psi_list:838for o_coeff, o_add_gen in o_taut_class.psi_list:839prod = self.intersection_AG(840s_add_gen, o_add_gen, amb_enh_profile)841if prod == 0 or prod == self.ZERO:842continue843return_list.append(s_coeff * o_coeff * prod)844return_value = self.ELGsum(return_list)845if return_value == 0:846return self.ZERO847if s_taut_class.is_equidimensional() and o_taut_class.is_equidimensional():848assert return_value.is_equidimensional(),\849"Product of equidimensional classes is not equidimensional! %s * %s = %s"\850% (s_taut_class, o_taut_class, return_value)851return return_value852853@cached_method854def intersection_AG(self, s_add_gen, o_add_gen, amb_enh_profile=None):855"""856Excess intersection formula for two AdditiveGenerators in Chow of amb_enh_profile.857858Note that as AdditiveGenerators and enhanced profiles are hashable,859this method can (and will) be cached (in contrast with intersection).860861Raises a RuntimeError if any of the AdditiveGenerators is not on862a degeneration of ambient_enh_profile.863864INPUT:865866s_add_gen (AdditiveGenerator): first AG867o_add_gen (AdditiveGenerator): second AG868amb_enh_profile (tuple, optional): enhanced profile of ambient graph.869Defaults to None.870871OUTPUT:872873A Tautological class on common degenerations874"""875if amb_enh_profile is None:876amb_enh_profile = ((), 0)877s_enh_profile = s_add_gen.enh_profile878o_enh_profile = o_add_gen.enh_profile879if not self.is_degeneration(s_enh_profile, amb_enh_profile):880raise RuntimeError("%r is not a degeneration of %r" %881(s_enh_profile, amb_enh_profile))882if not self.is_degeneration(o_enh_profile, amb_enh_profile):883raise RuntimeError("%r is not a degeneration of %r" %884(o_enh_profile, amb_enh_profile))885# Degree check:886# * the degree of the product is the sum of the degrees887# * the product is supported on a set of codim >= max(codim(s),codim(o))888# => if the sum of the degrees is > (dim(self) - max(codim(s),codim(o)))889# the product will be 0 in any case890# NOTE: degree = codim + psi-degree891deg_sum = s_add_gen.psi_degree + o_add_gen.psi_degree892if deg_sum > self.dim() - \893max(len(s_enh_profile[0]), len(o_enh_profile[0])):894return self.ZERO895degenerations = self.common_degenerations(s_enh_profile, o_enh_profile)896if not degenerations:897return self.ZERO898NB = self.cnb(s_enh_profile, o_enh_profile, amb_enh_profile)899if NB == 1:900# Intersection is transversal, in this case we are done:901# the pullback of an additive generator is a taut class902# where all classes live on the same graph:903prod = [(_cs * _co, s_pb * o_pb)904for L in degenerations905for _cs, s_pb in s_add_gen.pull_back(L).psi_list906for _co, o_pb in o_add_gen.pull_back(L).psi_list]907return admcycles.diffstrata.elgtautclass.ELGTautClass(908self, list(prod))909elif NB == 0 or NB == self.ZERO:910# product with 0 is 0 ...911return NB # maybe better: always self.ZERO?912else:913# intersect the pullback to L with the normal bundle pulled back to914# L (in L):915summands = [self.intersection(916self.intersection(917s_add_gen.pull_back(L),918o_add_gen.pull_back(L),919L),920self.gen_pullback_taut(921NB,922L,923self.minimal_common_undegeneration(924s_enh_profile, o_enh_profile)925),926L)927for L in degenerations]928return self.ELGsum(list(summands))929930def normal_bundle(self, enh_profile, ambient=None):931"""932Normal bundle of enh_profile in ambient.933934Note that this is equivalent to cnb(enh_profile, enh_profile, ambient).935936Args:937enh_profile (tuple): enhanced profile938ambient (tuple, optional): enhanced profile. Defaults to None.939940Raises:941ValueError: Raised if enh_profile is not a codim 1 degeneration of ambient942943Returns:944ELGTautClass: Normal bundle N_{enh_profile, ambient}945"""946if ambient is None:947ambient = ((), 0)948else:949ambient = (tuple(ambient[0]), ambient[1])950if len(enh_profile[0]) != len(ambient[0]) + \9511 or not self.is_degeneration(enh_profile, ambient):952raise ValueError(953"%r is not a codim 1 degeneration of %r" %954(enh_profile, ambient))955return self.cnb(enh_profile, enh_profile, ambient)956957# This is an element of CH^s(ambient) where s is the cardinality of the intersection of profiles958# or equivalently in CH^(c+s)(B) where c is the codimension of ambient.959@cached_method960def cnb(self, s_enh_profile, o_enh_profile, amb_enh_profile=None):961"""962Common Normal bundle of two graphs in an ambient graph.963964Note that for a trivial normal bundle (transversal intersection)965we return 1 (int) and NOT self.ONE !!966967The reason is that the correct ONE would be the ambient graph and that968is a pain to keep track of in intersection....969970Raises a RuntimeError if s_enh_profile or o_enh_profile do not971degenerate from amb_enh_profile.972973INPUT:974975s_enh_profile (tuple): enhanced profile976o_enh_profile (tuple): enhanced profile977amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.978979OUTPUT:980981ELGTautClass: Product of normal bundles appearing.9821 if the intersection is transversal.983"""984# check/normalise input:985if amb_enh_profile is None:986amb_enh_profile = ((), 0)987else:988amb_enh_profile = (tuple(amb_enh_profile[0]), amb_enh_profile[1])989if not self.is_degeneration(s_enh_profile, amb_enh_profile):990raise RuntimeError("%r is not a degeneration of %r" %991(s_enh_profile, amb_enh_profile))992if not self.is_degeneration(o_enh_profile, amb_enh_profile):993raise RuntimeError("%r is not a degeneration of %r" %994(o_enh_profile, amb_enh_profile))995min_com = self.minimal_common_undegeneration(996s_enh_profile, o_enh_profile)997if min_com == amb_enh_profile:998return 1 # terminating condition, transversal999else:1000assert self.codim_one_common_undegenerations(s_enh_profile, o_enh_profile, amb_enh_profile),\1001"minimal common undegeneration is %r, ambient profile is %r, but there aren't codim one common undegenerations!"\1002% (min_com, amb_enh_profile)1003return_list = []1004for ep in self.codim_one_common_undegenerations(1005s_enh_profile, o_enh_profile, amb_enh_profile):1006p, i = ep1007# This is the "difference" between ep and amb_enh_profile:1008# i.e. the inserted level, i in paper notation1009squished_level = get_squished_level(ep, amb_enh_profile)1010ll = self.bics[p[squished_level]].ell1011xi_top = self.xi_at_level(squished_level, ep, quiet=True)1012xi_bot = self.xi_at_level(squished_level + 1, ep, quiet=True)1013xis = -xi_top + xi_bot1014summand = 1 / QQ(ll) * self.gen_pullback_taut(xis, min_com, ep)1015# calL pulled back to min_com:1016summand -= 1 / \1017QQ(ll) * self.gen_pullback_taut(self.calL(ep,1018squished_level), min_com, ep)1019if summand == 0:1020# product is zero!1021return self.ZERO1022assert summand.is_equidimensional(),\1023"Not all summands in %s of same degree!" % summand1024return_list.append(summand)1025# product over normal bundles:1026if not return_list:1027return 1 # empty product => transversal1028NBprod = return_list[0]1029for nb in return_list[1:]:1030NBprod = self.intersection(NBprod, nb, min_com)1031assert NBprod.is_equidimensional(), "Not all summands in %s of same degree!" % NBprod1032return NBprod10331034@cached_method1035def gen_pullback(self, add_gen, o_enh_profile, amb_enh_profile=None):1036"""1037Generalised pullback of additive generator to o_enh_profile in amb_enh_profile.10381039Args:1040add_gen (AdditiveGenerator): additive generator on a degeneration of amb_enh_profile.1041o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)1042amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.10431044Raises:1045RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.10461047Returns:1048ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.1049"""1050# check input:1051if amb_enh_profile is None:1052amb_enh_profile = ((), 0)1053if not self.is_degeneration(o_enh_profile, amb_enh_profile):1054raise RuntimeError("%r is not a degeneration of %r" %1055(o_enh_profile, amb_enh_profile))1056s_enh_profile = add_gen.enh_profile1057if not self.is_degeneration(s_enh_profile, amb_enh_profile):1058raise RuntimeError("%r is not a degeneration of %r" %1059(s_enh_profile, amb_enh_profile))1060degenerations = self.common_degenerations(s_enh_profile, o_enh_profile)1061# if there are no common degenerations, pullback is 01062if not degenerations:1063return self.ZERO1064NB = self.cnb(s_enh_profile, o_enh_profile, amb_enh_profile)1065# stop condition1066if NB == 0 or NB == self.ZERO:1067return 01068return_list = []1069for L in degenerations:1070if NB == 1:1071# transversal1072return_list.append(add_gen.pull_back(L))1073else:1074return_list.append(1075self.intersection(1076self.gen_pullback_taut(1077NB,1078L,1079self.minimal_common_undegeneration(1080s_enh_profile,1081o_enh_profile)),1082add_gen.pull_back(L),1083L))1084return_value = self.ELGsum(return_list)1085if return_value != 0:1086return return_value1087else:1088return self.ZERO10891090def gen_pullback_taut(self, taut_class, o_enh_profile,1091amb_enh_profile=None):1092"""1093Generalised pullback of tautological class to o_enh_profile in amb_enh_profile.10941095This simply returns the ELGSum of gen_pullback of all AdditiveGenerators.10961097Args:1098taut_class (ELGTautClass): tautological class each summand on a degeneration of amb_enh_profile.1099o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)1100amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.11011102Raises:1103RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.11041105Returns:1106ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.1107"""1108return_list = []1109for c, AG in taut_class.psi_list:1110return_list.append(1111c * self.gen_pullback(AG, o_enh_profile, amb_enh_profile))1112return self.ELGsum(return_list)11131114# TODO: There should be a better way for this, using just BICs and where1115# marked points go ... (see discussion above)1116@cached_method1117def explicit_edge_becomes_long(self, enh_profile, edge):1118"""1119A list of enhanced profiles where the (explicit) edge 'edge' became long.11201121We go through the codim one degenerations of enh_profile and check1122each graph, if edge became long (under any degeneration).11231124However, we count each graph only once, even if there are several ways1125to undegenerate (see examples).11261127Raises a RuntimeError if the leg is not a leg of the graph of enh_profile.11281129INPUT:11301131enh_profile (tuple): enhanced profile: (profile, index).11321133edge (tuple): edge of the LevelGraph associated to enh_profile:1134(start leg, end leg).11351136OUTPUT:11371138The list of enhanced profiles.11391140EXAMPLES::11411142sage: from admcycles.diffstrata import *1143sage: X=Stratum((1,1))1144sage: V=[ep for ep in X.enhanced_profiles_of_length(1) if X.lookup_graph(*ep).level(0)._h0 == 2]1145sage: epV=V[0]1146sage: VLG=X.lookup_graph(*epV).LG1147sage: assert len(X.explicit_edge_becomes_long(epV, VLG.edges[1])) == 11148sage: assert X.explicit_edge_becomes_long(epV, VLG.edges[1]) == X.explicit_edge_becomes_long(epV, VLG.edges[1])11491150"""1151ep_list = []1152for ep in self.codim_one_degenerations(enh_profile):1153g = self.lookup_graph(*ep)1154if g.LG.has_long_edge:1155for leg_map in self.explicit_leg_maps(enh_profile, ep):1156try:1157if g.LG.is_long((leg_map[edge[0]], leg_map[edge[1]])):1158ep_list.append(ep)1159# Not sure, if we want to record several1160# occurrences...1161break1162except KeyError:1163raise RuntimeError(1164"%r does not seem to be an edge of %r" %1165(edge, enh_profile))1166return ep_list11671168@cached_method1169def explicit_edges_between_levels(1170self, enh_profile, start_level, stop_level):1171"""1172Edges going from (relative) level start_level to (relative) level stop_level.11731174Note that we assume here that edges respect the level structure, i.e.1175start on start_level and end on end_level!11761177Args:1178enh_profile (tuple): enhanced profile1179start_level (int): relative level number (0...codim)1180stop_level (int): relative level number (0...codim)11811182Returns:1183list: list of edges, i.e. tuples (start_point,end_point)11841185EXAMPLES::11861187sage: from admcycles.diffstrata import *1188sage: X=Stratum((2,))11891190Compact type:11911192sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 1]) == 111931194Banana:11951196sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 2]) == 111971198"""1199G = self.lookup_graph(*enh_profile)1200# TODO: There should be a way smarter way for doing this...1201edges = [1202e for e in G.LG.edges if (1203G.LG.level_number(1204G.LG.levelofleg(1205e[0])) == start_level and G.LG.level_number(1206G.LG.levelofleg(1207e[1])) == stop_level)]1208return edges12091210# Finding codimension one degenerations:1211# This is not very fancy yet.1212# At the moment, we take a profile and check at which places we can compatibly1213# insert a BIC (similarly to creating the lookup_list).1214# We then check "by hand", if this is ok with the enhanced structure, i.e.1215# on connected components.1216# Note that this check is bypassed if the input profile is irreducible.1217@cached_method1218def codim_one_degenerations(self, enh_profile):1219"""1220Degenerations of enh_profile with one more level.12211222Raises a RuntimeError if we find a degeneration that doesn't squish1223back to the graph we started with.12241225INPUT:12261227enh_profile (enhanced profile): tuple (profile, index)12281229OUTPUT:12301231list of enhanced profiles.12321233EXAMPLES::12341235sage: from admcycles.diffstrata import *1236sage: X=GeneralisedStratum([Signature((4,))])1237sage: assert all(len(p) == 2 for p, _ in X.codim_one_degenerations(((2,),0)))12381239Empty profile gives all bics:12401241sage: assert X.codim_one_degenerations(((),0)) == [((0,), 0), ((1,), 0), ((2,), 0), ((3,), 0), ((4,), 0), ((5,), 0), ((6,), 0), ((7,), 0)]1242"""1243profile = list(enh_profile[0])1244# empty profile gives all bics:1245if not profile:1246return [((b,), 0) for b in range(len(self.bics))]1247deg_list = []1248# build all length 1 profile extensions:1249# The first and last entry don't have any compatibility conditions:1250# add all top degenerations of the first guy1251for bic in self.DG.top_to_bic(profile[0]).values():1252deg_list.append(tuple([bic] + profile[:]))1253# and all bottom degenerations of the last guy1254for bic in self.DG.bot_to_bic(profile[-1]).values():1255deg_list.append(tuple(profile[:] + [bic]))1256# For the "middle" entries of the profile, we have to check1257# compatibility1258for i in range(len(profile) - 1):1259for bic in self.DG.bot_to_bic(profile[i]).values(): # candidates1260if bic in self.DG.top_to_bic(profile[i + 1]).values():1261deg_list.append(1262tuple(profile[:i + 1] + [bic] + profile[i + 1:]))1263deg_list = list(set(deg_list)) # remove duplicates1264# We now build the list of enhanced profiles:1265enh_list = []1266for p in deg_list:1267for i in range(len(self.lookup(p))):1268if self.is_degeneration((p, i), enh_profile):1269enh_list.append((p, i))1270return enh_list12711272@cached_method1273def codim_one_common_undegenerations(1274self, s_enh_profile, o_enh_profile, amb_enh_profile=None):1275"""1276Profiles that are 1-level degenerations of amb_enh_profile and include1277s_enh_profile and o_enh_profile.12781279INPUT:12801281s_enh_profile (tuple): enhanced profile1282o_enh_profile (tuple): enhanced profile1283amb_enh_profile (tuple): enhanced profile12841285OUTPUT:12861287list of enhanced profiles1288"""1289if amb_enh_profile is None:1290amb_enh_profile = ((), 0)1291profile_list = []1292for ep in self.codim_one_degenerations(amb_enh_profile):1293if self.is_degeneration(1294s_enh_profile,1295ep) and self.is_degeneration(1296o_enh_profile,1297ep):1298profile_list.append(ep)1299return profile_list13001301@cached_method1302def minimal_common_undegeneration(self, s_enh_profile, o_enh_profile):1303"""1304The minimal dimension graph that is undegeneration of both s_enh_profile1305and o_enh_profile.13061307Raises a RuntimeError if there are no common undgenerations in the intersection profile.13081309INPUT:13101311s_enh_profile (tuple): enhanced profile1312o_enh_profile (tuple): enhanced profile13131314OUTPUT:13151316tuple: enhanced profile1317"""1318s_profile = s_enh_profile[0]1319o_profile = o_enh_profile[0]1320# build "sorted" intersection1321intersection = []1322for b in s_profile:1323if b in o_profile:1324intersection.append(b)1325# make hashable1326intersection = tuple(intersection)1327# if the intersection profile is irreducible, we are done:1328if len(self.lookup(intersection)) == 1:1329return (intersection, 0)1330else:1331for i in range(len(self.lookup(intersection))):1332if (self.is_degeneration(s_enh_profile, (intersection, i))1333and self.is_degeneration(o_enh_profile, (intersection, i))):1334return (intersection, i)1335else:1336raise RuntimeError(1337"No common undegeneration in profile %r" % intersection)13381339@cached_method1340def is_degeneration(self, s_enh_profile, o_enh_profile):1341"""1342Check if s_enh_profile is a degeneration of o_enh_profile.13431344Args:1345s_enh_profile (tuple): enhanced profile1346o_enh_profile (tuple): enhanced profile13471348Returns:1349bool: True if the graph associated to s_enh_profile is a degeneration1350of the graph associated to o_enh_profile, False otherwise.13511352EXAMPLES::13531354sage: from admcycles.diffstrata import *1355sage: X=GeneralisedStratum([Signature((4,))])1356sage: assert X.is_degeneration(((7,),0),((7,),0))13571358The empty tuple corresponds to the stratum:13591360sage: assert X.is_degeneration(((2,),0),((),0))1361"""1362s_profile = s_enh_profile[0]1363o_profile = o_enh_profile[0]1364# first check: subset:1365if not set(o_profile) <= set(s_profile):1366return False1367# in the irreducible case, we are done:1368if len(self.lookup(s_profile)) == len(self.lookup(o_profile)) == 1:1369assert self.explicit_leg_maps(o_enh_profile, s_enh_profile),\1370"%r and %r contain only one graph, but these are not degenerations!"\1371% (o_enh_profile, s_enh_profile)1372return True1373else:1374# otherwise: check if an undegeneration map exists:1375try:1376if self.explicit_leg_maps(1377o_enh_profile, s_enh_profile, only_one=True) is None:1378return False1379else:1380return True1381except UserWarning:1382# This is raised if there is no undegeneration inside the1383# expected profile...1384return False13851386@cached_method1387def squish(self, enh_profile, l):1388"""1389Squish level l of the graph associated to enh_profile. Returns the enhanced profile1390associated to the squished graph.13911392Args:1393enh_profile (tuple): enhanced profile1394l (int): level of graph associated to enhanced profile13951396Raises:1397RuntimeError: Raised if a BIC is squished at a level other than 0.1398RuntimeError: Raised if the squished graph is not found in the squished profile.13991400Returns:1401tuple: enhanced profile.14021403EXAMPLES::14041405sage: from admcycles.diffstrata import *1406sage: X=Stratum((2,))1407sage: assert all(X.squish(ep,0) == ((),0) for ep in X.enhanced_profiles_of_length(1))1408sage: 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))1409"""1410p, i = enh_profile1411if len(p) == 1:1412if l != 0:1413raise RuntimeError(1414"BIC can only be squished at level 0!")1415return ((), 0)1416new_p = list(p)1417new_p.pop(l)1418new_p = tuple(new_p)1419enhancements = []1420for j in range(len(self.lookup(new_p))):1421if self.is_degeneration(enh_profile, (new_p, j)):1422enhancements.append(j)1423if len(enhancements) != 1:1424raise RuntimeError(1425"Cannot squish %r at level %r! No unique graph found in %r!" %1426(enh_profile, l, new_p))1427return (new_p, enhancements[0])14281429# Partial order1430# The lookup graph gives a partial order on the BICs (the 3-level graph (i,j)1431# implies i > j).1432@cached_method1433def lies_over(self, i, j):1434"""1435Determine if (i,j) is a 3-level graph.14361437INPUT:14381439i (int): Index of BIC.14401441j (int): Index of BIC.14421443OUTPUT:14441445bool: True if (i,j) is a 3-level graph, False otherwise.1446"""1447if j in self.DG.bot_to_bic(i).values():1448assert i in self.DG.top_to_bic(j).values(),\1449"%r is a bottom degeneration of %r, but %r is not a top degeneration of %r!"\1450% (j, i, i, j)1451return True1452else:1453assert i not in self.DG.top_to_bic(j).values(),\1454"%r is not a bottom degeneration of %r, but %r is a top degeneration of %r!"\1455% (j, i, i, j)1456return False14571458# Merging profiles (with respect to lies_over)1459@cached_method1460def merge_profiles(self, p, q):1461"""1462Merge profiles with respect to the ordering "lies_over".14631464Args:1465p (iterable): sorted profile1466q (iterable): sorted profile14671468Returns:1469tuple: sorted profile or None if no such sorted profile exists.14701471EXAMPLES::14721473sage: from admcycles.diffstrata import *1474sage: X=GeneralisedStratum([Signature((4,))])1475sage: X.merge_profiles((5,),(5,))1476(5,)1477"""1478# input profiles should be sorted:1479assert all(self.lies_over(p[i], p[i + 1]) for i in range(len(p) - 1)),\1480"Profile %r not sorted!" % (p,)1481assert all(self.lies_over(q[i], q[i + 1]) for i in range(len(q) - 1)),\1482"Profile %r not sorted!" % (q,)1483new_profile = []1484next_p = 01485next_q = 01486while next_p < len(p) and next_q < len(q):1487if p[next_p] == q[next_q]:1488new_profile.append(p[next_p])1489next_p += 11490next_q += 11491else:1492if self.lies_over(p[next_p], q[next_q]):1493new_profile.append(p[next_p])1494next_p += 11495else:1496if self.lies_over(q[next_q], p[next_p]):1497new_profile.append(q[next_q])1498else:1499return None1500next_q += 11501# pick up rest (one of these is empty!):1502new_profile += p[next_p:]1503new_profile += q[next_q:]1504return tuple(new_profile)15051506# Better graph lookup:1507# Here we should really work with "enhanced dominos", because1508# otherwise it's not clear how the list indices of degenerations are related1509# to each other.1510# Therefore, arguments are:1511# * a sorted(!) list of BICs, i.e. an element of the lookup_list1512# * a (consistent) choice of components of the involved 3-level graph (i.e.1513# enhanced dominos)1514# This can consistently produce a graph.1515##1516# For now, we use the workaround to forcibly only work with sorted profiles1517# where the indexing is at least consistent.1518###15191520def lookup_graph(self, bic_list, index=0):1521"""1522Return the graph associated to an enhanced profile.15231524Note that starting in SAGE 9.0 profile numbering will change between sessions!15251526Args:1527bic_list (iterable): (sorted!) tuple/list of indices of bics.1528index (int, optional): Index in lookup list. Defaults to 0.15291530Returns:1531EmbeddedLevelGraph: graph associated to the enhanced (sorted) profile1532(None if empty).15331534EXAMPLES::15351536sage: from admcycles.diffstrata import *1537sage: X=Stratum((2,))1538sage: X.lookup_graph(())1539EmbeddedLevelGraph(LG=LevelGraph([2],[[1]],[],{1: 2},[0],True),dmp={1: (0, 0)},dlevels={0: 0})15401541Note that an enhanced profile needs to be unpacked with *:15421543sage: X.lookup_graph(*X.enhanced_profiles_of_length(2)[0]) # 'unsafe' (edge ordering may change) # doctest:+SKIP1544EmbeddedLevelGraph(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})15451546"""1547# this is a bit stupid, but whatever...1548if all(self.lies_over(bic_list[i], bic_list[i + 1])1549for i in range(len(bic_list) - 1)):1550return self.lookup(bic_list)[index]1551else:1552return None15531554def lookup(self, bic_list, quiet=True):1555"""1556Take a profile (i.e. a list of indices of BIC) and return the corresponding1557EmbeddedLevelGraphs (i.e. the product of these BICs).15581559Note that this will be a one element list "most of the time", but1560it can happen that this is not irreducible:15611562This implementation is not dependent on the order (!) (we look in top and1563bottom degenerations and clutch...)15641565However, for caching purposes, it makes sense to use only the sorted profiles...15661567NOTE THAT IN PYTHON3 PROFILES ARE NO LONGER DETERMINISTIC!!!!!15681569(they typically change with every python session...)15701571Args:1572bic_list (iterable): list of indices of bics15731574Returns:1575list: The list of EmbeddedLevelGraphs corresponding to the profile.15761577EXAMPLES::15781579sage: from admcycles.diffstrata import *1580sage: X=GeneralisedStratum([Signature((4,))])15811582This is independent of the order.15831584sage: p, _ = X.enhanced_profiles_of_length(2)[0]1585sage: assert any(X.lookup(p)[0].is_isomorphic(G) for G in X.lookup((p[1],p[0])))15861587Note that the profile can be empty or reducible.15881589"""1590if not quiet:1591print("Looking up enhanced profiles in %r..." % (bic_list,))1592sys.stdout.flush() # MPI computer has congestion issues...1593lookup_key = tuple(bic_list)1594if not bic_list: # empty1595if not quiet:1596print("Empty profile, returning smooth_LG. Done.")1597sys.stdout.flush()1598return [self.smooth_LG]1599if len(bic_list) == 1:1600if not quiet:1601print("BIC, profile irreducible by definition. Done.")1602sys.stdout.flush()1603return [self.bics[bic_list[0]]]1604try:1605cached_list = self._lookup[lookup_key]1606if not quiet:1607print("Using cached lookup. Done.")1608sys.stdout.flush()1609return cached_list1610except KeyError:1611bic_list = list(bic_list) # in case we are passed a tuple...1612# otherwise, making a copy if we're about to manipulate is also not1613# such a bad idea...1614i = bic_list.pop() # index in self.bics1615B = self.bics[i] # this might build bics (!)1616# We split the remainder of bic_list into those coming from1617# degenerations of the top component and those from bottom.1618# Note that these lists will contain their indices in B.top1619# and B.bot, respectively.1620# Moreover, they have to be nested in case there are multiple1621# components.1622top_lists = [[]]1623bot_lists = [[]]1624for j in bic_list:1625if not quiet:1626print("Looking at BIC %r:" % j, end=' ')1627sys.stdout.flush()1628# a bic is either in the image of top_to_bic1629# or bot_to_bic.1630# If it isn't in any image, the intersection is empty1631# and we return None.1632# Note that again this might build the maps.1633try:1634top_bics = self.DG.top_to_bic_inv(i)[j]1635if not quiet:1636print(1637"Adding %r BICs from top component to top_lists..." %1638len(top_bics))1639sys.stdout.flush()1640# if there are several components, we "branch out":1641new_top_lists = []1642for b in top_bics:1643for top_list in top_lists:1644new_top_lists.append(top_list + [b])1645top_lists = new_top_lists1646except KeyError:1647try:1648bot_bics = self.DG.bot_to_bic_inv(i)[j]1649if not quiet:1650print(1651"Adding %r BICs from bottom component to bot_lists..." %1652len(bot_bics))1653sys.stdout.flush()1654# if there are several components, we "branch out":1655new_bot_lists = []1656for b in bot_bics:1657for bot_list in bot_lists:1658new_bot_lists.append(bot_list + [b])1659bot_lists = new_bot_lists1660except KeyError:1661# Intersection empty.1662return []1663if not quiet:1664print("Done building top_lists and bot_lists.")1665print(1666"This gives us %r profiles in %s and %r profiles in %s that we will now clutch pairwise and recursively." %1667(len(top_lists), B.top, len(bot_lists), B.bot))1668sys.stdout.flush()1669graph_list = [admcycles.diffstrata.stratatautring.clutch(1670self,1671top,1672bot,1673B.clutch_dict,1674B.emb_top,1675B.emb_bot1676)1677for top_list, bot_list in itertools.product(top_lists, bot_lists)1678for top in B.top.lookup(top_list, quiet=quiet)1679for bot in B.bot.lookup(bot_list, quiet=quiet)1680]1681# we might have picked up isomorphic guys (e.g. v-graph)1682if not quiet:1683print(1684"For profile %r in %s, we have thus obtained %r graphs." %1685(bic_list, self, len(graph_list)))1686print("Sorting these by isomorphism class...", end=' ')1687sys.stdout.flush()1688rep_list = admcycles.diffstrata.bic.isom_rep(graph_list)1689self._lookup[lookup_key] = rep_list1690if not quiet:1691print("Done. Found %r isomorphism classes." % len(rep_list))1692sys.stdout.flush() # MPI computer has congestion issues...1693return rep_list16941695@cached_method1696def sub_graph_from_level(self, enh_profile, l,1697direction='below', return_split_edges=False):1698"""1699Extract an EmbeddedLevelGraph from the subgraph of enh_profile that is either1700above or below level l.17011702This is embedded into the top/bottom component of the bic at profile[l-1].1703In particular, this is a 'true' sub graph, i.e. the names of the vertices and1704legs are the same as in enh_profile.17051706Note: For l==0 or l==number_of_levels we just return enh_profile.17071708INPUT:17091710l (int): (relative) level number.1711direction (str, optional): 'above' or 'below'. Defaults to 'below'.1712return_split_edges (bool, optional. Defaults to False): also return a tuple1713of the edges split across level l.17141715OUTPUT:17161717EmbeddedLevelGraph: Subgraph of top/bottom component of the bic at profile[l-1].17181719If return_split_edges=True: Returns tuple (G,e) where17201721* G is the EmbeddedLevelGraph1722* e is a tuple of edges of enh_profile that connect legs above level1723l with those below (i.e. those edges needed for clutching!)17241725EXAMPLES::17261727sage: from admcycles.diffstrata import *1728sage: X=Stratum((2,))1729sage: ep = X.enhanced_profiles_of_length(2)[0]1730sage: X.sub_graph_from_level(ep, 1, 'above')1731EmbeddedLevelGraph(LG=LevelGraph([1],[[1]],[],{1: 0},[0],True),dmp={1: (0, 0)},dlevels={0: 0})1732sage: X.sub_graph_from_level(ep, 1, 'below') # 'unsafe' (edge order might change) # doctest:+SKIP1733EmbeddedLevelGraph(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})1734sage: X.bics[ep[0][0]].top1735LevelStratum(sig_list=[Signature((0,))],res_cond=[],leg_dict={1: (0, 0)})1736sage: X.bics[ep[0][1]].bot1737LevelStratum(sig_list=[Signature((2, -2, -2))],res_cond=[[(0, 1), (0, 2)]],leg_dict={3: (0, 0), 4: (0, 1), 5: (0, 2)})1738"""1739G = self.lookup_graph(*enh_profile)1740if l == 0:1741if direction == 'below':1742if return_split_edges:1743return (G, tuple())1744return G1745if return_split_edges:1746return (None, tuple())1747return None1748if l == G.number_of_levels:1749if direction == 'above':1750if return_split_edges:1751return (G, tuple())1752return G1753if return_split_edges:1754return (None, tuple())1755return None1756profile, _i = enh_profile1757# The BIC that will give us the level is BIC l-1 in the profile:1758bic_number = profile[l - 1]1759B = self.bics[bic_number]1760# We extract the subgraph from the underlying LevelGraph, so we have1761# to work with the internal level numbering:1762internal_l = G.LG.internal_level_number(l)1763# Actually only three things depend on above/below:1764# * The choice of vertices in the subgraph.1765# * The choice of level to embed into (top/bottom of B).1766# * The new level dictionary (as extracting does not change the levels,1767# this just consists of the relevant part of G.dlevels)1768# Note that in the 'below' case we consider levels <= l, while in 'above'1769# we consider > l (we want to cut level passage l!)1770if direction == 'below':1771new_vertices = [v for v in range(len(G.LG.genera))1772if G.LG.levelofvertex(v) <= internal_l]1773# in this case, the level we want to embed into is the bottom of B1774L = B.bot1775# the levels <= internal_l survive into dlevels1776new_dlevels = {k: v for k, v in G.dlevels.items()1777if k <= internal_l}1778else:1779assert direction == 'above'1780new_vertices = [v for v in range(len(G.LG.genera))1781if G.LG.levelofvertex(v) > internal_l]1782# in this case, the level we want to embed into is the top of B1783L = B.top1784# the levels >= internal_l survive into dlevels1785new_dlevels = {k: v for k, v in G.dlevels.items()1786if k > internal_l}1787vertex_set = set(new_vertices)1788new_edges = [e for e in G.LG.edges1789if G.LG.vertex(e[0]) in vertex_set and1790G.LG.vertex(e[1]) in vertex_set]1791new_LG = G.LG.extract(new_vertices, new_edges)1792leg_set = set(flatten(new_LG.legs))1793# Next, we take the part of dmp that we still need:1794# Note that G.dmp maps legs of G to points of X, but we want is a map1795# to points of L.1796# We get this from the following observation:1797# We have1798# * L.leg_dict: points of B -> points of L1799# * B.dmp_inv: points of X -> points of B1800# Therefore the composition gives the desired map1801# points of G -> points of L1802new_dmp = {k: L.leg_dict[B.dmp_inv[v]]1803for k, v in G.dmp.items() if k in leg_set}1804# The only thing missing is to add the marked points of the edges1805# that we have cut:1806# We do this in no particular order, as the clutching information will1807# have to be retrieved anyways when actually splitting the graph.1808# Note that != is boolean xor (!)1809split_edges = [e for e in G.LG.edges1810if (e[0] in leg_set) != (e[1] in leg_set)]1811split_half_edges = [e[0] if e[0] in leg_set else e[1]1812for e in split_edges]1813# To place these into new_dmp, we pick an undegeneration map G -> B1814# Note that the choice of map *should* not matter, as they should differ1815# only by an automorphism of B... (except for psi classes, where we have1816# to be careful with xi_on_level!!!)1817B_to_G = self.explicit_leg_maps(1818((bic_number,), 0), enh_profile, only_one=True)1819assert B_to_G # G is actually a degeneration of B!1820G_to_B = {v: k for k, v in B_to_G.items()}1821# check the points we already placed are consistent:1822assert all(L.leg_dict[G_to_B[leg]] == new_dmp[leg] for leg in new_dmp)1823while split_half_edges:1824leg = split_half_edges.pop()1825new_dmp[leg] = L.leg_dict[G_to_B[leg]]1826# some more checks:1827legs_in_new_edges = set(flatten(new_edges))1828marked_points = set(new_dmp.keys())1829assert legs_in_new_edges.isdisjoint(marked_points)1830assert leg_set == (legs_in_new_edges | marked_points)1831sub_graph = admcycles.diffstrata.embeddedlevelgraph.EmbeddedLevelGraph(1832L, new_LG, new_dmp, new_dlevels)1833if return_split_edges:1834return (sub_graph, tuple(split_edges))1835return sub_graph18361837# @cached_method1838def split_graph_at_level(self, enh_profile, l):1839"""1840Splits enh_profile self into top and bottom component at level l.18411842(Note that the 'cut' occurs right above level l, i.e. to get the top level1843and the rest, l should be 1! (not 0))18441845The top and bottom components are EmbeddedLevelGraphs, embedded into1846top and bottom of the corresponding BIC (obtained via sub_graph_from_level).18471848The result is made so that it can be fed into clutch.18491850Args:1851enh_profile (tuple): enhanced profile.1852l (int): (relative) level of enh_profile.18531854Returns:1855dict: dictionary consising of1856* X: GeneralisedStratum self.X1857* top: EmbeddedLevelGraph: top component1858* bottom: EmbeddedLevelGraph: bottom component1859* clutch_dict: clutching dictionary mapping ex-half-edges on1860top to their partners on bottom (both as points in the1861respective strata via dmp!)1862* emb_dict_top: a dictionary embedding top into the stratum of self1863* emb_dict_bot: a dictionary embedding bot into the stratum of self1864* leg_dict: a dictionary legs of enh_profile -> legs of top/bottom18651866Note that clutch_dict, emb_top and emb_bot are dictionaries between1867points of strata, i.e. after applying dmp to the points!18681869EXAMPLES::18701871In particular, we can use this to "glue" the BICs of 10^top into (10,9,6) and1872obtain all components of the profile.18731874"""1875# Split the graph into top and bottom components at level l:1876top_graph, se_top = self.sub_graph_from_level(1877enh_profile, l, direction='above', return_split_edges=True)1878bot_graph, se_bot = self.sub_graph_from_level(1879enh_profile, l, direction='below', return_split_edges=True)1880assert se_top == se_bot1881split_edges = se_top1882# We construct the clutching info by splitting the BIC that corresponds1883# to level l:1884p, _i = enh_profile1885# TODO: edge cases1886B = self.bics[p[l - 1]]1887clutching_info = B.split()1888# we simply replace the top and bottom components of B by our graphs:1889assert clutching_info['top'] == top_graph.X == B.top1890clutching_info['top'] = top_graph1891assert clutching_info['bottom'] == bot_graph.X == B.bot1892clutching_info['bottom'] = bot_graph1893# the clutch_dict has to be replaced by the split_edges:1894# Note that these are currently edges of enh_profile, so they need to be1895# translated to points on the corresponding stratum via the embedding1896# of top_graph/bot_graph:1897# WARNING: We use here (once again) implicitly that e[0] is above e[1]!1898clutching_info['clutch_dict'] = {1899top_graph.dmp[e[0]]: bot_graph.dmp[e[1]] for e in split_edges}1900return clutching_info19011902# @cached_method1903def doublesplit(self, enh_profile):1904"""1905Splits embedded 3-level graph into top, middle and bottom component, along with1906all the information required (by clutch) to reconstruct self.19071908We return a dictionary so that the result can be fed into clutch (naming of1909optional arguments...)19101911This is mainly a technical backend for doublesplit_graph_before_and_after_level.19121913Note that in contrast to EmbeddedLevelGraph.split, we want to feed a length-2-profile1914so that we can really split into the top and bottom of the associated BICs (the only1915strata we can control!)19161917This method is mainly intended for being fed into clutch.19181919Raises a ValueError if self is not a 3-level graph.19201921INPUT:19221923enh_profile (tuple): enhanced profile.19241925Returns:19261927A dictionary consisting of19281929* X: GeneralisedStratum self,1930* top: LevelStratum top level of top BIC,1931* bottom: LevelStratum bottom level of bottom BIC,1932* middle: LevelStratum level -1 of enh_profile,1933* emb_dict_top: dict: points of top stratum -> points of X,1934* emb_dict_bot: dict: points of bottom stratum -> points of X,1935* emb_dict_mid: dict: points of middle stratum -> points of X,1936* clutch_dict: dict: points of top stratum -> points of middle stratum,1937* clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,1938* clutch_dict_long: dict: points of top stratum -> points of bottom stratum.1939"""1940p, i = enh_profile1941if not len(p) == 2:1942raise ValueError("Error: Not a 3-level graph! %r" % self)1943G = self.lookup_graph(p, i)1944# Here it is important that we pick top and bot of the corresponding BICs and identify them with1945# level(0) and level(2) of G (as these might not be the same (e.g.1946# switched components!)!)1947top = self.bics[p[0]].top1948middle = G.level(1)1949bottom = self.bics[p[1]].bot1950# To construct the embedding dictionaries, we have to identify legs of G1951# with (stratum) points of top/middle/bottom as keys and points on self as1952# values.1953#1954# The values (points on self) are given by G.dmp.1955#1956# The keys for middle are given via leg_dict.1957#1958# For top and bottom, we have to first fix a map from G to1959# p[0] and p[1] and then combine self.dmp with the leg_dicts of the LevelStrata.1960# It *shouldn't* matter, which undegeneration we take:1961top_to_G = self.explicit_leg_maps(1962((p[0],), 0), enh_profile, only_one=True)1963G_to_top = {v: k for k, v in top_to_G.items()}1964bot_to_G = self.explicit_leg_maps(1965((p[1],), 0), enh_profile, only_one=True)1966G_to_bot = {v: k for k, v in bot_to_G.items()}1967# More precisely: We now have the following maps (e.g. for top):1968#1969# G_to_top: points in G -> points in p[0]1970# top.leg_dict: points in p[0] -> stratum points of top1971#1972# and1973#1974# top.leg_dict_inv: stratum points of top -> points in p[0]1975# top_to_G: points in p[0] -> points in G1976# G.dmp: points in G -> stratum points on self1977#1978# i.e. emb_top is the composition of the inverse of the leg_dict1979# of top, i.e. top.stratum_number, with top_to_G and G.dmp1980# (giving a map from the points of top to the points of self)1981# and the same for middle and bottom.1982#1983# We implement this by iterating over the marked points of G on top level,1984# which are exactly the keys of G.dmp that are on top level.1985#1986# For this, we have to compose with G_to_top and top.leg_dict again.1987#1988# Note that we make extra sure that we didn't mess up the level numbering by1989# using the relative level numbering (where the top level is guaranteed to be 0,1990# the middle is 1 and the bottom level is 2 (positive!)).1991emb_dict_top = {top.leg_dict[G_to_top[l]]: G.dmp[l]1992for l in iter(G.dmp)1993if G.LG.level_number(G.LG.levelofleg(l)) == 0}1994emb_dict_mid = {middle.leg_dict[l]: G.dmp[l]1995for l in iter(G.dmp)1996if G.LG.level_number(G.LG.levelofleg(l)) == 1}1997emb_dict_bot = {bottom.leg_dict[G_to_bot[l]]: G.dmp[l]1998for l in iter(G.dmp)1999if G.LG.level_number(G.LG.levelofleg(l)) == 2}2000# Because this is a 3-level graph, all edges of self are cut in this process2001# and this gives us exactly the dictionary we must remember:2002# Note however, that we have to check if the edge connects top - middle, middle - bottom2003# or top - bottom.2004# Note that all these dictionaries map points of GeneralisedStrata to each2005# other so we must take the corresponding stratum_number!2006clutch_dict = {}2007clutch_dict_lower = {}2008clutch_dict_long = {}2009# If the edges are not sorted with e[0] above e[1], we complain.2010for e in G.LG.edges:2011if G.LG.level_number(G.LG.levelofleg(e[0])) == 0:2012if G.LG.level_number(G.LG.levelofleg(e[1])) == 1:2013clutch_dict[top.stratum_number(2014G_to_top[e[0]])] = middle.stratum_number(e[1])2015else:2016assert G.LG.level_number(G.LG.levelofleg(e[1])) == 22017clutch_dict_long[top.stratum_number(2018G_to_top[e[0]])] = bottom.stratum_number(G_to_bot[e[1]])2019else:2020assert G.LG.level_number(G.LG.levelofleg(e[0])) == 12021assert G.LG.level_number(G.LG.levelofleg(e[1])) == 22022clutch_dict_lower[middle.stratum_number(2023e[0])] = bottom.stratum_number(G_to_bot[e[1]])2024return {2025'X': self,2026'top': top,2027'bottom': bottom,2028'middle': middle,2029'emb_dict_top': emb_dict_top,2030'emb_dict_mid': emb_dict_mid,2031'emb_dict_bot': emb_dict_bot,2032'clutch_dict': clutch_dict,2033'clutch_dict_lower': clutch_dict_lower,2034'clutch_dict_long': clutch_dict_long}20352036@cached_method2037def three_level_profile_for_level(self, enh_profile, l):2038"""2039Find the 3-level graph that has level l of enh_profile as its middle level.20402041A RuntimeError is raised if no unique (or no) 3-level graph is found.20422043INPUT:20442045enh_profile (tuple): enhanced profile2046l (int): (relative) level number20472048OUTPUT:20492050tuple: enhanced profile of the 3-level graph.2051"""2052profile, _ = enh_profile2053three_level_profile = (profile[l - 1], profile[l])2054# in case this is reducible, we have to find the correct enhanced2055# profile:2056possible_enhancements = len(self.lookup(three_level_profile))2057assert possible_enhancements > 0, "No 3-level graph for subprofile %r of %r found!" % (2058three_level_profile, profile)2059enhancements = []2060for i in range(possible_enhancements):2061if self.is_degeneration(enh_profile, (three_level_profile, i)):2062enhancements.append(i)2063if len(enhancements) != 1:2064raise RuntimeError(2065"No unique 3-level undegeneration in %r around level %r! %r" %2066(three_level_profile, l, enhancements))2067return (three_level_profile, enhancements[0])20682069# @cached_method2070def doublesplit_graph_before_and_after_level(self, enh_profile, l):2071"""2072Split the graph enh_profile directly above and below level l.20732074This can be used for gluing an arbitrary degeneration of level l into enh_profile.20752076The result is made so that it can be fed into clutch.20772078To ensure compatibility with top/bot/middle_to_bic when gluing, we have2079to make sure that everything is embedded into the "correct" generalised strata.20802081We denote the 3-level graph around level l by H.20822083Then the top part will be embedded into the top of the top BIC of H,2084the bottom part will be embedded into the bot of the bottom BIC of H2085and the middle will be the middle level of H.20862087For a 3-level graph is (almost) equivalent to doublesplit(), the only difference2088being that here we return the 0-level graph for each level.20892090Args:2091enh_profile (tuple): enhanced profile.2092l (int): (relative) level of enh_profile.20932094Raises:2095ValueError: Raised if l is 0 or lowest level.2096RuntimeError: Raised if we don't find a unique 3-level graph around l.20972098Returns:2099dict: A dictionary consisting of:2100X: GeneralisedStratum self.X,2101top: LevelStratum top level of top BIC of H,2102bottom: LevelStratum bottom level of bottom BIC of H,2103middle: LevelStratum middle level of H,2104emb_dict_top: dict: points of top stratum -> points of X,2105emb_dict_bot: dict: points of bottom stratum -> points of X,2106emb_dict_mid: dict: points of middle stratum -> points of X,2107clutch_dict: dict: points of top stratum -> points of middle stratum,2108clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,2109clutch_dict_long: dict: points of top stratum -> points of bottom stratum.21102111EXAMPLES::21122113sage: from admcycles.diffstrata import *2114sage: X=GeneralisedStratum([Signature((4,))])2115sage: 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))2116sage: X=GeneralisedStratum([Signature((2,2,-2))])2117sage: 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 time2118"""2119p, i = enh_profile2120if l == 0 or l == len(p) + 1:2121raise ValueError("Doublesplit must occur at 'inner' level! %r" % l)2122G = self.lookup_graph(p, i)2123# Split the graph into top and bottom components around level l:2124top_graph, se_top = self.sub_graph_from_level(2125enh_profile, l, direction='above', return_split_edges=True)2126bot_graph, se_bot = self.sub_graph_from_level(2127enh_profile, l + 1, direction='below', return_split_edges=True)2128# We construct the clutching info by splitting the 3-level graph around l2129# Note that the middle level is really the same as that of enh_profile (that's2130# why we have to care about components of the profile here), but the leg2131# numbering might be different, so we still have to work with an2132# undegeneration map:2133t_l_enh_profile = self.three_level_profile_for_level(enh_profile, l)2134clutching_info = self.doublesplit(t_l_enh_profile)2135assert top_graph.X == clutching_info['top']2136assert bot_graph.X == clutching_info['bottom']2137L = clutching_info['middle']2138assert L == self.lookup_graph(*t_l_enh_profile).level(1)2139# we simply replace the top and bottom components of B by our graphs:2140clutching_info['top'] = top_graph2141clutching_info['bottom'] = bot_graph2142# Now we have to match up the edges:2143# Note that se_top consists of the edges connecting top_graph to any vertex2144# on or below level l2145# We therefore start by distinguishing those edges ending on level l from the others2146# (long edges):2147# WARNING: We use here (once again) implicitly that e[0] is above e[1]!2148top_to_l = []2149top_to_bot = []2150for e in se_top:2151if G.LG.level_number(G.LG.levelofleg(e[1])) == l:2152top_to_l.append(e)2153else:2154top_to_bot.append(e)2155# the same for se_bot:2156bot_to_l = []2157bot_to_top = []2158for e in se_bot:2159if G.LG.level_number(G.LG.levelofleg(e[0])) == l:2160bot_to_l.append(e)2161else:2162bot_to_top.append(e)2163assert set(top_to_bot) == set(bot_to_top)2164# Translating the edges into points on the strata immediately gives the2165# three clutching dictionaries:2166# Note that instead of directly using leg_dict for the middle level,2167# we first pick an undegeneration map to the 3-level graph and compose2168# with (the inverse of) that:2169middle_leg_map = self.explicit_leg_maps(2170t_l_enh_profile, enh_profile, only_one=True)2171ep_to_m = {v: k for k, v in middle_leg_map.items()}2172# WARNING: We use here (once again) implicitly that e[0] is above e[1]!2173clutching_info['clutch_dict'] = {2174top_graph.dmp[e[0]]: L.leg_dict[ep_to_m[e[1]]] for e in top_to_l}2175clutching_info['clutch_dict_lower'] = {2176L.leg_dict[ep_to_m[e[0]]]: bot_graph.dmp[e[1]] for e in bot_to_l}2177clutching_info['clutch_dict_long'] = {2178top_graph.dmp[e[0]]: bot_graph.dmp[e[1]] for e in top_to_bot}2179return clutching_info21802181# @cached_method2182def splitting_info_at_level(self, enh_profile, l):2183"""2184Retrieve the splitting and embedding dictionaries for splitting at level l,2185as well as the level in 'standard form', i.e. as either:21862187* a top of a BIC2188* a bot of a BIC2189* a middle of a 3-level graph21902191This is essentially only a frontend for split_graph_at_level and2192doublesplit_graph_before_and_after_level and saves us the annoying2193case distinction.21942195This is important, because when we glue we should *always* use the2196dmp's of the splitting dictionary, which can (and will) be different2197from leg_dict of the level!21982199INPUT:22002201enh_profile (tuple): enhanced profile2202l (int): (relative) level number22032204OUTPUT:22052206tuple: (splitting dict, leg_dict, level) where2207splitting dict is the splitting dictionary:22082209* X: GeneralisedStratum self.X2210* top: EmbeddedLevelGraph: top component2211* bottom: EmbeddedLevelGraph: bottom component2212* clutch_dict: clutching dictionary mapping ex-half-edges on2213top to their partners on bottom (both as points in the2214respective strata via dmp!)2215* emb_dict_top: a dictionary embedding top into the stratum of self2216* emb_dict_bot: a dictionary embedding bot into the stratum of self22172218leg_dict is the dmp at the current level (to be used instead2219of leg_dict of G.level(l)!!!)22202221and level is the 'standardised' LevelStratum at l (as described above).22222223Note that clutch_dict, emb_top and emb_bot are dictionaries between2224points of strata, i.e. after applying dmp to the points!2225"""2226profile, _ = enh_profile2227# For this, we have to distinguish again, if we're gluing into the middle2228# (two cuts) or at one end of the profile (1 cut):2229if l == 0:2230d = self.split_graph_at_level(enh_profile, 1)2231assert d['top'].is_isomorphic(d['top'].X.smooth_LG)2232return d, d['top'].dmp, d['top'].X2233if l == len(profile):2234d = self.split_graph_at_level(enh_profile, l)2235assert d['bottom'].is_isomorphic(d['bottom'].X.smooth_LG)2236return d, d['bottom'].dmp, d['bottom'].X2237d = self.doublesplit_graph_before_and_after_level(enh_profile, l)2238three_level_profile = self.three_level_profile_for_level(2239enh_profile, l)2240assert self.lookup_graph(*three_level_profile).level(1) == d['middle']2241# for the middle level, we have to use the undegeneration map to2242# the 3-level graph:2243middle_leg_map = self.explicit_leg_maps(2244three_level_profile, enh_profile, only_one=True)2245L_to_m = {v: d['middle'].leg_dict[k] for k, v in middle_leg_map.items()2246if k in d['middle'].leg_dict}2247return d, L_to_m, d['middle']22482249@cached_method2250def enhanced_profiles_of_length(self, l, quiet=True):2251"""2252A little helper for generating all enhanced profiles in self of a given length.22532254Note that this generates the *entire* lookup_list first!2255For large strata this can take a long time!22562257Args:2258l (int): length (codim) of profiles to be generated.22592260Returns:2261tuple: tuple of enhanced profiles22622263EXAMPLES::22642265sage: from admcycles.diffstrata import *2266sage: X=Stratum((4,))2267sage: len(X.lookup_list[2])2268172269sage: len(X.enhanced_profiles_of_length(2))22701922712272"""2273if not quiet:2274print('Generating enhanced profiles of length %r...' % l)2275sys.stdout.flush()2276if l >= len(self.lookup_list):2277return tuple()2278ep_list = []2279for c, p in enumerate(self.lookup_list[l]):2280if not quiet:2281print('Building all graphs in %r (%r/%r)...' %2282(p, c + 1, len(self.lookup_list[l])))2283sys.stdout.flush()2284# quiet=False gives A LOT of output here...2285for i in range(len(self.lookup(p, quiet=True))):2286ep_list.append((p, i))2287return tuple(ep_list)22882289#########################################################2290# Checks2291#########################################################22922293def check_dims(self, codim=None, quiet=False):2294"""2295Check if, for each non-horizontal level graph of codimension codim2296the dimensions of the levels add up to the dimension of the level graph2297(dim of stratum - codim).22982299If codim is omitted, check the entire stratum.23002301EXAMPLES::23022303sage: from admcycles.diffstrata import *2304sage: X=GeneralisedStratum([Signature((1,1))])2305sage: X.check_dims()2306Codimension 0 Graph 0: Level sums ok!2307Codimension 1 Graph 0: Level sums ok!2308Codimension 1 Graph 1: Level sums ok!2309Codimension 1 Graph 2: Level sums ok!2310Codimension 1 Graph 3: Level sums ok!2311Codimension 2 Graph 0: Level sums ok!2312Codimension 2 Graph 1: Level sums ok!2313Codimension 2 Graph 2: Level sums ok!2314Codimension 2 Graph 3: Level sums ok!2315Codimension 3 Graph 0: Level sums ok!2316True23172318sage: X=GeneralisedStratum([Signature((4,))])2319sage: X.check_dims(quiet=True)2320True23212322sage: X=GeneralisedStratum([Signature((10,0,-10))])2323sage: X.check_dims()2324Codimension 0 Graph 0: Level sums ok!2325Codimension 1 Graph 0: Level sums ok!2326Codimension 1 Graph 1: Level sums ok!2327Codimension 1 Graph 2: Level sums ok!2328Codimension 1 Graph 3: Level sums ok!2329Codimension 1 Graph 4: Level sums ok!2330Codimension 1 Graph 5: Level sums ok!2331Codimension 1 Graph 6: Level sums ok!2332Codimension 1 Graph 7: Level sums ok!2333Codimension 1 Graph 8: Level sums ok!2334Codimension 1 Graph 9: Level sums ok!2335Codimension 1 Graph 10: Level sums ok!2336Codimension 1 Graph 11: Level sums ok!2337True23382339sage: X=GeneralisedStratum([Signature((2,2,-2))])2340sage: X.check_dims(quiet=True) # long time (3 seconds)2341True2342"""2343return_value = True2344if codim is None:2345codims = range(self.dim())2346else:2347codims = [codim]2348for c in codims:2349for i, emb_g in enumerate(self.all_graphs[c]):2350g = emb_g.LG2351dimsum = 02352if not quiet:2353print("Codimension", c, "Graph", repr(i) + ":", end=" ")2354for l in range(g.numberoflevels()):2355L = g.stratum_from_level(l)2356if L.dim() == -1:2357if quiet:2358print("Codimension", c, "Graph",2359repr(i) + ":", end=" ")2360print("Error: Level", l, "is of dimension -1!")2361return_value = False2362dimsum += L.dim()2363if dimsum != self.dim() - c:2364if quiet:2365print("Codimension", c, "Graph",2366repr(i) + ":", end=" ")2367print("Error: Level dimensions add up to",2368dimsum, "not", self.dim() - c, "!")2369return_value = False2370else:2371if not quiet:2372print("Level sums ok!")2373return return_value23742375###########2376# Chern class calculation:2377def psi(self, leg):2378"""2379CURRENTLY ONLY ALLOWED FOR CONNECTED STRATA!!!!23802381The psi class on the open stratum at leg.23822383Args:2384leg (int): leg number (as index of signature, not point of stratum!!!)23852386Returns:2387ELGTautClass: Tautological class associated to psi.2388"""2389psi = self.additive_generator([tuple(), 0], {leg: 1})2390return psi.as_taut()23912392# @cached_method2393def taut_from_graph(self, profile, index=0):2394"""2395Tautological class from the graph with enhanced profile (profile, index).23962397INPUT:23982399profile (iterable): profile2400index (int, optional): Index of profile. Defaults to 0.24012402OUTPUT:24032404ELGTautClass: Tautological class consisting just of this one graph.2405"""2406return self.additive_generator((tuple(profile), index)).as_taut()24072408def ELGsum(self, L):2409"""2410Sum of tautological classes.24112412This is generally faster than += (i.e. sum()), because reduce is only called2413once at the end and not at every step.24142415Args:2416L (iterable): Iterable of ELGTautClasses on self.24172418Returns:2419ELGTautClass: Sum over input classes.2420"""2421new_psi_list = []2422for T in L:2423if T == 0:2424continue2425new_psi_list.extend(T.psi_list)2426return admcycles.diffstrata.elgtautclass.ELGTautClass(2427self, new_psi_list)24282429def pow(self, T, k, amb=None):2430"""2431Calculate T^k with ambient amb.24322433Args:2434T (ELGTautClass): Tautological class on self.2435k (int): positive integer.2436amb (tuple, optional): enhanced profile. Defaults to None.24372438Returns:2439ELGTautClass: T^k in CH(amb).2440"""2441if amb is None:2442amb = ((), 0)2443ONE = self.ONE2444else:2445ONE = self.taut_from_graph(*amb)2446prod = ONE2447for _ in range(k):2448prod = self.intersection(prod, T, amb)2449return prod24502451def exp(self, T, amb=None, quiet=True, prod=True, stop=None):2452"""2453(Formal) exp of a Tautological Class.24542455This is done (by default) by calculating exp of every AdditiveGenerator2456(which is cached) and calculating the product of these.24572458Alternatively, prod=False computes sums of powers of T.24592460Args:2461T (ELGTautClass): Tautological Class on X.24622463Returns:2464ELGTautClass: Tautological Class on X.2465"""2466N = self.dim()2467if amb is None:2468amb = ((), 0)2469if not prod:2470if not quiet:2471print("Calculating exp of %s..." % T)24722473def _status(i):2474# primitive, but whatever2475if not quiet:2476print("Calculating power %r..." % i)2477return 12478return self.ELGsum(2479[_status(i) * QQ((1, factorial(i))) * self.pow(T, i, amb)2480for i in range(N + 1)])2481# Calculate instead product of exp(AG):2482e = self.taut_from_graph(*amb)2483if not quiet:2484print("Calculating exp as product of %r factors..." %2485len(T.psi_list), end=' ')2486sys.stdout.flush()2487for c, AG in T.psi_list:2488f = AG.exp(c, amb, stop)2489if f == 0 or f == self.ZERO:2490return self.ZERO2491e = self.intersection(e, f, amb)2492if not quiet:2493print('Done!')2494return e24952496@cached_method2497def exp_bic(self, i):2498l = self.bics[i].ell2499AG = self.additive_generator(((i,), 0))2500return AG.exp(l, amb=None) - self.ONE25012502def td_contrib(self, l, T, amb=None):2503"""2504(Formal) td^-1 contribution, i.e. (1-exp(-l*T))/T.25052506Args:2507l (int): weight2508T (ELGTautClass): Tautological class on self.25092510Returns:2511ELGTautClass: Tautological class on self.2512"""2513N = self.dim()2514if amb is None:2515amb = ((), 0)2516return self.ELGsum([QQ(-l)**k / QQ(factorial(k + 1)) *2517self.pow(T, k, amb) for k in range(N + 1)])25182519@property2520def xi(self):2521"""2522xi of self in terms of psi and BICs according to Sauvaget's formula.25232524Note that we first find an "optimal" leg.25252526Returns:2527ELGTautClass: psi class on smooth stratum + BIC contributions (all2528with multiplicities...)25292530EXAMPLES::25312532sage: from admcycles.diffstrata import *2533sage: X=Stratum((2,))2534sage: print(X.xi) # 'unsafe' (order of summands might change) # doctest:+SKIP2535Tautological class on Stratum: (2,)2536with residue conditions: []2537<BLANKLINE>25383 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +2539-1 * Graph ((0,), 0) +2540-1 * Graph ((1,), 0) +2541<BLANKLINE>2542"""2543try:2544return self._xi2545except AttributeError:2546self._xi = self.xi_with_leg(quiet=True)2547return self._xi25482549@cached_method2550def xi_pow(self, n):2551"""2552Cached method for calculating powers of xi.25532554Args:2555n (int): non-negative integer (exponent)25562557Returns:2558ELGTautClass: xi^n2559"""2560if n == 0:2561return self.ONE2562return self.xi * self.xi_pow(n - 1)25632564@cached_method2565def xi_with_leg(self, leg=None, quiet=True, with_leg=False):2566"""2567xi class of self expressed using Sauvaget's relation (with optionally a choice of leg)25682569INPUT:25702571leg (tuple, optional): leg on self, i.e. tuple (i,j) for the j-th element2572of the signature of the i-th component. Defaults to None. In this case,2573an optimal leg is chosen.25742575quiet (bool, optional): No output. Defaults to False.25762577with_leg (bool, optional): Return choice of leg. Defaults to False.25782579OUTPUT:25802581ELGTautClass: xi in terms of psi and bics according to Sauvaget.2582(ELGTautClass, tuple): if with_leg=True, where tuple is the corresponding2583leg on the level i.e. (component, signature index) used.25842585EXAMPLES:25862587In the stratum (2,-2) the pole is chosen by default (there is no 'error term')::25882589sage: from admcycles.diffstrata import *2590sage: X=Stratum((2,-2))2591sage: print(X.xi)2592Tautological class on Stratum: (2, -2)2593with residue conditions: []2594<BLANKLINE>2595-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +2596<BLANKLINE>2597sage: print(X.xi_with_leg(leg=(0,1)))2598Tautological class on Stratum: (2, -2)2599with residue conditions: []2600<BLANKLINE>2601-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +2602<BLANKLINE>26032604We can specify the zero instead and pick up the extra divisor::26052606sage: print(X.xi_with_leg(leg=(0,0))) # 'unsafe' (order of summands might change) # doctest:+SKIP2607Tautological class on Stratum: (2, -2)2608with residue conditions: []2609<BLANKLINE>26103 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +2611-1 * Graph ((0,), 0) +2612<BLANKLINE>2613"""2614if not quiet:2615print(2616"Applying Sauvaget's relation to express xi for %r..." %2617self)2618if leg is None:2619# choose a "good" leg:2620l, k, bot_bic_list = self._choose_leg_for_sauvaget_relation(quiet)2621else:2622l = leg2623k = self._sig_list[l[0]].sig[l[1]]2624bot_bic_list = self.bics_with_leg_on_bottom(l)2625# find internal leg number on smooth graph corresponding to l:2626G = self.lookup_graph(tuple())2627internal_leg = G.dmp_inv[l] # leg number on graph2628xi = (k + 1) * self.psi(internal_leg)2629add_gens = [self.additive_generator([(b,), 0]) for b in bot_bic_list]2630self._xi = xi + admcycles.diffstrata.elgtautclass.ELGTautClass(2631self, [(-self.bics[bot_bic_list[i]].ell, AG) for i, AG in enumerate(add_gens)])2632# self._xi = xi + sum([QQ(1)/QQ(AG.stack_factor)*AG.as_taut() \2633# for i, AG in enumerate(add_gens)])2634if with_leg:2635return (self._xi, l)2636else:2637return self._xi26382639def _choose_leg_for_sauvaget_relation(self, quiet=True):2640"""2641Choose the best leg for Sauvaget's relation, i.e. the one that appears on bottom2642level for the fewest BICs.26432644Returns:2645tuple: tuple (leg, order, bic_list) where:2646* leg (tuple), as a tuple (number of conn. comp., index of the signature tuple),2647* order (int) the order at leg, and2648* bic_list (list of int) is a list of indices of self.bics where leg2649is on bottom level.26502651EXAMPLES::26522653sage: from admcycles.diffstrata import *2654sage: X=Stratum((2,-2))2655sage: X._choose_leg_for_sauvaget_relation()2656((0, 1), -2, [])26572658In the minimal stratum, we always find all BICS:26592660sage: X=Stratum((2,))2661sage: X._choose_leg_for_sauvaget_relation()2662((0, 0), 2, [0, 1])2663"""2664best_case = len(self.bics)2665best_leg = -12666# points of the stratum are best accessed through the embedding of the smooth graph:2667# (we sort for better testing...)2668leg_list = sorted(list(self.smooth_LG.dmp_inv.keys()),2669key=lambda x: x[1])2670for l in leg_list:2671bot_list = self.bics_with_leg_on_bottom(l)2672# none is best we can do:2673if not bot_list:2674order = self._sig_list[l[0]].sig[l[1]]2675if not quiet:2676print(2677"Choosing leg %r (of order %r) because it never appears on bottom level." %2678(l, order))2679return (l, order, [])2680on_bottom = len(bot_list)2681if on_bottom <= best_case:2682best_case = on_bottom2683best_leg = l2684best_bot_list = bot_list[:] # copy!2685assert best_leg != -1, "No best leg found for %r!" % self2686order = self._sig_list[best_leg[0]].sig[best_leg[1]]2687if not quiet:2688print(2689"Choosing leg %r (of order %r), because it only appears on bottom %r out of %r times." %2690(best_leg, order, best_case, len(2691self.bics)))2692return (best_leg, order, best_bot_list)26932694def bics_with_leg_on_bottom(self, l):2695"""2696A list of BICs where l is on bottom level.26972698Args:2699l (tuple): leg on self (i.e. (i,j) for the j-th element of the signature2700of the i-th component)27012702Returns:2703list: list of indices self.bics27042705EXAMPLES::27062707sage: from admcycles.diffstrata import *2708sage: X=GeneralisedStratum([Signature((2,))])2709sage: X.bics_with_leg_on_bottom((0,0))2710[0, 1]2711"""2712bot_list = []2713# the corresponding point on each EmbeddedLevelGraph is leg2714for i, B in enumerate(self.bics):2715# reminder: l is leg on stratum, i.e. (i,j)2716# dmp_inv maps this to a leg on a graph (integer)2717leg = B.dmp_inv[l]2718leg_level = B.dlevels[B.LG.levelofleg(leg)]2719assert leg_level in [27200, -1], "Leg %r of BIC %r is not on level 0 or -1!" % (leg, B)2721if leg_level == -1:2722bot_list.append(i)2723return bot_list27242725@cached_method2726def xi_at_level(self, l, enh_profile, leg=None, quiet=True):2727"""2728Pullback of xi on level l to enh_profile.27292730This corresponds to xi_Gamma^[i] in the paper.27312732RuntimeError raised if classes produced by xi on the level have2733unexpected codimension. ValueError is raised if the leg provided is not2734found on the level.27352736INPUT:27372738l (int): level number (0,...,codim)2739enh_profile (tuple): enhanced profile2740leg (int, optional): leg (as a leg of enh_profile!!!), to be used2741in Sauvaget's relation. Defaults to None, i.e. optimal choice.27422743OUTPUT:27442745ELGTautClass: tautological class consisting of psi classes on2746enh_profile and graphs with oner more level.27472748EXAMPLES:27492750Compare multiplication with xi to xi_at_level (for top-degree)::27512752sage: from admcycles.diffstrata import *2753sage: X=Stratum((2,-2,0))2754sage: assert all(X.xi_at_level(0, ((i,),0)) == X.xi*X.taut_from_graph((i,)) for i in range(len(X.bics)))2755"""2756if enh_profile == ((), 0):2757assert l == 02758if leg:2759level_leg = self.smooth_LG.dmp[leg]2760return self.xi_with_leg(level_leg)2761return self.xi2762# we need to use splitting info instead of direct level extraction,2763# because the embeddings might differ by an automorphism!2764d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)2765inv_leg_dict = {v: k for k, v in leg_dict.items()}2766assert set(leg_dict.values()) == set(L.leg_dict.values())2767if leg is None:2768l_xi, level_leg = L.xi_with_leg(with_leg=True, quiet=quiet)2769else:2770if not (leg in leg_dict):2771raise ValueError('Leg %r is not on level %r of %r!' %2772(leg, l, enh_profile))2773level_leg = leg_dict[leg]2774l_xi = L.xi_with_leg(level_leg, quiet=quiet)2775taut_list = []2776if l_xi == 0:2777return self.ZERO2778for c, AG in l_xi.psi_list:2779if AG.codim == 0:2780# psi class on L:2781new_leg_dict = {}2782for AGleg in AG.leg_dict:2783leg_on_G = inv_leg_dict[L.smooth_LG.dmp[AGleg]]2784new_leg_dict[leg_on_G] = AG.leg_dict[AGleg]2785next_taut = (c, self.additive_generator(2786enh_profile, leg_dict=new_leg_dict))2787elif AG.codim == 1:2788coeff, glued_AG = self.glue_AG_at_level(AG, enh_profile, l)2789next_taut = (c * coeff, glued_AG)2790else:2791raise RuntimeError(2792"Classes in xi should all be of codim 0 or 1! %s" % l_xi)2793taut_list.append(next_taut)2794return admcycles.diffstrata.elgtautclass.ELGTautClass(self, taut_list)27952796@cached_method2797def glue_AG_at_level(self, AG, enh_profile, l):2798"""2799Glue an AdditiveGenerator into level l of enh_profile.28002801Note that AG must be an AdditiveGenerator on the level obtained via2802self.splitting_info_at_level!28032804Currently this is only implemented for graphs (and only really tested2805for BICs!!!)28062807TODO: Test for AGs that are not BICs and psi classes.28082809Args:2810AG (AdditiveGenerator): AdditiveGenerator on level2811enh_profile (tuple): enhanced profile of self.2812l (int): level number of enh_profile.28132814Raises:2815RuntimeError: raised if the new profile is empty.28162817Returns:2818tuple: A tuple consisting of the stackfactor (QQ) and the2819AdditiveGenerator of the glued graph.2820"""2821# TODO: Check if longer profiles work + psis!2822#2823# First, we figure out the profile of the new graph of self.2824# For this, we must translate the profile (inside L) of the AG2825# into an extended profile (of self) as a degeneration of enh_profile:2826profile, _comp = enh_profile2827AGprofile, AGcomp = AG.enh_profile2828# We start by deciding where something must be inserted into enh_profile:2829#2830# We observe that level l is either:2831# * B^top of the first BIC in profile (level 0),2832# * B^bot of the last BIC in profile (lowest level), or2833# * the middle of the 3-level graph (profile[l-1],profile[l]).2834#2835# There is also the "degenerate case" of an empty profile that2836# we should exclude first:2837if len(profile) == 0:2838assert l == 02839# level stratum == stratum2840# stack_factor = QQ(AG.stack_factor)2841return (1, self.additive_generator((AGprofile, AGcomp)))2842elif l == 0:2843new_bics = [self.DG.top_to_bic(2844profile[l])[bic_index] for bic_index in AGprofile]2845elif l == len(profile):2846new_bics = [self.DG.bot_to_bic(2847profile[l - 1])[bic_index] for bic_index in AGprofile]2848else: # we are in the middle of the corresponding 3-level graph:2849three_level_profile, enhancement = self.three_level_profile_for_level(2850enh_profile, l)2851new_bics = [self.DG.middle_to_bic((three_level_profile, enhancement))[2852bic_index] for bic_index in AGprofile]2853p = list(profile)2854p = tuple(p[:l] + new_bics + p[l:])2855# Now we know the profile, we have to figure out, which component2856# we're on.2857# For this, we split the enh_profile apart, replace one part by the BIC and2858# and glue it back together again.2859comp_list = []2860assert len(self.lookup(p)) > 0, "Error: Glued into empty profile %r" % p2861# The splitting information and the level in 'standard form' (i.e. one2862# of the three above possibilities), is given by2863# splitting_info_at_level:2864d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)2865if AG._X is not L:2866print(2867"Warning! Additive Generator should live on level %r of %r! I hope you know what you're doing...." %2868(l, enh_profile))2869# We first build the "big" graph, i.e. glue in the AG.2870# For this, we have to distinguish again, if we're gluing into the middle2871# (two cuts) or at one end of the profile (1 cut):2872if l == 0:2873assert d['top'].X is L2874# we glue into top:2875d['top'] = d['top'].X.lookup_graph(*AG.enh_profile)2876elif l == len(profile):2877assert d['bottom'].X is L2878# we glue into bottom:2879d['bottom'] = d['bottom'].X.lookup_graph(*AG.enh_profile)2880else:2881assert d['middle'] is L2882# we glue into middle:2883d['middle'] = d['middle'].lookup_graph(*AG.enh_profile)2884glued_graph = admcycles.diffstrata.stratatautring.clutch(**d)2885# Now we check the components of p for glued_graph:2886for i, H in enumerate(self.lookup(p)):2887if glued_graph.is_isomorphic(H):2888comp_list.append(i)2889if len(comp_list) != 1:2890raise RuntimeError("%r is not a unique degeneration of %r! %r" % (2891p, enh_profile, comp_list))2892i = comp_list[0]2893glued_AG = self.additive_generator((p, i))2894GAG = self.additive_generator(enh_profile)2895stack_factor = 12896for i in range(len(AGprofile)):2897stack_factor *= QQ(self.bics[new_bics[i]].ell) / \2898QQ(L.bics[AGprofile[i]].ell)2899stack_factor *= QQ(len(glued_graph.automorphisms)) / \2900QQ(len(AG._G.automorphisms) * len(GAG._G.automorphisms))2901return (stack_factor, glued_AG)29022903def calL(self, enh_profile=None, l=0):2904"""2905The error term of the normal bundle on level l of enh_profile * -ll2906(pulled back to enh_profile)29072908Args:2909enh_profile (tuple, optional): enhanced profile. Defaults to None.2910l (int, optional): level. Defaults to 0.29112912Returns:2913ELGTautClass: Tautological class on self2914"""2915result = []2916if enh_profile is None or enh_profile == ((), 0):2917for i, B in enumerate(self.bics):2918ll = self.bics[i].ell2919result.append(ll * self.taut_from_graph((i,)))2920else:2921# Morally, L = G.level(squished_level)2922# but we have to use splitting_info_at_level to glue in safely!2923d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)2924for i, B in enumerate(L.bics):2925BAG = L.additive_generator(((i,), 0))2926sf, glued_AG = self.glue_AG_at_level(BAG, enh_profile, l)2927coeff = QQ(sf * B.ell)2928result.append(coeff * glued_AG.as_taut())2929if not result:2930return self.ZERO2931return self.ELGsum(result)29322933##############################################################2934# SEC 9 FORMULAS #2935##############################################################2936# The following formulas check various identities used in #2937# and around sec 9 of the paper. They also serve as examples #2938# for the methods introduced above. #2939##############################################################29402941@property2942def c1_E(self):2943"""2944The first chern class of Omega^1(log) (Thm 1.1).29452946OUTPUT:29472948ELGTautClass: c_1(E) according to Thm 1.1.2949"""2950N = self.dim() + 12951c1E = [N * self.xi]2952for i, B in enumerate(self.bics):2953Ntop = B.top.dim() + 12954l = B.ell2955c1E.append(((N - Ntop) * l) * self.taut_from_graph((i,)))2956return self.ELGsum(c1E)29572958@property2959def c2_E(self):2960"""2961A direct formula for the second Chern class.29622963Returns:2964ELGTautClass: c_2 of the Tangent bundle of self.2965"""2966N = QQ(self.dim() + 1)2967c2E = [N * (N - 1) / QQ(2) * (self.xi_pow(2))]2968for i, B in enumerate(self.bics):2969Ntop = B.top.dim() + 12970xitop = self.xi_at_level(0, ((i,), 0))2971xibot = self.xi_at_level(1, ((i,), 0))2972l = QQ(B.ell)2973c2E.append(l / 2 * ((N * (N - 1) - Ntop * (Ntop - 1)) * xitop +2974((N - Ntop)**2 + Ntop - N) * xibot))2975for ep in self.enhanced_profiles_of_length(2):2976p, _ = ep2977delta0 = self.bics[p[0]]2978delta1 = self.bics[p[1]]2979Nd0 = delta0.top.dim() + 12980Nd1 = delta1.top.dim() + 12981ld0 = QQ(delta0.ell)2982ld1 = QQ(delta1.ell)2983factor = QQ(1) / QQ(2) * ld0 * ld1 * \2984(N * (N - 2 * Nd0) - Nd1 * (Nd1 - 2 * Nd0) - N + Nd1)2985c2E.append(factor * self.taut_from_graph(*ep))2986return self.ELGsum(c2E)29872988@cached_method2989def ch1_pow(self, n):2990"""2991A direct formula for powers of ch_129922993Args:2994n (int): exponent29952996Returns:2997ELGTautClass: ch_1(T)^n2998"""2999N = QQ(self.dim() + 1)3000chpow = [QQ(N**n) / QQ(factorial(n)) * self.xi_pow(n)]3001for L in range(1, n + 1):3002summand = []3003for ep in self.enhanced_profiles_of_length(L):3004p, _ = ep3005delta = [self.bics[b] for b in p]3006ld = [B.ell for B in delta]3007Nd = [B.top.dim() + 1 for B in delta]3008exi = self.exp(N * self.xi_at_level(0, ep), amb=ep)3009factor = 13010td_prod = self.taut_from_graph(*ep)3011for i in range(L):3012factor *= (N - Nd[i]) * ld[i]3013td_prod = self.intersection(td_prod,3014self.td_contrib(-ld[i] * (N - Nd[i]),3015self.cnb(ep, ep, self.squish(ep, i)), ep),3016ep)3017prod = self.intersection(exi, td_prod, ep)3018summand.append(factor * prod.degree(n))3019chpow.append(self.ELGsum(summand))3020return factorial(n) * self.ELGsum(chpow)30213022@property3023def ch2_E(self):3024"""3025A direct formula for ch_2.30263027Returns:3028ELGTautClass: ch_23029"""3030N = QQ(self.dim() + 1)3031ch2E = [N / QQ(2) * (self.xi_pow(2))]3032for i, B in enumerate(self.bics):3033Ntop = B.top.dim() + 13034xitop = self.xi_at_level(0, ((i,), 0))3035xibot = self.xi_at_level(1, ((i,), 0))3036l = QQ(B.ell)3037ch2E.append(l / 2 * ((N - Ntop) * (xitop + xibot)))3038for ep in self.enhanced_profiles_of_length(2):3039p, _ = ep3040delta0 = self.bics[p[0]]3041delta1 = self.bics[p[1]]3042Nd1 = delta1.top.dim() + 13043ld0 = QQ(delta0.ell)3044ld1 = QQ(delta1.ell)3045factor = QQ(1) / QQ(2) * ld0 * ld1 * (N - Nd1)3046ch2E.append(factor * self.taut_from_graph(*ep))3047return self.ELGsum(ch2E)30483049def ch_E_alt(self, d):3050"""3051A formula for the Chern character.30523053Args:3054d (int): cut-off degree30553056Returns:3057ELGTautClass: sum of ch_0 to ch_d.3058"""3059N = QQ(self.dim() + 1)3060ch_E = [N / QQ(factorial(d)) * self.xi_pow(d)]3061for L in range(1, d + 1):3062summand = []3063for ep in self.enhanced_profiles_of_length(L):3064p, _ = ep3065ld = [self.bics[b].ell for b in p]3066Nd = self.bics[p[-1]].top.dim() + 13067ld_prod = 13068for l in ld:3069ld_prod *= l3070factor = ld_prod * (N - Nd)3071td_prod = self.ONE3072for i in range(L):3073td_prod = self.intersection(3074td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)3075inner_sum = []3076for j in range(d - L + 1):3077pr = self.intersection(self.pow(self.xi_at_level(30780, ep), j, ep), td_prod.degree(d - j), ep)3079inner_sum.append(QQ(1) / QQ(factorial(j)) * pr)3080summand.append(factor * self.ELGsum(inner_sum))3081ch_E.append(self.ELGsum(summand))3082return self.ELGsum(ch_E)30833084@cached_method3085def exp_xi(self, quiet=True):3086"""3087Calculate exp(xi) using that no powers higher than 2g appear for connected3088holomorphic strata.30893090Args:3091quiet (bool, optional): No output. Defaults to True.30923093Returns:3094ELGTautClass: exp(xi)3095"""3096if not self._polelist and len(self._g) == 1:3097stop = 2 * self._g[0]3098else:3099stop = None3100if not quiet:3101if stop:3102stop_str = stop - 13103else:3104stop_str = stop3105print('Stoping exp(xi) at degree %r' % stop_str)3106return self.exp(self.xi, quiet=quiet, stop=stop)31073108def xi_at_level_pow(self, level, enh_profile, exponent):3109"""3110Calculate powers of xi_at_level (using ambient enh_profile).31113112Note that when working with xi_at_level on enh_profile, multiplication3113should always take place in CH(enh_profile), i.e. using intersection3114instead of ``*``. This is simplified for powers by this method.31153116Moreover, by Sauvaget, xi^n = 0 for n >= 2g for connected holomorphic3117strata, so we check this before calculating.31183119INPUT:31203121level (int): level of enh_profile.3122enh_profile (tuple): enhanced profile of self.3123exponent (int): exponent31243125OUTPUT:31263127ELGTautClass: Pushforward of (xi_{enh_profile}^[l])^n to self.3128"""3129G = self.lookup_graph(*enh_profile)3130L = G.level(level)3131if not L._polelist and len(L._g) == 1:3132if exponent >= 2 * L._g[0]:3133return self.ZERO3134if enh_profile == ((), 0):3135assert level == 03136return self.xi_pow(exponent)3137# ambient!3138power = self.taut_from_graph(*enh_profile)3139# maybe consecutive squaring is better? Seems that it isn't :/3140# xi = self.xi_at_level(level, enh_profile)3141# def _rec(x, n):3142# if n == 0:3143# return self.taut_from_graph(*enh_profile)3144# if n == 1:3145# return x3146# if n % 2 == 0:3147# return _rec(self.intersection(x, x, enh_profile), n // 2)3148# return self.intersection(x, _rec(self.intersection(x, x, enh_profile), (n - 1) // 2), enh_profile)3149# return _rec(xi, exponent)3150xi = self.xi_at_level(level, enh_profile)3151for _ in range(exponent):3152power = self.intersection(power, xi, enh_profile)3153return power31543155@cached_method3156def exp_L(self, quiet=True):3157"""3158exp(calL)31593160Args:3161quiet (bool, optional): No output. Defaults to True.31623163Returns:3164ELGTautClass: exp(calL)3165"""3166return self.exp(self.calL(), quiet=quiet)31673168@property3169def P_B(self):3170"""3171The twisted Chern character of self, see sec 9 of the paper.31723173Returns:3174ELGTautClass: class of P_B3175"""3176# Prop. 9.23177N = QQ(self.dim() + 1)3178PB = [N * self.exp_xi() + (-1) * self.ONE]3179for L in range(1, N):3180inner = []3181for enh_profile in self.enhanced_profiles_of_length(L):3182p, _ = enh_profile3183B = self.bics[p[0]]3184Ntop = B.top.dim() + 13185summand = (-1)**L * (Ntop * self.exp_xi() + (-1) * self.ONE)3186prod_list = []3187for i in range(L):3188ll = self.bics[p[i]].ell3189squish = self.squish(enh_profile, i)3190td_NB = ll * \3191self.td_contrib(ll, self.cnb(3192enh_profile, enh_profile, squish), enh_profile)3193prod_list.append(td_NB)3194if prod_list:3195prod = prod_list[0]3196for f in prod_list[1:]:3197# multiply with ambient Gamma (=enh_profile)!3198prod = self.intersection(prod, f, enh_profile)3199const = prod.degree(0)3200prod += (-1) * const3201summand *= (prod + const *3202self.taut_from_graph(*enh_profile))3203inner.append(summand)3204PB.append(self.ELGsum(inner))3205return self.ELGsum(PB)32063207def charToPol(self, ch, upto=None, quiet=True):3208"""3209Newton's identity to recursively translate the Chern character into the3210Chern polynomial.32113212Args:3213ch (ELGTautClass): Chern character3214upto (int, optional): Calculate polynomial only up to this degree. Defaults to None (full polynomial).3215quiet (bool, optional): No output. Defaults to True.32163217Returns:3218list: Chern polynomial as list of ELGTautClasses (indexed by degree)3219"""3220if not quiet:3221print('Starting charToPol...')3222C = ch.list_by_degree()3223# throw out factorials:3224p = [factorial(k) * c for k, c in enumerate(C)]3225# calculate recursively using Newton's identity:3226E = [self.ONE]3227if upto is None:3228upto = self.dim()3229for k in range(1, upto + 1):3230if not quiet:3231print('Calculating c_%r...' % k)3232ek = []3233for i in range(1, k + 1):3234ek.append((-1)**(i - 1) * E[k - i] * p[i])3235E.append(QQ(1) / QQ(k) * self.ELGsum(ek))3236return E32373238def top_chern_class_alt(self, quiet=True):3239"""3240Top chern class from Chern polynomial.32413242Args:3243quiet (bool, optional): No output. Defaults to True.32443245Returns:3246ELGTautClass: c_top of the tangent bundle of self.3247"""3248ch = self.ch_E_fast(quiet=quiet).list_by_degree()3249top_c = []3250N = self.dim()3251for p in partitions(N):3252l = sum(p.values())3253factor = (-1)**(N - l)3254# for r, n in enumerate(p.values()):3255# factor *= QQ(factorial(r)**n)/QQ(factorial(n))3256ch_prod = self.ONE3257for i, n in p.items():3258factor *= QQ(factorial(i - 1)**n) / QQ(factorial(n))3259if i == 1:3260ch_prod *= self.ch1_pow(n)3261else:3262ch_prod *= ch[i]**n3263top_c.append(factor * ch_prod)3264return self.ELGsum(top_c)32653266def top_chern_class_direct(self, quiet=True):3267"""3268A direct formula for the top Chern class using only xi_at_level.32693270Args:3271quiet (bool, optional): No output. Defaults to True.32723273Returns:3274ELGTautClass: c_top of the Tangent bundle of self.3275"""3276N = self.dim()3277top_c = []3278for L in range(N + 1):3279if not quiet:3280print('Going through %r profiles of length %r...' %3281(len(self.enhanced_profiles_of_length(L)), L))3282summand = []3283for ep in self.enhanced_profiles_of_length(L):3284p, _ = ep3285ld = [self.bics[b].ell for b in p]3286ld_prod = 13287for l in ld:3288ld_prod *= l3289inner = []3290for K in WeightedIntegerVectors(N - L, [1] * (L + 1)):3291xi_prod = self.taut_from_graph(*ep)3292for i, k in enumerate(K):3293xi_prod = self.intersection(3294xi_prod, self.xi_at_level_pow(i, ep, k), ep)3295inner.append((K[0] + 1) * xi_prod)3296summand.append(ld_prod * self.ELGsum(inner))3297top_c.append(self.ELGsum(summand))3298return self.ELGsum(top_c)32993300def top_xi_at_level_comparison(self, ep, quiet=False):3301"""3302Comparison of level-wise computation vs xi_at_level.33033304Args:3305ep (tuple): enhanced profile3306quiet (bool, optional): no output. Defaults to False.33073308Returns:3309bool: Should always be True.33103311EXAMPLES::33123313sage: from admcycles.diffstrata import *3314sage: X=Stratum((2,))3315sage: 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))3316"""3317N = self.dim()3318p, _ = ep3319L = len(p)3320ld = [self.bics[b].ell for b in p]3321Nvec = [self.bics[b].top.dim() + 1 for b in p]3322Nvec.append(N + 1)3323ld_prod = 13324for l in ld:3325ld_prod *= l3326xi_prod = self.xi_at_level_pow(0, ep, Nvec[0] - 1)3327for i in range(1, L + 1):3328xi_prod = self.intersection(3329xi_prod, self.xi_at_level_pow(i, ep, Nvec[i] - Nvec[i - 1] - 1), ep)3330xi_at_level_prod = (Nvec[0] * xi_prod).evaluate(quiet=True)3331if not quiet:3332print("Product of xis at levels: %r" % xi_at_level_prod)3333G = self.lookup_graph(*ep)3334AG = self.additive_generator(ep)3335top_xi_at_level = [3336(G.level(i).xi_at_level_pow(33370, ((), 0), G.level(i).dim())).evaluate(3338quiet=True) for i in range(3339L + 1)]3340if not quiet:3341print(top_xi_at_level)3342prod = Nvec[0]3343for x in top_xi_at_level:3344prod *= x3345tot_prod = AG.stack_factor * prod3346if not quiet:3347print("Stack factor: %r" % AG.stack_factor)3348print("Product: %r" % prod)3349print("Total product: %r" % tot_prod)3350return tot_prod == xi_at_level_prod33513352def top_xi_at_level(self, ep, level, quiet=True):3353"""3354Evaluate the top xi power on a level.33553356Note that this is _not_ computed on self but on the GeneralisedStratum3357corresponding to level l of ep (the result is a number!).33583359Moreover, all results are cached and the cache is synchronised with3360the ``XI_TOPS`` cache.33613362The key for the cache is L.dict_key (where L is the LevelStratum).33633364Args:3365ep (tuple): enhanced profile3366level (int): level number of ep3367quiet (bool, optional): No output. Defaults to True.33683369Returns:3370QQ: integral of the top xi power against level l of ep.33713372EXAMPLES::33733374sage: from admcycles.diffstrata import *3375sage: X=Stratum((2,))3376sage: X.top_xi_at_level(((),0), 0)3377-1/64033783379TESTS:33803381Check that the cache can be deactivated and reactivated::33823383sage: from admcycles.diffstrata import Stratum3384sage: import admcycles.diffstrata.cache3385sage: tmp = admcycles.diffstrata.cache.TOP_XIS3386sage: admcycles.diffstrata.cache.TOP_XIS = admcycles.diffstrata.cache.FakeCache()3387sage: X = Stratum((2,))3388sage: X.top_xi_at_level(((),0), 0) is X.top_xi_at_level(((),0), 0)3389False3390sage: admcycles.diffstrata.cache.TOP_XIS = tmp3391sage: X.top_xi_at_level(((),0), 0) is X.top_xi_at_level(((),0), 0)3392True3393"""3394G = self.lookup_graph(*ep)3395L = G.level(level)3396key = L.dict_key()3397from .cache import TOP_XIS3398if key not in TOP_XIS:3399N = L.dim()3400if not quiet:3401print('(calc)', end=' ')3402sys.stdout.flush()3403top_xi = L.xi_at_level_pow(0, ((), 0), N)3404answer = TOP_XIS[key] = top_xi.evaluate(quiet=True)3405return answer3406else:3407if not quiet:3408print('(cache)', end=' ')3409sys.stdout.flush()3410return TOP_XIS[key]34113412def euler_char_immediate_evaluation(self, quiet=True):3413"""3414Calculate the (Orbifold) Euler characteristic of self by evaluating top xi3415powers on levels.34163417This is (by far) the fastest way of computing Euler characteristics.34183419Note that only combinatorial information about the degeneration graph3420of self is used (enhanced_profiles_of_length(L)) and top_xi_at_level3421the values of which are cached and synched with ``TOP_XIS`` cache.34223423Args:3424quiet (bool, optional): No output. Defaults to True.34253426Returns:3427QQ: (Orbifold) Euler characteristic of self.34283429EXAMPLES::34303431sage: from admcycles.diffstrata import *3432sage: X=Stratum((2,))3433sage: X.euler_char_immediate_evaluation()3434-1/403435"""3436N = self.dim()3437ec = 03438for L in range(N + 1):3439if not quiet:3440total = len(self.enhanced_profiles_of_length(L, quiet=False))3441print('Going through %r profiles of length %r...' % (total, L))3442for i, ep in enumerate(self.enhanced_profiles_of_length(L)):3443if not quiet:3444print('%r / %r, %r:' % (i + 1, total, ep), end=' ')3445sys.stdout.flush()3446p, _ = ep3447ld = [self.bics[b].ell for b in p]3448if p:3449NGammaTop = self.bics[p[0]].top.dim() + 13450else:3451NGammaTop = N + 13452ld_prod = 13453for l in ld:3454ld_prod *= l3455AG = self.additive_generator(ep)3456prod = ld_prod * NGammaTop * AG.stack_factor3457if not quiet:3458print("Calculating xi at", end=' ')3459sys.stdout.flush()3460for i in range(L + 1):3461if not quiet:3462print('level %r' % i, end=' ')3463sys.stdout.flush()3464prod *= self.top_xi_at_level(ep, i, quiet=quiet)3465if prod == 0:3466if not quiet:3467print("Product 0.", end=' ')3468sys.stdout.flush()3469break3470if not quiet:3471print('Done.')3472sys.stdout.flush()3473ec += prod3474return (-1)**N * ec34753476def euler_characteristic(self):3477"""3478Calculate the (Orbifold) Euler characteristic of self by evaluating top xi3479powers on levels. See also euler_char_immediate_evaluation.34803481Returns:3482QQ: (Orbifold) Euler characteristic of self.34833484EXAMPLES::34853486sage: from admcycles.diffstrata import *3487sage: X=Stratum((2,))3488sage: X.euler_characteristic()3489-1/403490"""3491return self.euler_char_immediate_evaluation()34923493def euler_char(self, quiet=True, alg='direct'):3494"""3495Calculate the (Orbifold) Euler characteristic of self by computing the top3496Chern class and evaluating this.34973498Note that this is significantly slower than using self.euler_characteristic!34993500The optional keyword argument alg determines how the top Chern class3501is computed and can be either:3502* direct (default): using top_chern_class_direct3503* alt: using top_chern_class_alt3504* other: using top_chern_class35053506Args:3507quiet (bool, optional): no output. Defaults to True.3508alg (str, optional): algorithm (see above). Defaults to 'direct'.35093510Returns:3511QQ: (Orbifold) Euler characteristic of self.35123513EXAMPLES::35143515sage: from admcycles.diffstrata import *3516sage: X=Stratum((2,))3517sage: X.euler_char()3518-1/403519sage: X.euler_char(alg='alt')3520-1/403521sage: X.euler_char(alg='other')3522-1/403523"""3524if alg == 'direct':3525tcc = self.top_chern_class_direct(quiet=quiet)3526elif alg == 'alt':3527tcc = self.top_chern_class_alt(quiet=quiet)3528else:3529tcc = self.top_chern_class(quiet=quiet, alg=alg)3530if not quiet:3531print('Evaluating...')3532return (-1)**self.dim() * tcc.evaluate(quiet=True)35333534def top_chern_class(self, inside=True, prod=True,3535top=False, quiet=True, alg='fast'):3536"""3537Compute the top Chern class from the Chern polynomial via the Chern character.35383539This uses chern_poly.35403541INPUT:35423543inside: bool (optional)3544Passed to chern_poly. Defaults to True.35453546prod: bool (optional)3547Passed to chern_poly. Defaults to True.35483549top: bool (optional)3550Passed to chern_poly. Defaults to False.35513552quiet: bool (optional)3553Passed to chern_poly. Defaults to True.35543555alg: str (optional)3556Passed to chern_poly. Defaults to 'fast'.35573558OUTPUT:35593560c_top(T) of self.3561"""3562return self.chern_poly(inside=inside, prod=prod,3563top=top, quiet=quiet, alg=alg)[-1]35643565def chern_poly(self, inside=True, prod=True, top=False,3566quiet=True, alg='fast', upto=None):3567"""3568The Chern polynomial calculated from the Chern character.35693570The optional keyword argument alg determines how the Chern character3571is computed and can be either:35723573* fast (default): use ch_E_fast3574* bic_prod: use ch_E_prod3575* other: use ch_E35763577INPUT:35783579inside: bool (optional)3580Passed to ch_E. Defaults to True.35813582prod: bool (optional)3583Passed to ch_E. Defaults to True.35843585top: bool (optional)3586Passed to ch_E. Defaults to False.35873588quiet: bool (optional)3589No output. Defaults to True.35903591alg: str (optional)3592Algorithm used (see above). Defaults to 'fast'.35933594upto: integer (optional)3595highest degree of polynomial to calculate. Defaults to None (i.e. dim so the whole polynomial).35963597OUTPUT:35983599The Chern polynomial as list of ELGTautClasses (indexed by degree)3600"""3601if alg == 'bic_prod':3602ch = self.ch_E_prod(quiet=quiet)3603elif alg == 'fast':3604ch = self.ch_E_fast(quiet=quiet)3605else:3606ch = self.ch_E(inside=inside, prod=prod, top=top, quiet=quiet)3607return self.charToPol(ch, quiet=quiet, upto=upto)36083609def chern_class(self, n, quiet=True):3610"""3611A direct formula for the n-th Chern class of the tangent bundle of self.36123613Args:3614n (int): degree3615quiet (bool, optional): No output. Defaults to True.36163617Returns:3618ELGTautClass: c_n(T) of self.3619"""3620N = self.dim() + 13621c_n = []3622for L in range(N):3623if not quiet:3624print('Going through %r profiles of length %r...' %3625(len(self.enhanced_profiles_of_length(L)), L))3626summand = []3627for ep in self.enhanced_profiles_of_length(L):3628if not quiet:3629print("Profile: %r" % (ep,), end=' ')3630p, _ = ep3631delta = [self.bics[b] for b in p]3632ld = [B.ell for B in delta]3633Nd = [B.top.dim() + 1 for B in delta]3634ld_prod = 13635for l in ld:3636ld_prod *= l3637inner = []3638for K in WeightedIntegerVectors(n - L, [1] * (L + 1)):3639if not quiet:3640print('xi coefficient: k_0:', K[0], end=' ')3641print('N-L-sum:', N - L - sum(K[1:]), end=' ')3642print('Binomial:', binomial(N - L - sum(K[1:]), K[0]))3643factor = binomial(N - L - sum(K[1:]), K[0])3644prod = self.xi_at_level_pow(0, ep, K[0])3645for i, k in list(enumerate(K))[1:]:3646if not quiet:3647print('k_%r: %r' % (i, k), end=' ')3648print('r_Gamma,i:', (N - Nd[i - 1]), end=' ')3649print('L-i: %r, sum: %r' %3650(L - i, sum(K[i + 1:])), end=' ')3651print('Binomial:', binomial(3652(N - Nd[i - 1]) - (L - i) - sum(K[i + 1:]), k + 1))3653factor *= binomial((N - Nd[i - 1]) -3654(L - i) - sum(K[i + 1:]), k + 1)3655squish = self.squish(ep, i - 1)3656X_pow = self.pow(3657ld[i - 1] * self.cnb(ep, ep, squish), k, ep)3658prod = self.intersection(prod, X_pow, ep)3659inner.append(factor * prod)3660summand.append(ld_prod * self.ELGsum(inner))3661c_n.append(self.ELGsum(summand))3662return self.ELGsum(c_n)36633664def ch_E_prod(self, quiet=True):3665"""3666The product version of the Chern character formula.36673668Args:3669quiet (bool, optional): No output. Defaults to True.36703671Returns:3672ELGTautClass: Chern character of the tangent bundle.3673"""3674N = QQ(self.dim() + 1)3675ch_E = [N * self.ONE]3676for L, profiles in enumerate(self.lookup_list):3677if not quiet:3678print('Going through %r profiles of length %r...' %3679(len(profiles), L))3680summand = []3681for p in profiles:3682if not p:3683continue3684Nd = self.bics[p[-1]].top.dim() + 13685if N == Nd: # factor == 03686continue3687factor = (N - Nd)3688bic_prod = self.ONE3689for Di in p:3690bic_prod *= self.exp_bic(Di)3691summand.append(factor * bic_prod)3692ch_E.append(self.ELGsum(summand))3693return self.exp_xi(quiet=quiet) * self.ELGsum(ch_E)36943695def ch_E_fast(self, quiet=True):3696"""3697A more direct (and faster) formula for the Chern character (see sec 9 of the paper).36983699Args:3700quiet (bool, optional): No output. Defaults to True.37013702Returns:3703ELGTautClass: Chern character of the tangent bundle.3704"""3705N = QQ(self.dim() + 1)3706ch_E = [N * self.exp_xi(quiet=quiet)]3707for L in range(1, N):3708if not quiet:3709print('Going through %r profiles of length %r...' %3710(len(self.enhanced_profiles_of_length(L)), L))3711summand = []3712for ep in self.enhanced_profiles_of_length(L):3713p, _ = ep3714ld = [self.bics[b].ell for b in p]3715Nd = self.bics[p[-1]].top.dim() + 13716if N == Nd: # factor == 03717continue3718ld_prod = 13719for l in ld:3720ld_prod *= l3721factor = ld_prod * (N - Nd)3722td_prod = self.ONE3723for i in range(L):3724td_prod = self.intersection(3725td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)3726exi = self.exp(self.xi_at_level(0, ep), ep)3727pr = self.intersection(exi, td_prod, ep)3728summand.append(factor * pr)3729ch_E.append(self.ELGsum(summand))3730return self.ELGsum(ch_E)37313732def top_chern_alt(self):3733"""3734The top Chern class of self.37353736This is computed by calculating the Chern polynomial3737from the Chern character as P_B*exp(L) and taking the top-degree part.37383739Returns:3740ELGTautClass: top Chern class of the tangent bundle.3741"""3742return self.charToPol(self.P_B * self.exp_L())[-1]37433744def first_term(self, top=False, quiet=True):3745"""3746The calculation of (N*self.exp_xi() - self.ONE)*self.exp_L() split into3747pieces with more debugging outputs (calculation can take a LONG time!)37483749Args:3750top (bool, optional): Do calculations on level. Defaults to False.3751quiet (bool, optional): No output. Defaults to True.37523753Returns:3754ELGTautClass: First term of ch.3755"""3756if not quiet:3757print('Calculating first term...')3758N = QQ(self.dim() + 1)3759BICs = []3760for i, B in enumerate(self.bics):3761BICs.append((B.ell, self.additive_generator(((i,), 0))))3762L = admcycles.diffstrata.elgtautclass.ELGTautClass(3763self, BICs, reduce=False)3764if top:3765if not quiet:3766print('Calculating exp_xi_L...')3767exp_xi_L = self.ELGsum([N * B.ell * self.exp(self.xi_at_level(0, ((i,), 0)), ((3768i,), 0), quiet=quiet) for i, B in enumerate(self.bics)] + [(-1) * L])3769last = exp_xi_L3770if not quiet:3771print('Calculating recursive exponential factors: ', end=' ')3772for k in range(1, N - 1):3773if not quiet:3774print(k, end=' ')3775last = QQ(1) / QQ(k + 1) * L * last3776if last == self.ZERO:3777break3778exp_xi_L._psi_list.extend(last.psi_list)3779if not quiet:3780print('Done!')3781print('Adding exp_xi...')3782res = self.ELGsum(3783[N * self.exp_xi(quiet=quiet), -self.ONE, exp_xi_L])3784else:3785if not quiet:3786print('Calculating exp(xi+L)...')3787res = N * self.exp(self.xi + L, quiet=quiet)3788if not quiet:3789print('Subtracting exp_L...')3790res -= self.exp_L(quiet=quiet)3791if not quiet:3792print('Done calculating first term!')3793return res37943795def ch_E(self, inside=True, prod=True, top=False, quiet=True):3796"""3797The Chern character (according to sec. 9 of the paper)37983799Args:3800inside (bool, optional): work with ambient. Defaults to True.3801prod (bool, optional): product instead of sum. Defaults to True.3802top (bool, optional): work on level. Defaults to False.3803quiet (bool, optional): no output. Defaults to True.38043805Returns:3806ELGTautClass: Chern character of the tangent bundle of self.3807"""3808# Prop. 9.23809N = QQ(self.dim() + 1)3810# ch = [(N*self.exp_xi() + (-1)*self.ONE)*self.exp_L()]3811ch = [self.first_term(top=top, quiet=quiet)]3812for L in range(1, N):3813inner = []3814if not quiet:3815print('Going through profiles of length %r...' % L)3816for enh_profile in self.enhanced_profiles_of_length(L):3817p, _ = enh_profile3818B = self.bics[p[0]]3819Ntop = B.top.dim() + 13820if not inside:3821summand = (-1)**L * (Ntop * self.exp_xi() - self.ONE)3822else:3823if not quiet:3824print('Calculating inner exp(xi): ', end=' ')3825summand = (-1)**L * (Ntop * self.exp(self.xi_at_level(0, enh_profile),3826enh_profile, quiet=quiet) - self.taut_from_graph(*enh_profile))3827prod_list = []3828for i in range(L):3829ll = self.bics[p[i]].ell3830squish = self.squish(enh_profile, i)3831td_NB = ll * \3832self.td_contrib(-ll, self.cnb(enh_profile,3833enh_profile, squish), enh_profile)3834prod_list.append(td_NB)3835if prod_list:3836prod = prod_list[0]3837for f in prod_list[1:]:3838# multiply with ambient Gamma (=enh_profile)!3839prod = self.intersection(prod, f, enh_profile)3840if prod:3841for l in range(len(p) + 1):3842prod = self.intersection(3843prod,3844self.exp(3845self.calL(3846enh_profile,3847l),3848enh_profile),3849enh_profile)3850else:3851prod = self.intersection(3852prod,3853self.exp(3854self.ELGsum(3855self.calL(3856enh_profile,3857l) for l in range(3858len(p) +38591)),3860enh_profile),3861enh_profile)3862if inside:3863prod = self.intersection(prod, summand, enh_profile)3864# multiply constant term with Gamma (for i_*)3865const = prod.degree(0)3866prod += (-1) * const3867if inside:3868summand = prod3869else:3870summand *= (prod + const *3871self.taut_from_graph(*enh_profile))3872inner.append(summand)3873ch.append(self.ELGsum(inner))3874return self.ELGsum(ch)38753876################################################################3877# END OF SEC 9 FORMULAS #3878################################################################38793880def res_stratum_class(self, cond, debug=False):3881"""3882The class of the stratum cut out by cond inside self.38833884INPUT:38853886cond (list): list of a residue condition, i.e. a list of poles of self.38873888OUTPUT:38893890Tautological class of Prop. 9.33891"""3892st_class = -1 * self.xi_with_leg(quiet=True)3893bic_list = []3894if debug:3895print(3896"Calculating the class of the stratum cut out by %r in %r..." %3897(cond, self))3898print("-xi = %s" % st_class)3899for i, B in enumerate(self.bics):3900if debug:3901print("Checking BIC %r:" % i)3902top = B.top3903# we restrict/translate cond to top:3904poles_on_bic = [B.dmp_inv[p] for p in cond]3905cond_on_top = [top.leg_dict[leg]3906for leg in poles_on_bic if leg in top.leg_dict]3907# if there are RCs on top, we must check that they don't change the3908# rank3909if cond_on_top:3910MT = top.matrix_from_res_conditions([cond_on_top])3911top_G = top.smooth_LG3912RT = top_G.full_residue_matrix3913if (MT.stack(RT)).rank() != RT.rank():3914assert (MT.stack(RT)).rank() > RT.rank()3915if debug:3916print("Discarding (because of top).")3917continue3918l = B.ell3919if debug:3920print("Appending with coefficient -%r" % l)3921bic_list.append((l, i))3922st_class += self.ELGsum([-l * self.taut_from_graph((i,), 0)3923for l, i in bic_list])3924return st_class39253926def adm_evaluate(self, stgraph, psis, sig, g, quiet=False,3927admcycles_output=False):3928"""3929Evaluate the psi monomial on a (connected) stratum without residue conditions3930using admcycles.39313932stgraph should be the one-vertex graph associated to the stratum sig.39333934We use admcycles Strataclass to calculate the class of the stratum inside3935Mbar_{g,n} and multiply this with psis (in admcycles) and evaluate the product.39363937The result is cached and synched with the ``ADM_EVALS`` cache.39383939Args:3940stgraph (stgraph): admcycles stgraph3941psis (dict): psi polynomial on stgraph3942sig (tuple): signature tuple3943g (int): genus of sig3944quiet (bool, optional): No output. Defaults to False.3945admcycles_output (bool, optional): Print the admcycles classes. Defaults to False.39463947Returns:3948QQ: integral of psis on stgraph.39493950TESTS:39513952Check that the cache can be deactivated and reactivated::39533954sage: from admcycles import StableGraph3955sage: from admcycles.diffstrata import Stratum3956sage: import admcycles.diffstrata.cache3957sage: X = Stratum((1,1))3958sage: stg = StableGraph([2], [[1,2]], [])3959sage: psis = {1: 1, 2: 3}3960sage: sig = (1, 1)3961sage: g = 23962sage: tmp = admcycles.diffstrata.cache.ADM_EVALS3963sage: admcycles.diffstrata.cache.ADM_EVALS = admcycles.diffstrata.cache.FakeCache()3964sage: X.adm_evaluate(stg, psis, sig, g, quiet=True) is X.adm_evaluate(stg, psis, sig, g, quiet=True)3965False3966sage: admcycles.diffstrata.cache.ADM_EVALS = tmp3967sage: X.adm_evaluate(stg, psis, sig, g, quiet=True) is X.adm_evaluate(stg, psis, sig, g, quiet=True)3968True3969"""3970# key = (tuple(sorted(psis.items())), tuple(sig))3971key = adm_key(sig, psis)3972from .cache import ADM_EVALS3973if key not in ADM_EVALS:3974DS = admcycles.admcycles.decstratum(stgraph, psi=psis)3975Stratum_class = admcycles.stratarecursion.Strataclass(g, 1, sig)3976if not quiet or admcycles_output:3977print("DS: %r\n Stratum_class: %r" % (DS, Stratum_class))3978product = Stratum_class * DS # in admcycles!3979if not quiet or admcycles_output:3980print("Product: %r" % product.evaluate())3981answer = ADM_EVALS[key] = product.evaluate() # in admcycles!3982return answer3983else:3984return ADM_EVALS[key]39853986def remove_res_cond(self, psis=None):3987"""3988Remove residue conditions until the rank drops (or there are none left).39893990We return the stratum with fewer residue conditions and, in3991case the rank dropped, with the product of the stratum class.39923993Note that this does *not* ensure that all residue conditions are removed!39943995Args:3996psis (dict, optional): Psi dictionary on self. Defaults to None.39973998Returns:3999ELGTautClass: ELGTautClass on Stratum with less residue conditions4000(or self if there were none!)40014002EXAMPLES::40034004sage: from admcycles.diffstrata import *4005sage: X=GeneralisedStratum([Signature((1,1,-2,-2))], res_cond=[[(0,2)], [(0,3)]])4006sage: print(X.remove_res_cond())4007Tautological class on Stratum: Signature((1, 1, -2, -2))4008with residue conditions:4009dimension: 14010leg dictionary: {}4011<BLANKLINE>40121 * Psi class 3 with exponent 1 on level 0 * Graph ((), 0) +4013<BLANKLINE>4014sage: X.evaluate(quiet=True) == X.remove_res_cond().evaluate()4015True4016"""4017if psis is None:4018psis = {}40194020if not self.res_cond:4021return self.additive_generator(((), 0), psis).as_taut()40224023try:4024new_leg_dict = deepcopy(self._leg_dict)4025except AttributeError:4026new_leg_dict = {}40274028# Create new stratum with one residue condition less:4029new_rc = deepcopy(self._res_cond)4030# conditions from RT:4031RT_M = self.smooth_LG.residue_matrix_from_RT4032# we remove conditions until the rank drops:4033while new_rc:4034lost_cond = new_rc.pop()4035new_M = self.matrix_from_res_conditions(new_rc)4036if new_M:4037full_M = new_M.stack(RT_M)4038else:4039full_M = RT_M4040if full_M.rank() == self.smooth_LG.full_residue_matrix.rank() - 1:4041# rank dropped4042break4043new_stratum = LevelStratum(self._sig_list, new_rc, new_leg_dict)4044# Because only the RCs changed, X.smooth_LG still lives inside this stratum4045# so we can use it to build our new AdditiveGenerator:4046new_AG = new_stratum.additive_generator(((), 0), psis)4047if new_stratum.dim() == self.dim() + 1:4048new_class = new_AG.as_taut() * new_stratum.res_stratum_class(lost_cond)4049else:4050# rank did not drop so all residue conditions are gone:4051assert not new_rc4052new_class = new_AG.as_taut()40534054return new_class40554056def zeroStratumClass(self):4057"""4058Check if self splits, i.e. if a subset of vertices can be scaled4059independently (then the stratum class is ZERO).40604061We do this by checking if BICs B, B' exist with:4062* no edges4063* the top vertices of B are the bottom vertices of B'4064* the bottom vertices of B' are the top vertices of B.40654066Explicitly, we loop through all BICs with no edges, constructing for4067each one the BIC with the levels interchanged (as an EmbeddedLevelGraph)4068and check its legality.40694070Returns:4071boolean: True if splitting exists, False otherwise.40724073EXAMPLES::40744075sage: from admcycles.diffstrata import *4076sage: GeneralisedStratum([Signature((0,)),Signature((0,))]).zeroStratumClass()4077True4078sage: GeneralisedStratum([Signature((2,))]).zeroStratumClass()4079False4080sage: GeneralisedStratum([Signature((4,-2,-2,-2)),Signature((4,-2,-2,-2))], res_cond=[[(0,2),(1,2)]]).zeroStratumClass()4081True4082sage: GeneralisedStratum([Signature((2, -2, -2)), Signature((1, 1, -2, -2))],[[(0, 2), (1, 2)], [(0, 1), (1, 3)]]).zeroStratumClass()4083False4084"""4085bics_no_edges = [b for b in self.bics if not b.LG.edges]4086if not bics_no_edges:4087return False4088for b in bics_no_edges:4089internal_top = b.LG.internal_level_number(0)4090internal_bot = b.LG.internal_level_number(1)4091top_vertices = b.LG.verticesonlevel(internal_top)4092bot_vertices = b.LG.verticesonlevel(internal_bot)4093assert len(top_vertices) + len(bot_vertices) == len(b.LG.genera)4094# build graph levels exchanged:4095new_levels = [internal_bot if v in top_vertices else internal_top4096for v in range(len(b.LG.genera))]4097new_vertices = deepcopy(b.LG.genera)4098new_legs = deepcopy(b.LG.legs)4099new_edges = []4100new_poleorders = deepcopy(b.LG.poleorders)4101new_LG = admcycles.diffstrata.levelgraph.LevelGraph(4102new_vertices, new_legs, new_edges, new_poleorders, new_levels)4103new_ELG = admcycles.diffstrata.embeddedlevelgraph.EmbeddedLevelGraph(4104self, new_LG, deepcopy(b.dmp), deepcopy(b.dlevels))4105# check if new graph is legal:4106if new_ELG.is_legal():4107return True4108# no splitting found4109return False41104111def evaluate(self, psis={}, quiet=False, warnings_only=False,4112admcycles_output=False):4113"""4114Evaluate the psi monomial psis on self.41154116Psis is a dictionary legs of self.smooth_LG -> exponents encoding a psi monomial.41174118We translate residue conditions of self into intersections of simpler classes4119and feed the final pieces into admcycles for actual evaluation.41204121Args:4122psis (dict, optional): Psi monomial (as legs of smooth_LG -> exponent). Defaults to {}.4123quiet (bool, optional): No output. Defaults to False.4124warnings_only (bool, optional): Only warnings. Defaults to False.4125admcycles_output (bool, optional): adm_eval output. Defaults to False.41264127Raises:4128RuntimeError: raised if a required residue condition is not found.41294130Returns:4131QQ: integral of psis against self.4132"""4133G = self.smooth_LG4134LG = G.LG4135# Check if the rGRC doesn't cut down the dimension:4136# Recall:4137# * residue_matrix_from_RT has the RT on each component of G as rows4138# * full_residue_matrix is this + the res_cond of self4139if G.full_residue_matrix.rank() == G.residue_matrix_from_RT.rank():4140if self._h0 > 1:4141if not quiet or warnings_only:4142print("----------------------------------------------------")4143print("Level %r disconnected." % self)4144print("----------------------------------------------------")4145print("No residue conditions: contribution is 0.")4146return 04147# stratum is connected!4148# 0 dimensional strata contribute 14149if self.dim() == 0:4150return 14151# We can just use admcycles to evaluate:4152return self.adm_evaluate(4153LG.stgraph,4154psis,4155self._sig_list[0].sig,4156LG.g(),4157quiet=quiet,4158admcycles_output=admcycles_output)4159# There *are* non-trivial residue conditions!4160if self._h0 > 1:4161if not quiet or warnings_only:4162print("----------------------------------------------------")4163print("Level %r disconnected." % self)4164print("----------------------------------------------------")4165# Check if graph of residue conditions is disconnected:4166if not LG.underlying_graph.is_connected():4167if not quiet or warnings_only:4168print("Level is product: contribution is 0.")4169return 04170# Create new stratum with one residue condition less:4171new_rc = deepcopy(self._res_cond)4172# conditions from RT:4173RT_M = G.residue_matrix_from_RT4174# we remove conditions until the rank drops:4175while new_rc:4176lost_cond = new_rc.pop()4177new_M = self.matrix_from_res_conditions(new_rc)4178if new_M:4179full_M = new_M.stack(RT_M)4180else:4181full_M = RT_M4182if full_M.rank() == G.full_residue_matrix.rank() - 1:4183# rank dropped4184break4185else:4186raise RuntimeError(4187"No Conditions cause dimension to drop in %r!" %4188self._res_cond)4189try:4190new_leg_dict = deepcopy(self._leg_dict)4191except AttributeError:4192new_leg_dict = {}4193new_stratum = LevelStratum(self._sig_list, new_rc, new_leg_dict)4194if not quiet:4195print("Recursing into stratum %r" % new_stratum)4196assert new_stratum.dim() == self.dim() + 14197# Because only the RCs changed, G still lives inside this stratum4198# so we can use it to build our new AdditiveGenerator:4199new_AG = new_stratum.additive_generator(((), 0), psis)4200new_class = new_AG.as_taut() * new_stratum.res_stratum_class(lost_cond)4201result = new_class.evaluate(quiet=quiet)4202return result42034204#################################################################4205#################################################################42064207def boundary_pullback(self, G=None, quiet=True):4208# while our graphs have underlying stable graphs, we don't4209# guarantee the numbering (this is the reason we work with4210# EmbeddedLevelGraphs). To use admcycles specialisation check4211# we have to remedy this:4212def _rename(ELG, stgraph):4213# make sure the marked points of stgraph are numbered 1,...,n4214num = 04215mp_dict = {} # dict: leg of marked point -> number of mp4216for i in range(self._h0):4217for j in range(len(self._sig_list[i].sig)):4218num += 14219mp_dict[ELG.dmp_inv[(i, j)]] = num4220assert num == self._n4221stgraph.rename_legs(mp_dict, shift=num) # shift edges accordingly4222if G is None:4223# total pullback4224# TODO4225return self.ZERO4226tautlist = []4227# go through all BICs and check if G is an undegeneration (in M_g,n)4228# of the BIC's underlying stable graph:4229for b, B in enumerate(self.bics):4230stgraph = B.LG.stgraph.copy() # original is immutable...4231_rename(B, stgraph) # fix leg numbering4232graph_morphisms = admcycles.admcycles.Astructures(stgraph, G)4233num_degenerations = len(graph_morphisms)4234if num_degenerations == 0:4235continue4236# calculate preimage of edge:4237# note that leg names were shifted by n above!4238e = G.edges()[0]4239pre_e = [(m[1][e[0]] - self._n, m[1][e[1]] - self._n)4240for m in graph_morphisms]4241coeff = sum(QQ(B.ell) / QQ(B.LG.prong(e)) for e in pre_e)4242if not quiet:4243print(4244'\nFound BIC %r with stable graph %r in preimage with %r degenerations' %4245(b, stgraph, num_degenerations))4246print('sum of involved edges (ell / prong): ', coeff)4247print('Any level pushs to ZERO: ', any(4248B.level(l).zeroStratumClass() for l in range(B.codim + 1)))4249tautlist.append(coeff * self.taut_from_graph((b,)))4250return self.ELGsum(tautlist)42514252#################################################################4253#################################################################42544255@property4256def kappa_1(self):4257"""4258The Mumford class of self.42594260Note that kappa_1_AC = kappa_1 + sum(psis)42614262Returns:4263ELGTautClass: kappa_1_AC - sum(psis)42644265EXAMPLES::42664267sage: from admcycles.diffstrata import *4268sage: X=Stratum((1,1,1,-5))4269sage: X.kappa_1.evaluate()4270-34271"""4272if self._h0 > 1:4273return self.ZERO4274kappa = [self.kappa_1_AC]4275for i in range(1, self._n + 1):4276kappa.append(-self.psi(i))4277return self.ELGsum(kappa)42784279@property4280def kappa_1_AC(self):4281"""4282The Arbarello--Cornalba class of self.42834284Note that this 'corresponds' to admcycles kappaclass.42854286Returns:4287ELGTautClass: c_1(omega_log) + sum(l_Gamma * G_Gamma * bics) (-xi if hol)42884289EXAMPLES::42904291sage: from admcycles.diffstrata import *4292sage: X=Stratum((1,1,1,-5))4293sage: X.kappa_1_AC.evaluate()429414295sage: from admcycles import *4296sage: (kappaclass(1,0,4) * Strataclass(0, 1, [1,1,1,-5])).evaluate()429714298sage: from admcycles.diffstrata import *42994300sage: X=Stratum((2,))4301sage: (X.kappa_1_AC.to_prodtautclass().pushforward() - kappaclass(1, 2, 1)*Strataclass(2, 1, [2])).is_zero()4302True4303sage: X=Stratum((1,1))4304sage: (X.kappa_1_AC.to_prodtautclass().pushforward() - kappaclass(1, 2, 2)*Strataclass(2, 1, [1,1])).is_zero()4305True4306"""4307if self._h0 > 1:4308return self.ZERO4309kappa = [self.c1_E]4310for b, B in enumerate(self.bics):4311# note that every component has a pole!4312factor = B.ell * \4313(B.bot.smooth_LG.full_residue_matrix.rank() - B.bot._h0)4314if factor != 0:4315kappa.append(factor * self.taut_from_graph((b,)))4316if not self._polelist:4317# holomorphic!4318kappa.append(-self.xi)4319return self.ELGsum(kappa)43204321@property4322def kappa_1_dawei(self):4323if self._h0 > 1:4324return self.ZERO4325sig = self._sig_list[0].sig4326kappa = [sum(sig) * self.xi]4327kappa.extend([m * self.psi(i + 1) for i, m in enumerate(sig)])4328for b, B in enumerate(self.bics):4329# reminder: B.emb_bot: MP on bottom level -> MP on B4330sum_mi = sum(B.bot.stratum_point_order(p) for p in B.emb_bot)4331sum_k = sum(B.LG.prongs.values())4332factor = B.ell * (sum_mi - sum_k)4333kappa.append(factor * self.taut_from_graph((b,)))4334return self.ELGsum(kappa)43354336@property4337def kappa_1_dawei_alt(self):4338if self._h0 > 1:4339return self.ZERO4340kappa = [self.kappa_EKZ * self.xi]4341for b, B in enumerate(self.bics):4342sum_k = sum(B.LG.prongs.values())4343# reminder: B.emb_bot: MP on bottom level -> MP on B4344mis = [B.bot.stratum_point_order(p) for p in B.emb_bot]4345factor = B.ell * (sum(QQ(m * (m + 2)) / QQ(m + 1) for m in mis) - sum_k)4346kappa.append(factor * self.taut_from_graph((b,)))4347return self.ELGsum(kappa)43484349@property4350def kappa_EKZ(self):4351"""4352The EKZ kappa, i.e. sum m_i*(m_i+2)/(m_i+1) for self.43534354Raises:4355ValueError: If self has a simple pole (div by 0)43564357Returns:4358QQ: EKZ kappa factor of self.43594360EXAMPLES::43614362sage: from admcycles.diffstrata import *4363sage: Stratum((1,1)).kappa_EKZ436434365"""4366sigs = [m for sig in self._sig_list for m in sig.sig]4367if any(m == -1 for m in sigs):4368raise ValueError('Cannot compute EKZ kappa for simple poles!')4369return sum(QQ(m * (m + 2)) / QQ(m + 1) for m in sigs)43704371def horizontal_pushforward(self, graphs=None, psis=None):4372"""4373By default the total horizontal boundary.4374In general: takes an iterator (e.g. _horizontal_pf_iter) and4375sums over the second item (the pushforward classes)43764377Args:4378graphs (iterable, optional): an iterator yielding tuples of4379the form G, tautclass. By default _horizontal_pf_iter.4380Defaults to None.4381psis (dict, optional): psi dictionary for passing on.4382Defaults to None.43834384Returns:4385tautclass: the sum over the second items of graphs.4386"""4387if graphs is None:4388graphs = self._horizontal_pf_iter(psis)4389return sum(hpf for _, hpf in graphs)43904391def _horizontal_pf_iter(self, psis=None):4392"""4393Iterate over the components of the pushforward of the4394horizontal boundary.43954396Args:4397psis (dict, optional): Psi dictionary. Defaults to None.43984399Raises:4400StopIteration: If disconnected or done.44014402Yields:4403tuple: G, tautclass where G is StableGraph and tautclass4404is the pushed forward class of the horizontal divisor4405corresponding to G.4406"""4407if self._h0 > 1:4408raise StopIteration4409g = self._g[0]4410sig = self._sig_list[0].sig4411n = self._n4412if psis:4413H = admcycles.admcycles.StableGraph(4414[g], [list(range(1, n + 1))], [])4415psi_contr = admcycles.admcycles.tautclass(4416[admcycles.admcycles.decstratum(H, psi=psis)])4417else:4418psi_contr = 14419if g > 0:4420# build horizontal stable graph:4421G = self.smooth_LG.LG.stgraph.copy()4422G.degenerate_nonsep(0)4423# put the strataclass of the g-1 stratum on a prodtautclass:4424new_sig = list(sig) + [-1, -1]4425if self._polelist:4426# meromorphic: have to add extra residue condition4427rc = [(0, n), (0, n + 1)]4428X = GeneralisedStratum(4429[admcycles.diffstrata.sig.Signature(new_sig)])4430tautlist = [X.res_stratum_class(4431rc).to_prodtautclass().pushforward()]4432else:4433# holomorphic: only residue cond is RT -> OK to work directly4434# with Strataclass4435tautlist = [4436admcycles.stratarecursion.Strataclass(g - 1, 1, new_sig)]4437ptc = admcycles.admcycles.prodtautclass(G, protaut=tautlist)4438stack_factor = QQ(1) / QQ(2)4439yield G, stack_factor * psi_contr * ptc.pushforward()4440if len(self._polelist) >= 2:4441# in this case, we also have horizontal divisors of compact type4442# Note that each component4443# * must have at least one pole4444# * the orders must sum to 2g_i - 2 + 14445# We find all stable graphs with one edge and this property:4446for G in admcycles.admcycles.list_strata(g, n, 1):4447if len(G.genera()) == 1:4448continue4449# consider as disconnected stratum and resolve residue4450# condition4451gl, gr = G.genera()4452sigl = [sig[l - 1] for l in G.legs(0) if l <= n] + [-1]4453sigr = [sig[l - 1] for l in G.legs(1) if l <= n] + [-1]4454if sum(sigl) != 2 * gl - 2 or sum(sigr) != 2 * gr - 2:4455continue4456if all(a >= 0 for a in sigl) or all(a >= 0 for a in sigr):4457continue4458# we need the class of the disconnected stratum where the4459# residues at the simple poles add up to zero:4460rc = [(0, len(sigl) - 1), (1, len(sigr) - 1)]4461X = GeneralisedStratum([admcycles.diffstrata.sig.Signature(4462sigl), admcycles.diffstrata.sig.Signature(sigr)])4463ptc = X.res_stratum_class(rc).to_prodtautclass()4464if ptc == 0:4465continue4466# now we replace the underlying (disconnected!) graph4467# of ptc by the (connected!) graph G:4468ptc.gamma = G4469stack_factor = QQ(1) / QQ(G.automorphism_number())4470yield G, stack_factor * psi_contr * ptc.pushforward()44714472def masur_veech_volume(self):4473"""4474If self is a connected stratum of holomorphic differentials,4475calculate the Masur-Veech volume of self using the formula [CMSZ2020]_4476of Chen-Möller-Sauvaget-Zagier.4477Otherwise throws NotImplementedError.44784479EXAMPLES:44804481sage: from admcycles.diffstrata import Stratum4482sage: X = Stratum((0,))4483sage: X.masur_veech_volume()44841/3*pi^244854486sage: X = Stratum((2,))4487sage: X.masur_veech_volume()44881/120*pi^444894490sage: X = Stratum((1,1))4491sage: X.masur_veech_volume()44921/135*pi^444934494sage: X = Stratum((4,))4495sage: X.masur_veech_volume()449661/108864*pi^644974498sage: X = Stratum((-1,-1,0))4499sage: X.masur_veech_volume()4500Traceback (most recent call last):4501...4502NotImplementedError45034504"""4505if self._h0 != 1 or self._p != 0:4506raise NotImplementedError4507g = self._g[0]4508n = self._n4509return - 2 * (2 * I * pi)**(2 * g) / factorial(2 * g - 3 + n) * \4510(self.xi_pow(2 * g - 2) *4511prod(self.psi(l) for l in range(1, n + 1))).evaluate()45124513#################################################################4514#################################################################4515#################################################################4516#################################################################451745184519class Stratum(GeneralisedStratum):4520"""4521A simpler frontend for a GeneralisedStratum with one component and4522no residue conditions.4523"""45244525def __init__(self, sig):4526super().__init__(4527[admcycles.diffstrata.sig.Signature(sig)])45284529#################################################################4530#################################################################4531#################################################################4532#################################################################453345344535class LevelStratum(GeneralisedStratum):4536"""4537A stratum that appears as a level of a levelgraph.45384539This is a ``GeneralisedStratum`` together with a dictionary mapping the4540leg numbers of the (big) graph to the legs of the ``Generalisedstratum``.45414542Note that if this is initialised from an EmbeddedLevelGraph, we also4543have the attribute leg_orbits, a nested list giving the orbits of4544the points under the automorphism group of the graph.45454546* leg_dict : a (bijective!) dictionary mapping the leg numbers of a graph4547to the corresponding tuple (i,j), i.e. the point j on the component i.45484549* res_cond : a (nested) list of residue conditions given by the r-GRC when4550extracting a level.45514552"""45534554def __init__(self, sig_list, res_cond=None, leg_dict=None):4555super().__init__(sig_list, res_cond)4556if leg_dict is None:4557# assume the points were numbered 1...n4558self._leg_dict = {}4559for i in range(len(sig_list)):4560for j in range(sig_list[i].n):4561self._leg_dict[i + j + 1] = (i, j)4562else:4563self._leg_dict = leg_dict4564# build inverse dictionary4565self._inv_leg_dict = {v: k for k, v in self._leg_dict.items()}45664567def __repr__(self):4568return "LevelStratum(sig_list=%r,res_cond=%r,leg_dict=%r)" % (4569self._sig_list, self._res_cond, self.leg_dict)45704571def __str__(self):4572rep = ''4573if self._h0 > 1:4574rep += 'Product of Strata:\n'4575else:4576rep += 'Stratum: '4577for sig in self._sig_list:4578rep += repr(sig) + '\n'4579rep += 'with residue conditions: '4580for res in self._res_cond:4581rep += repr(res) + ' '4582rep += '\n'4583rep += 'dimension: ' + repr(self.dim()) + '\n'4584rep += 'leg dictionary: ' + repr(self._leg_dict) + '\n'4585try:4586rep += 'leg orbits: ' + repr(self.leg_orbits) + '\n'4587except AttributeError:4588pass4589return rep45904591@cached_method4592def dict_key(self):4593"""4594The hash-key for the cache of top-xi-powers.45954596More precisely, we sort each signature, sort this list and renumber4597the residue conditions accordingly. Finally, everything is made into a tuple.45984599Returns:4600tuple: nested tuple.4601"""4602rc_dict = {}4603sig = []4604for new_i, new_sign in enumerate(4605sorted(enumerate(self._sig_list), key=lambda k: k[1].sig)):4606i, sign = new_sign4607curr_sig = []4608for new_j, s in enumerate(4609sorted(enumerate(sign.sig), key=lambda k: k[1])):4610j, a = s4611curr_sig.append(a)4612rc_dict[(i, j)] = (new_i, new_j)4613sig.append(tuple(curr_sig))4614sig = tuple(sig)4615rc = sorted([sorted([rc_dict[cond] for cond in conds])4616for conds in self._res_cond])4617rc = tuple(tuple(c) for c in rc)4618return (sig, rc)46194620@property4621def leg_dict(self):4622return self._leg_dict46234624@property4625def inv_leg_dict(self):4626return self._inv_leg_dict46274628# Psi classes are numbered according to the points of the stratum, but we want4629# to use them for the points of the graph. The leg_dicts translate between these,4630# we make this a little more user friendly.4631def stratum_number(self, n):4632"""4633Returns a tuple (i,j) for the point j on the component i that corresponds4634to the leg n of the graph.4635"""4636return self._leg_dict[n]46374638def leg_number(self, n):4639"""4640Returns the leg number (of the graph G) that corresponds to the psi class4641number n.4642"""4643return self._inv_leg_dict[n]464446454646