unlisted
ubuntu2004from copy import deepcopy12# pylint does not know sage3from sage.structure.sage_object import SageObject # pylint: disable=import-error4from sage.rings.integer_ring import ZZ # pylint: disable=import-error5from sage.misc.flatten import flatten # pylint: disable=import-error6from sage.functions.generalized import sign # pylint: disable=import-error7from sage.misc.cachefunc import cached_method # pylint: disable=import-error8from sage.graphs.graph import Graph # pylint: disable=import-error9from sage.plot.text import text # pylint: disable=import-error1011from admcycles.admcycles import GraphIsom12from admcycles.stable_graph import StableGraph as stgraph13from admcycles.diffstrata.sig import kSignature141516class KLevelGraph(SageObject):17r"""18Create a (stable) level graph for a k-differential.1920.. NOTE::2122This is a low-level class and should NEVER be invoked directly!23Preferably, ``EmbeddedLevelGraphs`` should be used and these should be24generated automatically by ``Stratum`` (or ``GeneralisedStratum``).2526.. NOTE::2728We do not inherit from stgraph/StableGraph anymore, as KLevelGraphs29should be static objects!3031Extends admcycles stgraph to represent a level graph as a stgraph,32i.e. a list of genera, a list of legs and a list of edges, plus a list of33poleorders for each leg and a list of levels for each vertex.3435Note that the class will warn if the data is not admissible, i.e. if the36graph is not stable or the pole orders at separating nodes across levels do37not add up to -2 or -1 on the same level (unless this is suppressed with38quiet=True).3940INPUT:4142genera : list43List of genera of the vertices of length m.4445legs : list46List of length m, where ith entry is list of legs attached to vertex i.47By convention, legs are unique positive integers.4849edges : list50List of edges of the graph. Each edge is a 2-tuple of legs.5152poleorders : dictionary53Dictionary of the form leg number : poleorder5455levels : list56List of length m, where the ith entry corresponds to the level of57vertex i.58By convention, top level is 0 and levels go down by 1, in particular59they are NEGATIVE numbers!6061k : int62The order of the differential.6364quiet : optional boolean (default = False)65Suppresses most error messages.6667ALTERNATIVELY, the pole orders can be supplied as a list by calling68KLevelGraph.fromPOlist:69poleorders : list70List of order of the zero (+) or pole (-) at each leg. The ith element71of the list corresponds to the order at the leg with the marking i+172(because lists start at 0 and the legs are positive integers).7374EXAMPLES:7576Creating a level graph with three components on different levels of genus 1,773 and 0. The bottom level has a horizontal node.::7879sage: from admcycles.diffstrata.klevelgraph import KLevelGraph80sage: G = KLevelGraph.fromPOlist([1,3,0],[[1,2],[3,4,5],[6,7,8,9]],[(2,3),(5,6),(7,8)],[2,-2,0,6,-2,0,-1,-1,0],[-2,-1,0],1); G # doctest: +SKIP81KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,True)8283or alternatively::8485sage: KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,quiet=True)86KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,True)8788Creating a level graph with two components on different levels of genus 1 and 1 and a quadratic differential.::8990sage: KLevelGraph.fromPOlist([1,1],[[1],[2,3]],[(1,2)],[0,-4,4],[0,-1],2)91KLevelGraph([1, 1],[[1], [2, 3]],[(1, 2)],{1: 0, 2: -4, 3: 4},[0, -1],2,True)9293We get a warning if the graph has non-stable components: (not any more ;-))::9495sage: G = KLevelGraph.fromPOlist([1,3,0,0],[[1,2],[3,4,5],[6,7,8,9],[10]],[(2,3),(5,6),(7,8),(9,10)],[2,-2,0,6,-2,0,-1,-1,0,-2],[-3,-2,0,-1],1); G # doctest: +SKIP96Warning: Component 3 is not stable: g = 0 but only 1 leg(s)!97Warning: Graph not stable!98KLevelGraph([1, 3, 0, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9], [10]],[(3, 2), (6, 5), (7, 8), (9, 10)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0, 10: -2},[-3, -2, 0, -1],1,True)99"""100def __init__(self, genera, legs, edges,101poleorders, levels, k, quiet=False):102checks = False103if len(genera) != len(legs):104raise ValueError('genera and legs must have the same length')105self.genera = genera106self.legs = legs107self.edges = edges108self.poleorders = poleorders109self.levels = levels110# the signature consists of the marked points that are not half-edges111sig_list = tuple(poleorders[l] for l in self.list_markings())112self.sig = kSignature(sig_list, k)113# associated stgraph...114self._stgraph = None115self._init_more() # initiate some things and make sure all is in order116if checks:117self.checkadmissible(quiet)118self._has_long_edge = None119self._is_long = {}120self._is_bic = None121122@classmethod123def fromPOlist(cls, genera, legs, edges,124poleordersaslist, levels, k, quiet=False):125"""126This gives a KLevelGraph where the poleorders are given as a list, not127directly as a dictionary.128"""129# translate poleorder list to dictionary:130sortedlegs = sorted(flatten(legs))131if len(sortedlegs) != len(poleordersaslist):132raise ValueError("Numbers of legs and pole orders do not agree!" +133" Legs: " + repr(len(sortedlegs)) +134" Pole orders: " + repr(len(poleordersaslist)))135poleorderdict = {sortedlegs[i]: poleordersaslist[i]136for i in range(len(sortedlegs))}137return cls(genera, legs, edges, poleorderdict, levels, k, quiet)138139def __repr__(self):140return "KLevelGraph(" + self.input_as_string() + ")"141142def __hash__(self):143return hash(repr(self))144145def __str__(self):146return (147"KLevelGraph " +148repr(149self.genera) +150' ' +151repr(152self.legs) +153' ' +154repr(155self.edges) +156' ' +157repr(158self.poleorders) +159' ' +160repr(161self.levels) +162' ' +163repr(164self.k))165166def input_as_string(self):167"""168return a string that can be given as argument to __init__ to give self169"""170return (repr(self.genera) + ',' + repr(self.legs) + ',' +171repr(self.edges) + ',' + repr(self.poleorders) + ',' +172repr(self.levels) + ',' + repr(self.sig.k) + ',True')173174def __eq__(self, other):175if not isinstance(other, KLevelGraph):176return False177return (178self.genera == other.genera) and (179self.levels == other.levels) and (180self.legs == other.legs) and (181set(182self.edges) == set(183other.edges)) and (184self.poleorders == other.poleorders) and (185self.sig.k == other.sig.k)186187# reimplementing the admcycles stuff we use:188@cached_method189def g(self):190genus = sum(self.genera) + len(self.edges) - \191len(self.genera) + ZZ.one()192assert genus == self.sig.g, "Signature %r does not match %r!" % (193self.sig, self)194return genus195196@property197def stgraph(self):198if self._stgraph is None:199self._stgraph = stgraph([int(g) for g in self.genera],200[[int(l) for l in leg_list]201for leg_list in self.legs],202self.edges)203return self._stgraph204205@cached_method206def list_markings(self, v=None):207r"""208Return the list of markings (non-edge legs) of self at vertex v.209"""210if v is None:211return tuple([j for v in range(len(self.genera))212for j in self.list_markings(v)])213s = set(self.legs[v])214for e in self.edges:215s -= set(e)216return tuple(s)217218##########################################################219# Interface:220# --- provide a series of methods that translate various data221# (maybe this can be optimised by smart caching later, so only222# these should be used...)223# up to now, we have:224# * vertex : (overloaded from admcycles)225# INPUT : leg226# returns : vertex of leg (index of genera)227# * vertex_list :228# returns : list of vertices (index of genera)229# * edges_list :230# returns : list of edges231# * levelofvertex :232# INPUT : index of genera233# returns : level number (negative)234# * levelofleg :235# INPUT : leg label (given as positive integer)236# returns : level number (negative)237# * orderatleg :238# INPUT : leg label (given as positive integer)239# returns : poleorder (integer)240# * ordersonvertex :241# INPUT : index of genera242# returns : list of poleorders (integers)243# * verticesonlevel :244# INPUT : level number (negative)245# returns : list of indices of genera246# * edgesatvertex :247# INPUT : index of genera248# returns : list of edges249# * legsatvertex :250# INPUT : index of genera251# returns : list of legs252# * simplepolesatvertex :253# INPUT : index of genera254# returns : list of legs with pole order -1255# * genus :256# INPUT : index of genera257# returns : genus258# * codim :259# returns : codimension260# * is_bic :261# returns : boolean262# * edgesatlevel :263# INPUT : level number (negative)264# returns : list of edges with at least one node at that level (or horizontal)265# * horizontaledgesatlevel :266# INPUT : level number (negative)267# returns : list of horizontal edges268# * nextlowerlevel :269# INPUT : level number270# returns : level number of next lower level271# * lowest_level :272# returns : level number of lowest level273# * is_horizontal :274# INPUT : edge (or none for whole graph)275# returns : boolean success276# * has_loop :277# INPUT : vertex278# returns : boolean success279# * edgesgoingupfromlevel :280# INPUT : level281# returns : list of edges with e[1] on level282# * verticesabove :283# INPUT : level284# returns : list of vertices with level > l285# * edgesabove :286# INPUT : level287# returns : list of edges with level of each vertex > l288# * crosseslevel :289# INPUT : edge, level290# returns : boolean291# * edgesgoingpastlevel :292# INPUT : level293# returns : list of edges with start level > l and294# end level < l295#########################################################296@cached_method297def vertex(self, leg):298"""299The vertex (as index of genera) where leg is located.300301Args:302leg (int): leg303304Returns:305int: index of genera306"""307return self.legdict[leg]308309@cached_method310def vertex_list(self):311return list(range(len(self.genera)))312313@cached_method314def edges_list(self):315return self.edges316317@cached_method318def levelofvertex(self, v):319return self.levels[v]320321@cached_method322def levelofleg(self, leg):323return self.levelofvertex(self.vertex(leg))324325@cached_method326def orderatleg(self, leg):327return self.poleorders[leg]328329@cached_method330def ordersonvertex(self, v):331return [self.orderatleg(leg) for leg in self.legsatvertex(v)]332333@cached_method334def verticesonlevel(self, level):335return [v for v in range(len(self.genera))336if self.levelofvertex(v) == level]337338@cached_method339def edgesatvertex(self, v):340return [e for e in self.edges if self.vertex(e[0]) == v or341self.vertex(e[1]) == v]342343@cached_method344def legsatvertex(self, v):345return self.legs[v]346347@cached_method348def is_halfedge(self, leg):349"""350Check if leg has an edge attached to it.351"""352return any(e[0] == leg or e[1] == leg for e in self.edges)353354@cached_method355def simplepolesatvertex(self, v):356return [l for l in self.legsatvertex(v) if self.orderatleg(l) == -1]357358@cached_method359def genus(self, v=None):360"""361Return the genus of vertex v.362363If v is None, return genus of the complete KLevelGraph.364"""365if v is None:366return self.g() # from admcycles367else:368return self.genera[v]369370@cached_method371def numberoflevels(self):372return len(set(self.levels))373374def is_bic(self):375if self._is_bic is None:376self._is_bic = self.numberoflevels() == 2 and not self.is_horizontal()377return self._is_bic378379@cached_method380def codim(self):381"""382Return the codim = No. of levels of self + horizontal edges.383"""384return (self.numberoflevels() - 1 +385sum(1 for e in self.edges if self.is_horizontal(e)))386387@cached_method388def edgesatlevel(self, level):389"""390Return list of edges with at least one node at level.391"""392return [e for e in self.edges if self.levelofleg(e[0]) == level or393self.levelofleg(e[1]) == level]394395@cached_method396def horizontaledgesatlevel(self, level):397return [e for e in self.edgesatlevel(level) if self.is_horizontal(e)]398399@cached_method400def nextlowerlevel(self, l):401"""402Return the next lower level number or False if no legal level403(e.g. lowest level) is given as input.404405Point of discussion: Is it better to tidy up the level numbers406to be directly ascending or not?407408Pro tidy: * this becomes obsolete ;)409* easier to check isomorphisms?410Con tidy: * we "see" where we came from411* easier to undo/glue back in after cutting412"""413try:414llindex = self.sortedlevels.index(l) - 1415except ValueError:416return False417if llindex == -1:418return False419else:420return self.sortedlevels[llindex]421422@cached_method423def internal_level_number(self, i):424"""425Return the internal i-th level, e.g.426427self.levels = [0,-2,-5,-3]428429then430431internal_level_number(0) is 0432internal_level_number(1) is -2433internal_level_number(2) is -3434internal_level_number(3) is -4435436Returns None if the level does not exist.437"""438reference_levels = list(reversed(sorted(list(set(self.levels)))))439i = abs(i)440if i >= len(reference_levels):441return None442else:443return reference_levels[i]444445@cached_method446def level_number(self, l):447"""448Return the relative level number (positive!) of l, e.g.449450self.levels = [0,-2,-5,-3]451452then453454level_number(0) is 0455level_number(-2) is 1456level_number(-3) is 2457level_number(-5) is 3458459Returns None if the level does not exist.460"""461reference_levels = list(reversed(sorted(list(set(self.levels)))))462try:463return reference_levels.index(l)464except ValueError:465return None466467@cached_method468def is_long(self, e):469"""470Check if edge e is long, i.e. passes through more than one level.471"""472try:473return self._is_long[e]474except KeyError:475il = abs(self.level_number(self.levelofleg(476e[0])) - self.level_number(self.levelofleg(e[1]))) > 1477self._is_long[e] = il478return il479480@cached_method481def has_long_edge(self):482if self._has_long_edge is None:483for e in self.edges:484if self.is_long(e):485self._has_long_edge = True486break487else:488self._has_long_edge = False489return self._has_long_edge490491@cached_method492def lowest_level(self):493return self.sortedlevels[0]494495@cached_method496def is_horizontal(self, e=None):497"""498Check if edge e is a horizontal edge or if self has a horizontal edge.499"""500if e is None:501return any(self.is_horizontal(e) for e in self.edges)502if e not in self.edges:503print("Warning: " + repr(e) + " is not an edge of this graph!")504return False505return self.levelofleg(e[0]) == self.levelofleg(e[1])506507@cached_method508def has_loop(self, v):509"""510Check if vertex v has a loop.511"""512return any(self.vertex(e[0]) == self.vertex(e[1])513for e in self.edgesatvertex(v))514515@cached_method516def edgesgoingupfromlevel(self, l):517"""518Return the edges going up from level l.519520This uses that e[0] is not on a lower level than e[1]!521"""522return [e for e in self.edges if self.levelofleg(e[1]) == l and523not self.is_horizontal(e)]524525@cached_method526def verticesabove(self, l):527"""528Return list of all vertices above level l.529530If l is None, return all vertices.531"""532if l is None:533return list(range(len(self.genera)))534return [v for v in range(len(self.genera))535if self.levelofvertex(v) > l]536537@cached_method538def edgesabove(self, l):539"""540Return a list of all edges above level l, i.e. start and end vertex541have level > l.542"""543return [e for e in self.edges544if self.levelofleg(e[0]) > l and self.levelofleg(e[1]) > l]545546@cached_method547def crosseslevel(self, e, l):548"""549Check if e crosses level l (i.e. starts > l and ends <= l)550"""551return self.levelofleg(e[0]) > l and self.levelofleg(e[1]) <= l552553@cached_method554def edgesgoingpastlevel(self, l):555"""556Return list of edges that go from above level l to below level l.557"""558return [e for e in self.edges559if self.levelofleg(e[0]) > l > self.levelofleg(e[1])]560#########################################################561# end interface562#########################################################563564#########################################################565# Checks566#########################################################567@cached_method568def checkadmissible(self, quiet=False):569"""570Run all kinds of checks on the level graph. Currently:571* Check if orders on each komponent add up to k*(2g-2)572* Check if graph is stable573* Check if orders at edges sum to -k*2574* Check if orders respect level crossing575"""576admissible = True577if not self.checkorders(quiet):578if not quiet:579print("Warning: Illegal orders!")580admissible = False581if not self.is_stable(quiet):582if not quiet:583print("Warning: Graph not stable!")584admissible = False585if not self.checkedgeorders(quiet):586if not quiet:587print("Warning: Illegal orders at a node!")588admissible = False589return admissible590591@cached_method592def checkorders(self, quiet=False):593"""594Check if the orders add up to k*(2g-2) on each component.595"""596for v, g in enumerate(self.genera):597if sum(self.ordersonvertex(v)) != self.sig.k * (2 * g - 2):598if not quiet:599print("Warning: Component " +600repr(v) +601" orders add up to " +602repr(sum(self.ordersonvertex(v))) +603" but component is of genus " +604repr(g))605return False606return True607608@cached_method609def is_stable(self, quiet=False):610"""611Check if graph is stable.612"""613# total graph:614e = 2 * self.genus() - 2 + len(self.leglist)615if e < 0:616if not quiet:617print("Warning: Total graph not stable! 2g-2+n = " + repr(e))618return False619620# components:621for v in range(len(self.genera)):622if 3 * self.genus(v) - 3 + len(self.legsatvertex(v)) < 0:623if not quiet:624print("Warning: Component " +625repr(v) +626" is not stable: g = " +627repr(self.genus(v)) +628" but only " +629repr(len(self.legsatvertex(v))) +630" leg(s)!")631return False632return True633634@cached_method635def checkedgeorders(self, quiet=False):636"""637Check that the orders at nodes (i.e. edges) sum to -k*2 and that lower638level has lower order.639"""640for e in self.edges:641orders = self.orderatleg(e[0]) + self.orderatleg(e[1])642if orders != -self.sig.k * 2:643if not quiet:644print(645"Warning: Orders at edge " +646repr(e) +647" add up to " +648repr(orders) +649" instead of -k*2!")650return False651# iff the pole order at e[0] is > the poleorder at e[1], e[0]652# should be on a higher level than e[1]653if (sign(self.orderatleg(e[0]) - self.orderatleg(e[1])) !=654sign(self.levelofleg(e[0]) - self.levelofleg(e[1]))):655if not quiet:656print("Warning: Orders at edge " + repr(e) +657" do not respect level crossing!")658print("Poleorders are",659(self.orderatleg(e[0]), self.orderatleg(e[1])),660"but levels are",661(self.levelofleg(e[0]), self.levelofleg(e[1]))662)663return False664return True665666#################################################################667# End checks668#################################################################669670#################################################################671# Cleanup functions672# USE ONLY IN __init__!!! KLevelGraphs should be static!!!673#################################################################674def _init_more(self):675"""676This should be used _only_ for initialisation!677678Make sure everything is in order, in particular:679* sortedlevels is fine680* leglist is fine681* legdict is fine682* maxleg is fine683* maxlevel is fine684* underlying graph is fine685"""686self.sortedlevels = sorted(self.levels)687# legs is a nested list:688self.leglist = flatten(self.legs)689# legs as dictionary690self.legdict = {l: v for v in range(len(self.genera))691for l in self.legs[v]}692self.maxleg = max([max(j + [0]) for j in self.legs])693# maxlevel is named a bit misleading, as it is the LOWEST level694# (the max of the absolute values...)695self.maxlevel = max([abs(l) for l in self.levels])696# construct the "underlying graph" (as a sage graph)697# the edges are labeled by their "real" name,698# the vertices are of the form (i,g_i,'LG')699# NOTE: In the case of an EmbeddedLevelGraph, vertices "at infinity" might700# be added to this!701self.underlying_graph = Graph([[self.UG_vertex(i) for i in range(len(self.genera))],702[(self.UG_vertex(self.vertex(e[0])), self.UG_vertex(self.vertex(e[1])), e)703for e in self.edges]],704format='vertices_and_edges', loops=True, multiedges=True)705706def UG_vertex(self, v):707"""708Vertex of the underlying graph corresponding to v.709710Args:711v (int): vertex number (index of self.genera)712713Returns:714tuple: tuple encoding the vertex of the Underlying Graph.715"""716return (v, self.genera[v], 'LG')717718#################################################################719# Extract a subgraph720#################################################################721def extract(self, vertices, edges):722"""723Extract the subgraph of self (as a KLevelGraph) consisting of vertices724(as list of indices) and edges (as list of edges).725726Returns the levelgraph consisting of vertices, edges and all legs on727vertices (of self) with their original poleorders and the original728level structure.729"""730newvertices = deepcopy([self.genera[i] for i in vertices])731newlegs = deepcopy([self.legs[i] for i in vertices])732newedges = deepcopy(edges)733newpoleorders = deepcopy({l: self.orderatleg(l)734for l in flatten(newlegs)})735newlevels = deepcopy([self.levels[i] for i in vertices])736return KLevelGraph(newvertices, newlegs, newedges,737newpoleorders, newlevels, self.sig.k)738739def levelGraph_from_subgraph(self, G):740"""741Returns the KLevelGraph associated to a subgraph of underlying_graph742(with the level structure induced by self)743"""744vertex_list = [v[0] for v in G.vertices(sort=True) if v[2] == 'LG']745return self.extract(vertex_list, G.edge_labels())746747def UG_vertices_above_level(self, l):748"""749The vertices above (internal) level l (including possible vertices at infinity).750751Used for checking residue conditions.752753Args:754l (int): internal level number755756Returns:757list: list of vertices of self.underlying_graph758"""759# note that verticesabove checks for >l760vertices_above = [self.UG_vertex(i) for i in self.verticesabove(l)]761vertices_above += [v for v in self.underlying_graph.vertices(sort=True)762if v[2] == 'res']763return vertices_above764765def UG_above_level(self, l):766"""767Subgraph of Underlying Graph (including vertices at infinity) (strictly) above768(relative!) level l.769770Args:771l (int): relative level number772773Returns:774SAGE Graph: subgraph of self.underlying_graph with all vertices strictly775above level l.776"""777internal_l = self.internal_level_number(l)778vertices = self.UG_vertices_above_level(internal_l)779abovegraph = self.underlying_graph.subgraph(vertices)780return abovegraph781782def UG_without_infinity(self):783"""784Subgraph of Underlying Graph without the vertices at infinity.785786Returns:787SAGE graph788"""789vertices = [self.UG_vertex(i) for i, _ in enumerate(self.genera)]790return self.underlying_graph.subgraph(vertices)791792#################################################################793# Squishing ---- i.e. contracting horizontal edges / levels794#################################################################795796def _squish_horizontal(self, e):797"""798Squish the horizontal edge e and returns the raw data for the new graph.799"""800# make sure v <= w801[v, w] = sorted([self.vertex(e[0]), self.vertex(e[1])])802803if not self.is_horizontal(e):804print("Warning: " + repr(e) +805" is not a horizontal edge -- Not contracting.")806return (self.genera, self.legs, self.edges,807self.poleorders, self.levels)808809newgenera = deepcopy(self.genera)810newlegs = deepcopy(self.legs)811newedges = deepcopy(self.edges)812newpoleorders = deepcopy(self.poleorders)813newlevels = deepcopy(self.levels)814if v == w:815# --horizontal loop getting contracted---816# add genus to node:817newgenera[v] += 1818else:819# --horizontal edge getting contracted---820# transfer genus and legs from w to v:821newgenera[v] += newgenera[w]822newlegs[v] += newlegs[w]823# remove all traces of w824newgenera.pop(w)825newlegs.pop(w)826newlevels.pop(w)827# remove edge:828newedges.remove(e)829# remove legs830newlegs[v].remove(e[0])831newlegs[v].remove(e[1])832# remove poleorders833del newpoleorders[e[1]]834del newpoleorders[e[0]]835836return [newgenera, newlegs, newedges, newpoleorders, newlevels]837838def squish_horizontal(self, e):839"""840Squish the horizontal edge e and return the new graph.841842EXAMPLES::843844sage: from admcycles.diffstrata.klevelgraph import KLevelGraph845sage: G = KLevelGraph.fromPOlist([1,1], [[1,2],[3,4]], [(2,3)], [1,-1,-1,1],[0,0],1)846sage: G.squish_horizontal((2,3))847KLevelGraph([2],[[1, 4]],[],{1: 1, 4: 1},[0],1,True)848"""849# Python2 only allows keyword arguments after *() arguments, so we need850# to repack851dat = self._squish_horizontal(e) + [self.sig.k]852return KLevelGraph(*dat, quiet=True)853854def squish_vertical_slow(self, l, adddata=False, quiet=False):855"""856Squish the level l (and the next lower level!) and return the new graph.857If addata=True is specified, we additionally return a boolean that tells858us if a level was squished and the legs that no longer exist.859860More precisely, adddata=True returns a tuple (G,boolean,legs) where861* G is the (possibly non-contracted) graph and862* boolean tells us if a level was squished863* legs is the (possibly empty) list of legs that don't exist anymore864865Implementation:866At the moment, we remember all the edges (ee) that connect level l to867level l-1. We then create a new KLevelGraph with the same data as self,868the only difference being that all vertices of level l-1 have now been869assigned level l. We then remove all (now horizontal!) edges in ee.870871In particular, all points and edges retain their numbering (only the level might have changed).872873WARNING: Level l-1 does not exist anymore afterwards!!!874875Downside: At the moment we get a warning for each edge in ee, as these876don't have legal pole orders for being horizontal (so checkedgeorders877complains each time the constructor is called :/).878"""879if l is None:880return self881ll = self.nextlowerlevel(l)882if ll is False:883if not quiet:884print("Warning: Illegal level to contract: " + repr(l))885if adddata:886return (self, False, None)887else:888return self889890if not quiet:891print("Squishing levels", l, "and", ll)892893vv = self.verticesonlevel(ll)894# edges that go to next lower level, i.e. will be contracted895ee = [e for e in self.edgesatlevel(l)896if self.levelofleg(e[1]) == ll and not self.is_horizontal(e)]897898if not quiet:899print("Contracting edges", ee)900901newgenera = deepcopy(self.genera)902newlegs = deepcopy(self.legs)903newedges = deepcopy(self.edges)904newpoleorders = deepcopy(self.poleorders)905newlevels = deepcopy(self.levels)906907# raise level908for v in vv:909newlevels[v] = l910returngraph = KLevelGraph(911newgenera,912newlegs,913newedges,914newpoleorders,915newlevels,916self.sig.k,917True)918# contract edges that are now horizontal:919for e in ee:920returngraph = returngraph.squish_horizontal(e)921922if adddata:923return (returngraph, True, flatten(ee))924else:925return returngraph926927def _squish_vertical(self, l, quiet=True):928"""929Squish the level l (and the next lower level!) and return the raw data for the new graph.930931Implementation:932At the moment, we remember all the edges (ee) that connect level l to933level l-1. We then create a new KLevelGraph with the same data as self,934the only difference being that all vertices of level l-1 have now been935assigned level l. We then remove all edges in ee.936937(In contrast to the old squish_vertical_slow, this is now done in938one step and not recursively so we avoid a lot of the copying of graphs.)939940In particular, all points and edges retain their numbering (only the level might have changed).941942WARNING: Level l-1 does not exist anymore afterwards!!!943944Args:945l (int): (internal) level number946quiet (bool, optional): No output. Defaults to True.947948Returns:949tuple: raw data of the new graph950"""951if l is None:952return self953ll = self.nextlowerlevel(l)954levelset = set([l, ll])955if ll is False:956if not quiet:957print("Warning: Illegal level to contract: " + repr(l))958else:959return (self.genera, self.legs, self.edges,960self.poleorders, self.levels)961vertices = self.verticesonlevel(l) + self.verticesonlevel(ll)962# edges that go to next lower level, i.e. will be contracted963edges = [e for e in self.edges if set(964[self.levelofleg(e[0]), self.levelofleg(e[1])]) == levelset]965# we use two dictionaries for quick lookup while we reorder the info:966genus_of_vertex = {v: self.genus(v) for v in vertices}967vertex_of_leg = {968leg: v for v in vertices for leg in self.legsatvertex(v)}969legs_of_vertex = {v: self.legsatvertex(v)[:] for v in vertices}970deleted_legs = []971while edges:972start, end = edges.pop()973v = vertex_of_leg[start]974w = vertex_of_leg[end]975del vertex_of_leg[start]976del vertex_of_leg[end]977legs_of_vertex[v].remove(start)978legs_of_vertex[w].remove(end)979deleted_legs.extend([start, end])980if v == w:981# if e (now) connects a vertex to itself, increase its genus982genus_of_vertex[v] += 1983else:984# if e connects two different vertices, combine their data985# (moving everything from w to v)986genus_of_vertex[v] += genus_of_vertex[w]987del genus_of_vertex[w]988for leg in legs_of_vertex[w]:989vertex_of_leg[leg] = v990legs_of_vertex[v].append(leg)991del legs_of_vertex[w]992# Build new graph with this data:993# We use sets for more efficient lookup:994vertices = set(vertices)995deleted_legs = set(deleted_legs)996997newgenera = []998newlegs = []999newlevels = []1000# we copy the untouched vertices and insert the new ones:1001for v in range(len(self.genera)):1002if v in vertices:1003if v in genus_of_vertex:1004newgenera.append(genus_of_vertex[v])1005newlegs.append(legs_of_vertex[v][:])1006newlevels.append(l)1007# otherwise it has been deleted1008else:1009newgenera.append(self.genus(v))1010newlegs.append(self.legsatvertex(v)[:])1011newlevels.append(self.levelofvertex(v))10121013# for the edges, we remove those with a deleted edge:1014newedges = []1015for start, end in self.edges:1016if start in deleted_legs:1017assert end in deleted_legs1018continue1019assert end not in deleted_legs1020newedges.append((start, end))1021# for the new poleorders, we only have to remove the deleted legs:1022newpoleorders = {1023leg: p for leg,1024p in self.poleorders.items() if leg not in deleted_legs}10251026return [newgenera, newlegs, newedges, newpoleorders, newlevels]10271028def squish_vertical(self, l, quiet=True):1029"""1030Squish the level l (and the next lower level!) and return the new graph.10311032In particular, all points and edges retain their numbering (only the level might have changed).10331034WARNING: Level l-1 does not exist anymore afterwards!!!10351036Args:1037l (int): (internal) level number1038quiet (bool, optional): No output. Defaults to True.10391040Returns:1041KLevelGraph: new levelgraph with one level less.10421043EXAMPLES::10441045sage: from admcycles.diffstrata.klevelgraph import KLevelGraph1046sage: G = KLevelGraph.fromPOlist([1,2], [[1],[2,3,4]], [(1,2)], [0,-2,1,3],[0,-1],1)1047sage: G.squish_vertical(0)1048KLevelGraph([3],[[3, 4]],[],{3: 1, 4: 3},[0],1,True)1049"""1050# Python2 only allows keyword arguments after *() arguments, so we need1051# to repack1052dat = self._squish_vertical(l, quiet=quiet) + [self.sig.k]1053return KLevelGraph(*dat, quiet=True)10541055def delta(self, k, quiet=False):1056"""1057Squish all levels except for the k-th.10581059Note that delta(1) contracts everything except top-level and that1060the argument is interpreted via internal_level_number10611062WARNING: Currently giving an out of range level (e.g.10630 or >= maxlevel) squishes the entire graph!!!10641065Return the corresponding divisor.1066"""1067G = self1068if self.is_horizontal():1069print("Error: Cannot delta a graph with horizontal edges!")1070return deepcopy(G)1071# recursive squishing forces us to go backwards, as the lower squished1072# level is always removed.1073for i in reversed(range(self.numberoflevels() - 1)):1074# unfortunately delta and squish use slightly incompatible level1075# numbering, as the "illegal" squish here is k-11076if abs(k) - 1 == i:1077continue1078if not quiet:1079print("Squishing level", i)1080# note that it is important that internal_level_number of1081# self and not G is called in the argument, as the relative1082# numbering of levels in G changes, but the internal names don't1083G = G.squish_vertical(self.internal_level_number(i), quiet=quiet)1084return G10851086#################################################################1087# end Squishing1088#################################################################10891090#################################################################1091# Isomorphisms1092# DEPRECATED!!!! USE EmbeddedLevelGraphs instead!1093####1094# This only remains for the BIC generation comparison tests.1095#################################################################1096def _init_invariant(self):1097"""1098DEPRECATED!! DO NOT USE!!10991100Compute a bunch of invariants (as a quick check for not isommorphic).1101Up to now we have:1102* sorted list of genera (by level)1103* number of edges1104* number of levels1105* names and pole orders of marked points1106* sorted list of pole orders1107"""1108self._invariant = (1109len(self.edges),1110self.codim(),1111[(l, self.orderatleg(l))1112for l in sorted(self.list_markings())],1113sorted([self.orderatleg(l) for l in self.leglist]))11141115def invariant(self):1116"""1117DEPRECATED!!! DO NOT USE!!!1118"""1119self._init_invariant()1120return self._invariant11211122def _genisom_preserves_levels(self, other, dV):1123"""1124Check if a "vertex isomorphism" preserves the (relative) level structure1125"""1126return all(self.sortedlevels.index(self.levelofvertex(sv)) ==1127other.sortedlevels.index(other.levelofvertex(ov))1128for sv, ov in dV.items())11291130def _legisom_preserves_poleorders(self, other, dL):1131"""1132Check if a "leg isomorphism" preserves pole orders.1133"""1134return all(self.orderatleg(sl) == other.orderatleg(ol)1135for sl, ol in dL.items())11361137def _leveliso(self, other, dV):1138"""1139Give a dictionary translating the levels of self to other in accordance1140with the dictionary of vertices dV.1141"""1142return {l: other.levelofvertex(dV[self.verticesonlevel(l)[0]])1143for l in self.levels}11441145def is_isomorphic(self, other, adddata=False):1146"""1147DEPRECATED!!! Instead, use EmbeddedLevelGraph!!!11481149Check if self is isomorphic to other.1150Return boolean + list of isomorphisms if adddata=True.11511152Note that our isomorphisms are stgraph isomorphisms plus a dictionary1153translating the level structure of self into that of other.11541155At the moment we use admcycles.GraphIsom and check which of these1156preserve the KLevelGraph structure (using the above invariants, though!)1157Probably it would be much faster to reimplement this for KLevelGraphs1158as it seems quite redundant as is...1159"""1160isoms = GraphIsom(self.stgraph, other.stgraph)1161# check if the stable graph isomorphisms preserve level structure1162# GraphIsoms consist of a tuple (dV,dL), where dV is a dictionary1163# translating the vertices and dL is one for the legs.1164levelisoms = [[dV, dL, self._leveliso(other, dV)] for [dV, dL] in isoms1165if self._genisom_preserves_levels(other, dV) and1166self._legisom_preserves_poleorders(other, dL)]1167if adddata:1168return (bool(levelisoms), levelisoms)1169else:1170return (bool(levelisoms))11711172#################################################################1173# Plotting1174#################################################################1175def plot_obj(self):1176"""1177Return a sage graphics object generated from a sage graph.11781179TODO: Some things that are still ugly:1180* No legs!!1181* currently the vertices have labels (index,genus)1182* loops are vertical not horizontal1183"""1184# Create the underlying graph with edge label prong order.1185G = Graph([self.genera,1186[(self.vertex(e[0]), self.vertex(e[1]), self.prong(e))1187for e in self.edges]],1188format='vertices_and_edges', loops=True, multiedges=True)1189# unfortunately, sage insists of displaying the vertex name (index)1190# and not only the label (genus), so we display both...1191G.relabel(list(enumerate(self.genera)))1192h = {l: [(v, self.genus(v)) for v in self.verticesonlevel(l)]1193for l in self.levels}1194# at the moment we just display a list of legs and orders at the bottom1195legtext = "Marked points: "1196for l in sorted(set(self.levels)):1197points_at_level = [leg for leg in self.list_markings()1198if self.levelofleg(leg) == l]1199if not points_at_level:1200continue1201legtext += "\nAt level %s: " % l1202for leg in points_at_level:1203legtext += "\n" + self._pointtype(leg)1204legtext += " of order " + repr(self.orderatleg(leg))1205print(legtext)1206return G.plot(edge_labels=True, vertex_size=1000,1207heights=h, layout='ranked') + text(legtext, (0, -0.1),1208vertical_alignment='top', axis_coords=True, axes=False)120912101211