| Download
Project: admcycles
Views: 724Visibility: Unlisted (only visible to those who know the link)
Image: ubuntu2004# -*- coding: utf-8 -*-1from collections import defaultdict23from sage.misc.misc_c import prod45from sage.misc.cachefunc import cached_method # pylint: disable=import-error67from sage.combinat.subset import Subsets8from sage.rings.integer_ring import ZZ9from sage.modules.free_module import FreeModule10from sage.functions.other import floor11from sage.graphs.graph import Graph12from sage.rings.all import Integer13from sage.arith.all import factorial14from sage.groups.perm_gps.permgroup_named import SymmetricGroup15from sage.structure.sage_object import SageObject1617# graphics:18from sage.plot.polygon import polygon2d19from sage.plot.text import text20from sage.plot.bezier_path import bezier_path21from sage.plot.line import line22from sage.plot.circle import circle2324class StableGraph(SageObject):25r"""26Stable graph.2728A stable graph is a graph (with allowed loops and multiple edges) that29has "genus" and "marking" decorations at its vertices. It is represented30as a list of genera of its vertices, a list of legs at each vertex and a31list of pairs of legs forming edges.3233The markings (the legs that are not part of edges) are allowed to have34repetitions.3536EXAMPLES:3738We create a stable graph with two vertices of genera 3,5 joined by an edge39with a self-loop at the genus 3 vertex::4041sage: from admcycles import StableGraph42sage: StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])43[3, 5] [[1, 3, 5], [2]] [(1, 2), (3, 5)]4445The markings can have repetitions::4647sage: StableGraph([1,0], [[1,2], [3,2]], [(1,3)])48[1, 0] [[1, 2], [3, 2]] [(1, 3)]4950It is also possible to create graphs which are not neccesarily stable::5152sage: StableGraph([1,0], [[1], [2,3]], [(1,2)])53[1, 0] [[1], [2, 3]] [(1, 2)]5455sage: StableGraph([0], [[1]], [])56[0] [[1]] []5758If the input is invalid a ``ValueError`` is raised::5960sage: StableGraph([0, 0], [[1], [2], [3]], [])61Traceback (most recent call last):62...63ValueError: genera and legs must have the same length6465sage: StableGraph([0, 'hello'], [[1], [2]], [(1,2)])66Traceback (most recent call last):67...68ValueError: genera must be a list of non-negative integers6970sage: StableGraph([0, 0], [[1,2], [3]], [(3,4)])71Traceback (most recent call last):72...73ValueError: the edge (3, 4) uses invalid legs7475sage: StableGraph([0, 0], [[2,3], [2]], [(2,3)])76Traceback (most recent call last):77...78ValueError: the edge (2, 3) uses invalid legs79"""80__slots__ = ['_genera', '_legs', '_edges', '_maxleg', '_mutable',81'_graph_cache', '_canonical_label_cache', '_hash']8283def __init__(self, genera, legs, edges, mutable=False, check=True):84"""85INPUT:8687- ``genera`` -- (list) List of genera of the vertices of length m.8889- ``legs`` -- (list) List of length m, where ith entry is list of legs90attached to vertex i.9192- ``edges`` -- (list) List of edges of the graph. Each edge is a 2-tuple of legs.9394- ``mutable`` - (boolean, default to ``False``) whether this stable graph95should be mutable9697- ``check`` - (boolean, default to ``True``) whether some additional sanity98checks are performed on input99"""100if check:101if not isinstance(genera, list) or \102not isinstance(legs, list) or \103not isinstance(edges, list):104raise TypeError('genera, legs, edges must be lists')105106if len(genera) != len(legs):107raise ValueError('genera and legs must have the same length')108109if not all(isinstance(g, (int, Integer)) and g >= 0 for g in genera):110raise ValueError("genera must be a list of non-negative integers")111112mlegs = defaultdict(int)113for v,l in enumerate(legs):114if not isinstance(l, list):115raise ValueError("legs must be a list of lists")116for i in l:117if not isinstance(i, (int, Integer)) or i <= 0:118raise ValueError("legs must be positive integers")119mlegs[i] += 1120121for e in edges:122if not isinstance(e, tuple) or len(e) != 2:123raise ValueError("invalid edge {}".format(e))124if mlegs.get(e[0], 0) != 1 or mlegs.get(e[1], 0) != 1:125raise ValueError("the edge {} uses invalid legs".format(e))126127self._genera = genera128self._legs = legs129self._edges = edges130self._maxleg = max([max(j+[0]) for j in self._legs]) #highest leg-label that occurs131self._mutable = bool(mutable)132self._graph_cache = None133self._canonical_label_cache = None134self._hash = None135136def set_immutable(self):137r"""138Set this graph immutable.139"""140self._mutable = False141142def is_mutable(self):143r"""144Return whether this graph is mutable.145"""146return self._mutable147148def __hash__(self):149r"""150EXAMPLES::151152sage: from admcycles import StableGraph153sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])154sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])155sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])156sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])157sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])158sage: hash(G1) == hash(G2)159True160sage: (hash(G1) == hash(G3) or hash(G1) == hash(G4) or161....: hash(G1) == hash(G5) or hash(G3) == hash(G4) or162....: hash(G3) == hash(G5) or hash(G4) == hash(G5))163False164"""165if self._mutable:166raise TypeError("mutable stable graph are not hashable")167if self._hash is None:168self._hash = hash((tuple(self._genera),169tuple(tuple(x) for x in self._legs),170tuple(self._edges)))171return self._hash172173def copy(self, mutable=True):174r"""175Return a copy of this graph.176177When it is asked for an immutable copy of an immutable graph the178current graph is returned without copy.179180INPUT:181182- ``mutable`` - (boolean, default ``True``) whether the returned graph must183be mutable184185EXAMPLES::186187sage: from admcycles import StableGraph188sage: G = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])189sage: H = G.copy()190sage: H == G191True192sage: H.is_mutable()193True194sage: H is G195False196197sage: G.copy(mutable=False) is G198True199"""200if not self.is_mutable() and not mutable:201# avoid copy when immutability holds for both202return self203G = StableGraph.__new__(StableGraph)204G._genera = self._genera[:]205G._legs = [x[:] for x in self._legs]206G._edges = [x[:] for x in self._edges]207G._maxleg = self._maxleg208G._mutable = mutable209G._graph_cache = None210G._canonical_label_cache = None211G._hash = None212return G213214def __repr__(self):215return repr(self._genera)+ ' ' + repr(self._legs) + ' ' + repr(self._edges)216217def __eq__(self, other):218r"""219TESTS::220221sage: from admcycles import StableGraph222sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])223sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])224sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])225sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])226sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])227sage: G1 == G2228True229sage: G1 == G3 or G1 == G4 or G1 == G5230False231"""232if type(self) is not type(other):233return False234return self._genera == other._genera and \235self._legs == other._legs and \236self._edges == other._edges237238def __ne__(self, other):239return not (self == other)240241# @cached_method242def _graph(self):243if not self._mutable and self._graph_cache is not None:244return self._graph_cache245246# TODO: it should be much faster to build this graph...247from collections import defaultdict248249legs = defaultdict(list)250251# the multiplicity of edges is stored as integral labels252# on the graph253G = Graph(len(self._genera), loops=True, multiedges=False, weighted=True)254for li,lj in self._edges:255i = self.vertex(li)256j = self.vertex(lj)257if G.has_edge(i, j):258G.set_edge_label(i, j, G.edge_label(i, j) + 1)259else:260G.add_edge(i, j, 1)261262if i < j:263legs[i,j].append((li,lj))264else:265legs[j,i].append((lj,li))266267vertex_partition = defaultdict(list)268for i, (g,l) in enumerate(zip(self._genera, self._legs)):269l = list(self.list_markings(i))270l.sort()271inv = (g,) + tuple(l)272vertex_partition[inv].append(i)273274vertex_data = sorted(vertex_partition)275partition = [vertex_partition[k] for k in vertex_data]276277output = (G, vertex_data, dict(legs), partition)278if not self._mutable:279self._graph_cache = output280return output281282def _canonical_label(self):283if not self._mutable and self._canonical_label_cache is not None:284return self._canonical_label_cache285G, _, _, partition = self._graph()286287# NOTE: we explicitely set algorithm='sage' since bliss got confused288# with edge labeled graphs. See289# https://trac.sagemath.org/ticket/28531290H, phi = G.canonical_label(partition=partition,291edge_labels=True,292certificate=True,293algorithm='sage')294295output = (H, phi)296if not self._mutable:297self._canonical_label_cache = output298return output299300def set_canonical_label(self, certificate=False):301r"""302Set canonical label to this stable graph.303304The canonical labeling is such that two isomorphic graphs get relabeled305the same way. If certificate is true return a pair `dicv`, `dicl` of306mappings from the vertices and legs to the canonical ones.307308EXAMPLES::309310sage: from admcycles import StableGraph311312sage: S = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)])313314sage: T = S.copy()315sage: T.set_canonical_label()316sage: T # random317[1, 2, 3] [[1, 4, 6], [2, 5, 8], [3, 7, 9]] [(4, 5), (6, 7), (8, 9)]318sage: assert S.is_isomorphic(T)319320Check that isomorphic graphs get relabelled the same way::321322sage: S0 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)], mutable=False)323sage: S1 = S0.copy()324sage: S2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)], mutable=True)325sage: S3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)], mutable=True)326sage: S1.set_canonical_label()327sage: S2.set_canonical_label()328sage: S3.set_canonical_label()329sage: assert S1 == S2 == S3330sage: assert S0.is_isomorphic(S1)331332sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=False)333sage: S1 = S0.copy()334sage: S2 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)335sage: S3 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)336sage: S1.set_canonical_label()337sage: S2.set_canonical_label()338sage: S3.set_canonical_label()339sage: assert S1 == S2 == S3340sage: assert S0.is_isomorphic(S1)341342sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=True)343sage: S1 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)344sage: S2 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)345sage: for S in [S0, S1, S2]:346....: T = S.copy()347....: assert S == T348....: vm, lm = T.set_canonical_label(certificate=True)349....: S.relabel(vm, lm, inplace=True)350....: S.tidy_up()351....: assert S == T, (S, T)352353354TESTS::355356sage: from admcycles import StableGraph357sage: S0 = StableGraph([2], [[]], [], mutable=True)358sage: S0.set_canonical_label()359360sage: S0 = StableGraph([0,1,0], [[1,2,4], [3], [5,6,7]], [(2,3), (4,5), (6,7)])361sage: S1 = S0.copy()362sage: S1.set_canonical_label()363sage: assert S0.is_isomorphic(S1)364sage: S2 = S1.copy()365sage: S1.set_canonical_label()366sage: assert S1 == S2367368sage: S0 = StableGraph([1,0], [[2,3], [5,7,9]], [(2,5), (7,9)], mutable=True)369sage: S1 = StableGraph([1,0], [[1,3], [2,7,9]], [(1,2), (7,9)], mutable=True)370sage: S0.set_canonical_label()371sage: S1.set_canonical_label()372sage: assert S0 == S1373"""374if not self._mutable:375raise ValueError("the graph must be mutable; use inplace=False")376377G, vdat, legs, part = self._graph()378H, dicv = self._canonical_label()379invdicv = {j:i for i,j in dicv.items()}380if certificate:381dicl = {}382383# vdat: list of the possible (g, l0, l1, ...)384# where l0, l1, ... are the markings385# legs: dico: (i,j) -> list of edges given by pair of legs386387new_genera = [self._genera[invdicv[i]] for i in range(self.num_verts())]388assert new_genera == sorted(self._genera)389new_legs = [sorted(self.list_markings(invdicv[i])) for i in range(self.num_verts())]390391can_edges = []392for e0,e1,mult in G.edges():393f0 = dicv[e0]394f1 = dicv[e1]395inv = False396if f0 > f1:397f0,f1 = f1,f0398inv = True399can_edges.append((f0,f1,e0,e1,mult,inv))400can_edges.sort()401402nl = self.num_legs()403marks = set(self.list_markings())404m0 = (max(marks) if marks else 0) + 1405non_markings = range(m0, m0 + nl - len(marks))406assert len(non_markings) == 2 * sum(mult for _,_,_,_,mult,_ in can_edges)407408k = 0409new_edges = []410for f0,f1,e0,e1,mult,inv in can_edges:411assert f0 <= f1412new_legs[f0].extend(non_markings[i] for i in range(k, k+2*mult, 2))413new_legs[f1].extend(non_markings[i] for i in range(k+1, k+2*mult, 2))414new_edges.extend((non_markings[i], non_markings[i+1]) for i in range(k, k+2*mult, 2))415416if certificate:417assert len(legs[e0, e1]) == mult, (len(legs[e0, e1]), mult)418if inv:419assert dicv[e0] == f1420assert dicv[e1] == f0421for i, (l1,l0) in enumerate(legs[e0,e1]):422dicl[l0] = non_markings[k + 2*i]423dicl[l1] = non_markings[k + 2*i + 1]424else:425assert dicv[e0] == f0426assert dicv[e1] == f1427for i, (l0,l1) in enumerate(legs[e0,e1]):428dicl[l0] = non_markings[k + 2*i]429dicl[l1] = non_markings[k + 2*i + 1]430431k += 2*mult432433self._genera = new_genera434self._legs = new_legs435self._edges = new_edges436437if certificate:438return (dicv, dicl)439440# TODO: to keep consistancy with reorder_vertices and rename_legs, should441# we do the relabeling inplace?442# (sage graphs do not do inplace by default)443def relabel(self, vm, lm, inplace=False, mutable=False):444r"""445INPUT:446447- ``vm`` -- (dictionary) a vertex map (from the label of this graph to448the new labels). If a vertex number is missing it will remain untouched.449450- ``lm`` -- (dictionary) a leg map (idem). If a leg label is missing it451will remain untouched.452453- ``inplace`` -- (boolean, default ``False``) whether to relabel the454graph inplace455456- ``mutable`` -- (boolean, default ``False``) whether to make the457resulting graph immutable. Ignored when ``inplace=True``458459EXAMPLES::460461sage: from admcycles import StableGraph462sage: G = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])463sage: vm = {0:1, 1:0}464sage: lm = {1:3, 2:5, 3:7, 4:9, 5:15}465sage: G.relabel(vm, lm)466[5, 3] [[5], [3, 7, 15, 9]] [(3, 5), (7, 15)]467468sage: G.relabel(vm, lm, mutable=False).is_mutable()469False470sage: G.relabel(vm, lm, mutable=True).is_mutable()471True472473sage: H = G.copy()474sage: H.relabel(vm, lm, inplace=True)475sage: H == G.relabel(vm, lm)476True477"""478m = len(self._genera)479genera = [None] * m480legs = [None] * m481for i,(g,l) in enumerate(zip(self._genera, self._legs)):482j = vm.get(i, i) # new label483genera[j] = g484legs[j] = [lm.get(k, k) for k in l]485486edges = [(lm.get(i, i), lm.get(j, j)) for i, j in self._edges]487488if inplace:489self._genera = genera490self._legs = legs491self._edges = edges492else:493return StableGraph(genera, legs, edges, mutable)494495def is_isomorphic(self, other, certificate=False):496r"""497Test whether this stable graph is isomorphic to ``other``.498499INPUT:500501- ``certificate`` - if set to ``True`` return also a vertex mapping and502a legs mapping503504EXAMPLES::505506sage: from admcycles import StableGraph507508sage: G1 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])509sage: G2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)])510sage: G3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)])511sage: G1.is_isomorphic(G2) and G1.is_isomorphic(G3)512True513514Graphs with distinct markings are not isomorphic::515516sage: G4 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,4)])517sage: G1.is_isomorphic(G4) or G2.is_isomorphic(G4) or G3.is_isomorphic(G4)518False519520Graph with marking multiplicities::521522sage: H1 = StableGraph([0], [[1,1]], [])523sage: H2 = StableGraph([0], [[1,1]], [])524sage: H1.is_isomorphic(H2)525True526527sage: H3 = StableGraph([0,0], [[1,2,4],[1,3]], [(2,3)])528sage: H4 = StableGraph([0,0], [[1,2],[1,3,4]], [(2,3)])529sage: H3.is_isomorphic(H4)530True531532TESTS::533534sage: from admcycles import StableGraph535536sage: G = StableGraph([0, 0], [[5, 8, 4, 3], [9, 2, 1]], [(8, 9)])537sage: H = StableGraph([0, 0], [[1, 2, 6], [3, 4, 5, 7]], [(6, 7)])538sage: G.is_isomorphic(H)539True540sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)541sage: assert ans542sage: G.relabel(vm, lm)543[0, 0] [[6, 2, 1], [5, 7, 4, 3]] [(7, 6)]544545sage: G = StableGraph([0, 0], [[4, 8, 2], [3, 9, 1]], [(8, 9)])546sage: H = StableGraph([0, 0], [[1, 3, 5], [2, 4, 6]], [(5, 6)])547sage: G.is_isomorphic(H)548True549sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)550sage: assert ans551sage: G.relabel(vm, lm)552[0, 0] [[3, 5, 1], [4, 6, 2]] [(6, 5)]553554sage: G = StableGraph([0, 1, 1], [[1, 3, 5], [2, 4], [6]], [(1, 2), (3, 4), (5, 6)])555sage: H = StableGraph([1, 0, 1], [[1], [2, 3, 5], [4, 6]], [(1, 2), (3, 4), (5, 6)])556sage: _ = G.is_isomorphic(H, certificate=True)557558Check for https://gitlab.com/jo314schmitt/admcycles/issues/22::559560sage: g1 = StableGraph([0, 0], [[1, 3], [2,4]], [(1, 2), (3, 4)])561sage: g2 = StableGraph([0, 0], [[1], [2]], [(1, 2)])562sage: g1.is_isomorphic(g2)563False564565Check for https://gitlab.com/jo314schmitt/admcycles/issues/24::566567sage: gr1 = StableGraph([0, 0, 0, 2, 1, 2], [[1, 2, 6], [3, 7, 8], [4, 5, 9], [10], [11], [12]], [(6, 7), (8, 9), (3, 10), (4, 11), (5, 12)])568sage: gr2 = StableGraph([0, 0, 1, 1, 1, 2], [[1, 2, 5, 6, 7], [3, 4, 8], [9], [10], [11], [12]], [(7, 8), (3, 9), (4, 10), (5, 11), (6, 12)])569sage: gr1.is_isomorphic(gr2)570False571"""572if type(self) != type(other):573raise TypeError574575sG, svdat, slegs, spart = self._graph()576oG, ovdat, olegs, opart = other._graph()577578# first compare vertex data partitions579if svdat != ovdat or any(len(p) != len(q) for p,q in zip(spart, opart)):580return (False, None) if certificate else False581582# next, graph isomorphism583# NOTE: since canonical label maps the first atom of the partition to584# {0, ..., m_1 - 1} the second to {m_1, ..., m_1 + m_2 - 1}, etc we are585# guaranteed that the partitions match when the graphs are isomorphic.586sH, sphi = self._canonical_label()587oH, ophi = other._canonical_label()588if sH != oH:589return (False, None) if certificate else False590591if certificate:592ophi_inv = {j: i for i,j in ophi.items()}593vertex_map = {i: ophi_inv[sphi[i]] for i in range(len(self._genera))}594595legs_map = {}596# legs that are not part of edges are untouched597# (slot zero in the list is the genus)598for l in svdat:599for i in l[1:]:600legs_map[i] = i601602# pair of legs that form edges have moved603for (i,j),sl in slegs.items():604m = sG.edge_label(i, j)605assert len(sl) == m, (m, sl, self, other)606607ii = ophi_inv[sphi[i]]608jj = ophi_inv[sphi[j]]609if ii <= jj:610ol = olegs[ii,jj]611else:612ol = [(b,a) for (a,b) in olegs[jj,ii]]613assert len(ol) == m, (m, sl, ol, self, other)614615for (a,b),(c,d) in zip(sl,ol):616legs_map[a] = c617legs_map[b] = d618619return True, (vertex_map, legs_map)620621else:622return True623624def vertex_automorphism_group(self, *args, **kwds):625r"""626Return the action of the automorphism group on the vertices.627628All arguments provided are forwarded to the method `automorphism_group`629of Sage graphs.630631EXAMPLES::632633sage: from admcycles import StableGraph634635sage: G = StableGraph([0], [[1, 2, 3, 4, 5, 6]], [(3, 4), (5, 6)])636sage: G.vertex_automorphism_group()637Permutation Group with generators [()]638639sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])640sage: G.vertex_automorphism_group()641Permutation Group with generators [(0,1)]642sage: G = StableGraph([0, 0], [[4, 1, 2], [1, 3, 4]], [(3, 2)])643sage: G.vertex_automorphism_group()644Permutation Group with generators [(0,1)]645646sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],647....: [(2,3),(4,5),(6,1)])648sage: A = G.vertex_automorphism_group()649sage: A650Permutation Group with generators [(1,2), (0,1)]651sage: A.cardinality()6526653654Using extra arguments::655656sage: G.vertex_automorphism_group(algorithm='sage')657Permutation Group with generators [(1,2), (0,1)]658sage: G.vertex_automorphism_group(algorithm='bliss') # optional - bliss659Permutation Group with generators [(1,2), (0,1)]660"""661G, _, _, partition = self._graph()662return G.automorphism_group(partition=partition,663edge_labels=True,664*args, **kwds)665666def leg_automorphism_induce(self, g, A=None, check=True):667r"""668Given a leg automorphism ``g`` return its action on the vertices.669670Note that there is no check that the element ``g`` is a valid automorphism.671672INPUT:673674- ``g`` - a permutation acting on the legs675676- ``A`` - ambient permutation group for the result677678- ``check`` - (default ``True``) parameter forwarded to the constructor679of the permutation group acting on vertices.680681EXAMPLES::682683sage: from admcycles import StableGraph684sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],685....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])686sage: Averts = G.vertex_automorphism_group()687sage: Alegs = G.leg_automorphism_group()688sage: g = Alegs('(1,14)(2,13)(3,4)(5,10)(6,9)(7,8)(11,12)(15,16)')689sage: assert G.leg_automorphism_induce(g) == Averts('(0,3)(1,2)')690sage: g = Alegs('(3,11)(4,12)(5,13)(6,14)')691sage: assert G.leg_automorphism_induce(g) == Averts('')692sage: g = Alegs('(1,11,13,15,9,3,5,7)(2,12,14,16,10,4,6,8)')693sage: assert G.leg_automorphism_induce(g) == Averts('(0,1,2,3)')694695TESTS::696697sage: G = StableGraph([3], [[]], [])698sage: S = SymmetricGroup([0])699sage: G.leg_automorphism_induce(S(''))700()701"""702if A is None:703A = SymmetricGroup(list(range(len(self._genera))))704if len(self._genera) == 1:705return A('')706p = [self.vertex(g(self._legs[u][0])) for u in range(len(self._genera))]707return A(p, check=check)708709def vertex_automorphism_lift(self, g, A=None, check=True):710r"""711Provide a canonical lift of vertex automorphism to leg automorphism.712713Note that there is no check that ``g`` defines a valid automorphism of714the vertices.715716INPUT::717718- ``g`` - permutation automorphism of the vertices719720- ``A`` - an optional permutation group in which the result belongs to721722- ``check`` -- (default ``True``) parameter forwarded to the constructor723of the permutation group724725EXAMPLES::726727sage: from admcycles import StableGraph728sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],729....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])730sage: Averts = G.vertex_automorphism_group()731sage: G.vertex_automorphism_lift(Averts(''))732()733sage: G.vertex_automorphism_lift(Averts('(0,3)(1,2)'))734(1,6)(2,5)(3,4)(7,8)(9,14)(10,13)(11,12)(15,16)735sage: G.vertex_automorphism_lift(Averts('(0,1,2,3)'))736(1,3,5,7)(2,4,6,8)(9,11,13,15)(10,12,14,16)737"""738p = sorted(sum(self._legs, []))739if A is None:740A = SymmetricGroup(p)741leg_pos = {j:i for i,j in enumerate(p)}742743G, _, edge_to_legs, partition = self._graph()744745# promote automorphisms of G as automorphisms of the stable graph746for u,v,lab in G.edges():747gu = g(u)748gv = g(v)749750if u < v:751start = edge_to_legs[u,v]752else:753start = [(t,s) for s,t in edge_to_legs[v,u]]754755if gu < gv:756end = edge_to_legs[gu,gv]757else:758end = [(t,s) for s,t in edge_to_legs[gv,gu]]759760for (lu, lv), (glu, glv) in zip(start, end):761p[leg_pos[lu]] = glu762p[leg_pos[lv]] = glv763764return A(p, check=check)765766def leg_automorphism_group_vertex_stabilizer(self, *args, **kwds):767r"""768Return the group of automorphisms of this stable graph stabilizing the vertices.769770All arguments are forwarded to the subgroup method of the Sage symmetric group.771772EXAMPLES::773774sage: from admcycles import StableGraph775sage: G = StableGraph([0],[[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])776sage: G.leg_automorphism_group_vertex_stabilizer()777Subgroup generated by [(1,2), (1,3)(2,4), (1,3,5,7)(2,4,6,8)] of (Symmetric group of order 8! as a permutation group)778779sage: G = StableGraph([1,1],[[1,2],[3,4]], [(1,3),(2,4)])780sage: G.leg_automorphism_group_vertex_stabilizer()781Subgroup generated by [(1,2)(3,4)] of (Symmetric group of order 4! as a permutation group)782"""783legs = sorted(sum(self._legs, []))784785G, _, edge_to_legs, partition = self._graph()786787S = SymmetricGroup(legs)788gens = []789for u,v,lab in G.edges():790if lab == 1 and u != v:791continue792793if u < v:794multiedge = edge_to_legs[u,v]795else:796multiedge = edge_to_legs[v,u]797798if lab >= 2:799(lu0, lv0) = multiedge[0]800(lu1, lv1) = multiedge[1]801gens.append( S.element_class([(lu0, lu1), (lv0, lv1)], S, check=False) )802if lab >= 3:803gens.append( S.element_class([ tuple(x for x,y in multiedge),804tuple(y for x,y in multiedge) ], S, check=False) )805if u == v:806(lu, lv) = multiedge[0]807gens.append( S.element_class([ (lu, lv) ], S, check=False) )808809return S.subgroup(gens, *args, **kwds)810811def leg_automorphism_group(self, *args, **kwds):812r"""813Return the action of the automorphism group on the legs.814815The arguments provided to this function are forwarded to the816constructor of the subgroup of the symmetric group.817818EXAMPLES::819820sage: from admcycles import StableGraph821822A triangle::823824sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],825....: [(2,3),(4,5),(6,1)])826sage: Alegs = G.leg_automorphism_group()827sage: assert Alegs.cardinality() == G.automorphism_number()828829A vertex with four loops::830831sage: G = StableGraph([0], [[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])832sage: Alegs = G.leg_automorphism_group()833sage: assert Alegs.cardinality() == G.automorphism_number()834sage: a = Alegs.random_element()835836Using extra arguments::837838sage: G = StableGraph([0,0,0], [[6,1,7,8],[2,3,9,10],[4,5,11,12]],839....: [(1,2), (3,4), (5,6), (7,8), (9,10), (11,12)])840sage: G.leg_automorphism_group()841Subgroup generated by [(11,12), (9,10), (7,8), (1,2)(3,6)(4,5)(7,9)(8,10), (1,6)(2,5)(3,4)(9,11)(10,12)] of (Symmetric group of order 12! as a permutation group)842sage: G.leg_automorphism_group(canonicalize=False)843Subgroup generated by [(1,6)(2,5)(3,4)(9,11)(10,12), (1,2)(3,6)(4,5)(7,9)(8,10), (11,12), (9,10), (7,8)] of (Symmetric group of order 12! as a permutation group)844"""845legs = sorted(sum(self._legs, []))846G, _, edge_to_legs, partition = self._graph()847# NOTE: this is too much generators. It is enough to move around the multiple848# edges in each orbit of the vertex automorphism group.849S = SymmetricGroup(legs)850gens = []851for g in self.vertex_automorphism_group().gens():852gens.append(self.vertex_automorphism_lift(g, S))853for g in self.leg_automorphism_group_vertex_stabilizer().gens():854gens.append(g)855856return S.subgroup(gens, *args, **kwds)857858def automorphism_number(self):859r"""860Return the size of the automorphism group (acting on legs).861862EXAMPLES::863864sage: from admcycles import StableGraph865sage: G = StableGraph([0, 2], [[1, 2, 4], [3, 5]], [(4, 5)])866sage: G.automorphism_number()8671868869sage: G = StableGraph([0, 0, 0], [[1,2,7,8], [3,4,9,10], [5,6,11,12]],870....: [(2,3),(4,5),(6,1),(8,9),(10,11),(12,7)])871sage: G.automorphism_number()87248873sage: G.leg_automorphism_group().cardinality()87448875876sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])877sage: G.automorphism_number()878Traceback (most recent call last):879...880NotImplementedError: automorphism_number not valid for repeated marking881"""882G, _, _, _ = self._graph()883aut = self.vertex_automorphism_group().cardinality()884885# edge automorphism886for i,j,lab in G.edges():887aut *= factorial(lab)888if i == j:889aut *= 2**lab890891# explicitly forbid marking multiplicity892markings = self.list_markings()893if len(markings) != len(set(markings)):894raise NotImplementedError("automorphism_number not valid for repeated marking")895896return aut897898def g(self):899r"""900Return the genus of this stable graph901902EXAMPLES::903904sage: from admcycles import StableGraph905sage: G = StableGraph([1,2],[[1,2], [3,4]], [(1,3),(2,4)])906sage: G.g()9074908"""909return sum(self._genera) + len(self._edges) - len(self._genera) + ZZ.one()910911def n(self):912r"""913Return the number of legs of the stable graph.914915EXAMPLES::916917sage: from admcycles import StableGraph918sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)]);G.n()9192920"""921return len(self.list_markings())922923def genera(self, i=None, copy=True):924"""925Return the list of genera of the stable graph.926927If an integer argument is given, return the genus at this vertex.928929EXAMPLES::930931sage: from admcycles import StableGraph932sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])933sage: G.genera()934[1, 2]935sage: G.genera(0), G.genera(1)936(1, 2)937"""938if i is None:939return self._genera[:] if copy else self._genera940else:941return self._genera[i]942943def edges(self, copy=True):944return self._edges[:] if copy else self._edges945946def num_verts(self):947return len(self._genera)948949def legs(self, v=None, copy=True):950if v is None:951return [l[:] for l in self._legs] if copy else self._legs952else:953return self._legs[v][:] if copy else self._legs[v]954955# TODO: deprecate956numvert = num_verts957958def num_edges(self):959r"""960Return the number of edges.961"""962return len(self._edges)963964def num_loops(self):965r"""966Return the number of loops (ie edges attached twice to a vertex).967"""968n = 0969for h0,h1 in self._edges:970n += self.vertex(h0) == self.vertex(h1)971return n972973def num_legs(self, i=None):974r"""975Return the number of legs.976"""977if i is None:978return sum(len(l) for l in self._legs)979else:980return len(self._legs[i])981982def _graph_(self):983r"""984Return the Sage graph object encoding the stable graph985986This inserts a vertex with label (i,j) in the middle of the edge (i,j).987Also inserts a vertex with label ('L',i) at the end of each leg l.988989EXAMPLES::990991sage: from admcycles import StableGraph992sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])993sage: SageGr = Gr._graph_()994sage: SageGr995Multi-graph on 5 vertices996sage: SageGr.vertices(sort=False) # random997[(1, 2), 0, 1, (3, 5), ('L', 7)]998"""999G = Graph(multiedges=True)1000for i, j in self._edges:1001G.add_vertex((i, j))1002G.add_edge((i, j), self.vertex(i))1003G.add_edge((i, j), self.vertex(j))1004for i in self.list_markings():1005G.add_edge(('L', i), self.vertex(i))1006return G10071008def dim(self, v=None):1009"""1010Return dimension of moduli space at vertex v.10111012If v=None, return dimension of entire stratum parametrized by graph.10131014EXAMPLES::10151016sage: from admcycles import StableGraph1017sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])1018sage: Gr.dim(0), Gr.dim(1), Gr.dim()1019(10, 13, 23)1020"""1021if v is None:1022return sum([self.dim(v) for v in range(len(self._genera))])1023return 3 * self._genera[v] - 3 + len(self._legs[v])10241025# TODO: deprecate and remove1026def invariant(self):1027r"""1028Return a graph-invariant in form of a tuple of integers or tuples10291030At the moment this assumes that we only compare stable graph with same total1031g and set of markings1032currently returns sorted list of (genus,degree) for vertices10331034IDEAS:10351036* number of self-loops for each vertex10371038EXAMPLES::10391040sage: from admcycles import StableGraph1041sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])1042sage: Gr.invariant()1043((3, 4, (7,)), (5, 1, ()))1044"""1045return tuple(sorted([(self._genera[v], len(self._legs[v]), tuple(sorted(self.list_markings(v)))) for v in range(len(self._genera))]))10461047def tidy_up(self):1048r"""1049Sort legs and edges.10501051EXAMPLES::10521053sage: from admcycles import StableGraph1054sage: S = StableGraph([1, 3, 2], [[1, 5, 4], [2, 3, 6], [9, 8, 7]], [(9,3), (1,2)], mutable=True)1055sage: S.tidy_up()1056sage: S1057[1, 3, 2] [[1, 4, 5], [2, 3, 6], [7, 8, 9]] [(1, 2), (3, 9)]1058"""1059if not self._mutable:1060raise ValueError("the graph is immutable; use a copy instead")1061for e in range(len(self._edges)):1062if self._edges[e][0] > self._edges[e][1]:1063self._edges[e] = (self._edges[e][1],self._edges[e][0])1064for legs in self._legs:1065legs.sort()1066self._edges.sort()1067self._maxleg = max([max(j+[0]) for j in self._legs])10681069def vertex(self, l):1070r"""1071Return vertex number of leg l1072"""1073for v in range(len(self._legs)):1074if l in self._legs[v]:1075return v1076return -1 #self does not have leg l10771078def list_markings(self, v=None):1079r"""1080Return the list of markings (non-edge legs) of self at vertex v.10811082EXAMPLES::10831084sage: from admcycles import StableGraph10851086sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1087sage: gam.list_markings(0)1088(3, 7)1089sage: gam.list_markings()1090(3, 7, 4)1091"""1092if v is None:1093return tuple([j for v in range(len(self._genera))1094for j in self.list_markings(v)])1095s = set(self._legs[v])1096for e in self._edges:1097s -= set(e)1098return tuple(s)10991100def leglist(self):1101r"""1102Return the list of legs11031104EXAMPLES::11051106sage: from admcycles import StableGraph11071108sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1109sage: gam.leglist()1110[1, 3, 7, 2, 4]1111"""1112return [j for leg in self._legs for j in leg]11131114def halfedges(self):1115r"""1116Return the tuple containing all half-edges, i.e. legs belonging to an edge11171118EXAMPLES::11191120sage: from admcycles import StableGraph11211122sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1123sage: gam.halfedges()1124(1, 2)1125"""1126return tuple(j for ed in self._edges for j in ed)11271128def edges_between(self, i, j):1129r"""1130Return the list [(l1,k1),(l2,k2), ...] of edges from i to j, where l1 in i, l2 in j; for i==j return each edge only once11311132EXAMPLES::11331134sage: from admcycles import StableGraph11351136sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1137sage: gam.edges_between(0, 1)1138[(1, 2)]1139sage: gam.edges_between(0, 0)1140[]1141"""1142if i == j:1143return [e for e in self._edges if (e[0] in self._legs[i] and e[1] in self._legs[j] ) ]1144else:1145return [e for e in self._edges if (e[0] in self._legs[i] and e[1] in self._legs[j] ) ]+ [(e[1],e[0]) for e in self._edges if (e[0] in self._legs[j] and e[1] in self._legs[i] ) ]11461147def forget_markings(self, markings):1148r"""1149Erase all legs in the list markings, do not check if this gives well-defined graph1150"""1151if not self._mutable:1152raise ValueError("the graph is not mutable; use a copy instead")1153for m in markings:1154self._legs[self.vertex(m)].remove(m)1155return self11561157def leginversion(self, l):1158r"""1159Returns l' if l and l' form an edge, otherwise returns l11601161EXAMPLES::11621163sage: from admcycles import StableGraph11641165sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1166sage: gam.leginversion(1)116721168"""1169for a, b in self._edges:1170if a == l:1171return b1172if b == l:1173return a1174return l11751176def stabilize(self):1177r"""1178Stabilize this stable graph.11791180(all vertices with genus 0 have at least three markings) and returns1181``(dicv,dicl,dich)`` where11821183- dicv a dictionary sending new vertex numbers to the corresponding old1184vertex numbers11851186- dicl a dictionary sending new marking names to the label of the last1187half-edge that they replaced during the stabilization this happens1188for instance if a g=0 vertex with marking m and half-edge l1189(belonging to edge (l,l')) is stabilized: l' is replaced by m, so1190dicl[m]=l'11911192- dich a dictionary sending half-edges that vanished in the1193stabilization process to the last half-edge that replaced them this1194happens if a g=0 vertex with two half-edges a,b (whose corresponding1195half-edges are c,d) is stabilized: we obtain an edge (c,d) and a ->1196d, b -> d we assume here that a stabilization exists (all connected1197components have 2g-2+n>0)11981199EXAMPLES::12001201sage: from admcycles import StableGraph12021203sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1204sage: gam.stabilize()1205({0: 0, 1: 1}, {}, {})12061207sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1208sage: gam.stabilize()1209Traceback (most recent call last):1210...1211ValueError: the graph is not mutable; use a copy instead12121213sage: g = StableGraph([0,0,0], [[1,5,6],[2,3],[4,7,8]], [(1,2),(3,4),(5,6),(7,8)], mutable=True)1214sage: g.stabilize()1215({0: 0, 1: 2}, {}, {2: 4, 3: 1})1216sage: h = StableGraph([0, 0, 0], [[1], [2,3], [4]], [(1,2), (3,4)], mutable=True)1217sage: h.stabilize()1218({0: 2}, {}, {})1219"""1220if not self._mutable:1221raise ValueError("the graph is not mutable; use a copy instead")1222markings = self.list_markings()1223stable = False1224verteximages = list(range(len(self._genera)))1225dicl = {}1226dich = {}1227while not stable:1228numvert = len(self._genera)1229count = 01230stable = True1231while count < numvert:1232if self._genera[count] == 0 and len(self._legs[count]) == 1:1233stable = False1234e0 = self._legs[count][0]1235e1 = self.leginversion(e0)1236v1 = self.vertex(e1)1237self._genera.pop(count)1238verteximages.pop(count)1239numvert -= 11240self._legs[v1].remove(e1)1241self._legs.pop(count)1242try:1243self._edges.remove((e0,e1))1244except:1245self._edges.remove((e1,e0))1246elif self._genera[count] == 0 and len(self._legs[count]) == 2:1247stable = False1248e0 = self._legs[count][0]1249e1 = self._legs[count][1]12501251if e1 in markings:1252swap = e01253e0 = e11254e1 = swap12551256e1prime = self.leginversion(e1)1257v1 = self.vertex(e1prime)1258self._genera.pop(count)1259verteximages.pop(count)1260numvert -= 112611262if e0 in markings:1263dicl[e0] = e1prime1264self._legs[v1].remove(e1prime)1265self._legs[v1].append(e0)1266self._legs.pop(count)1267try:1268self._edges.remove((e1,e1prime))1269except:1270self._edges.remove((e1prime,e1))1271else:1272e0prime = self.leginversion(e0)1273self._legs.pop(count)1274try:1275self._edges.remove((e0,e0prime))1276except:1277self._edges.remove((e0prime,e0))1278try:1279self._edges.remove((e1,e1prime))1280except:1281self._edges.remove((e1prime,e1))1282self._edges.append((e0prime,e1prime))12831284#update dich1285dich[e0] = e1prime1286dich[e1] = e0prime1287dich.update({h:e1prime for h in dich if dich[h]==e0})1288dich.update({h:e0prime for h in dich if dich[h]==e1})1289else:1290count += 11291return ({i:verteximages[i] for i in range(len(verteximages))},dicl,dich)12921293def degenerations(self, v=None, mutable=False):1294r"""1295Run through the list of all possible degenerations of this graph.12961297A degeneration happens by adding an edge at v or splitting it1298into two vertices connected by an edge the new edge always1299comes last in the list of edges.13001301If v is None, return all degenerations at all vertices.13021303EXAMPLES::13041305sage: from admcycles import StableGraph13061307sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1308sage: list(gam.degenerations(1))1309[[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)],1310[3, 0, 5] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],1311[3, 1, 4] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],1312[3, 1, 4] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],1313[3, 1, 4] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],1314[3, 1, 4] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],1315[3, 2, 3] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],1316[3, 2, 3] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],1317[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],1318[3, 2, 3] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)]]13191320All these graphs are immutable (or mutable when ``mutable=True``)::13211322sage: any(g.is_mutable() for g in gam.degenerations())1323False1324sage: all(g.is_mutable() for g in gam.degenerations(mutable=True))1325True13261327TESTS::13281329sage: from admcycles import StableGraph1330sage: G = StableGraph([2,2,2], [[1],[2,3],[4]], [(1,2),(3,4)])1331sage: sum(1 for v in range(3) for _ in G.degenerations(v))133281333sage: sum(1 for _ in G.degenerations())133481335"""1336if v is None:1337for v in range(self.num_verts()):1338for h in self.degenerations(v, mutable):1339yield h1340return13411342g = self._genera[v]1343l = len(self._legs[v])13441345# for positive genus: add loop to v and decrease genus1346if g > 0:1347G = self.copy()1348G.degenerate_nonsep(v)1349if not mutable:1350G.set_immutable()1351yield G13521353# now all graphs with separating edge : separate in (g1,M) and (g-g1,legs[v]-M), take note of symmetry and stability1354for g1 in range(floor(g/2)+1):1355for M in Subsets(set(self._legs[v])):1356if (g1 == 0 and len(M) < 2) or \1357(g == g1 and l-len(M) < 2) or \1358(2*g1 == g and l > 0 and (not self._legs[v][0] in M)):1359continue1360G = self.copy()1361G.degenerate_sep(v,g1,M)1362if not mutable:1363G.set_immutable()1364yield G13651366def newleg(self):1367r"""1368Create two new leg-indices that can be used to create an edge13691370This modifies ``self._maxleg``.13711372EXAMPLES::13731374sage: from admcycles import StableGraph13751376sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1377sage: gam.newleg()1378(8, 9)1379"""1380self._maxleg += 21381return (self._maxleg - 1, self._maxleg)13821383# TODO: this overlaps with relabel1384def rename_legs(self, di, shift=0):1385r"""1386Renames legs according to dictionary di, all other leg labels are shifted by shift (useful to keep half-edge labels separate from legs)1387TODO: no check that this renaming is legal, i.e. produces well-defined graph1388"""1389if not self._mutable:1390raise ValueError("the graph is not mutable; use a copy instead")1391for v in self._legs:1392for j in range(len(v)):1393v[j] = di.get(v[j],v[j]+shift) #replace v[j] by di[v[j]] if v[j] in di and leave at v[j] otherwise1394for e in range(len(self._edges)):1395self._edges[e] = (di.get(self._edges[e][0],self._edges[e][0]+shift) ,di.get(self._edges[e][1],self._edges[e][1]+shift))1396self.tidy_up()13971398def degenerate_sep(self, v, g1, M):1399r"""1400degenerate vertex v into two vertices with genera g1 and g(v)-g1 and legs M and complement14011402add new edge (e[0],e[1]) such that e[0] is in new vertex v, e[1] in last vertex, which is added14031404EXAMPLES::14051406sage: from admcycles import StableGraph14071408sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1409sage: G.degenerate_sep(1, 2, [4])1410sage: G1411[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)]14121413sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1414sage: G.degenerate_sep(1, 2, [4])1415Traceback (most recent call last):1416...1417ValueError: the graph is not mutable; use a copy instead1418"""1419if not self._mutable:1420raise ValueError("the graph is not mutable; use a copy instead")1421g = self._genera[v]1422oldleg = self._legs[v]1423e = self.newleg()14241425self._genera[v] = g11426self._genera += [g-g1]1427self._legs[v] = list(M)+[e[0]]1428self._legs += [list(set(oldleg)-set(M))+[e[1]]]1429self._edges += [e]14301431def degenerate_nonsep(self, v):1432"""1433EXAMPLES::14341435sage: from admcycles import StableGraph14361437sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1438sage: G.degenerate_nonsep(1)1439sage: G1440[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)]14411442sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1443sage: G.degenerate_nonsep(1)1444Traceback (most recent call last):1445...1446ValueError: the graph is not mutable; use a copy instead1447"""1448if not self._mutable:1449raise ValueError("the graph is not mutable; use a copy instead")1450e = self.newleg()1451self._genera[v] -= 11452self._legs[v] += [e[0], e[1]]1453self._edges += [e]14541455def contract_edge(self, e, adddata=False):1456r"""1457Contracts the edge e=(e0,e1).14581459This method returns nothing by default. But if ``adddata`` is set to1460``True``, then it returns a tuple ``(av, edgegraph, vnum)`` where14611462- ``av`` is the the vertex in the modified graph on which previously1463the edge ``e`` had been attached14641465- ``edgegraph`` is a stable graph induced in self by the edge e1466(1-edge graph, all legs at the ends of this edge are considered as1467markings)14681469- ``vnum`` is a list of one or two vertices e was attached before the1470contraction (in the order in which they appear)14711472- ``diccv`` is a dictionary mapping old vertex numbers to new vertex1473numbers14741475EXAMPLES::14761477sage: from admcycles import StableGraph14781479sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1480sage: G.contract_edge((1,2))1481sage: G1482[8] [[3, 7, 4]] [(3, 7)]1483sage: G.contract_edge((3,7))1484sage: G1485[9] [[4]] []14861487sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1488sage: G2.contract_edge((3,7))1489sage: G2.contract_edge((1,2))1490sage: G == G21491True14921493sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1494sage: G2.contract_edge((3,7), adddata=True)1495(0, [3] [[1, 3, 7]] [(3, 7)], [0], {0: 0, 1: 1})14961497sage: G = StableGraph([1,1,1,1],[[1],[2,4],[3,5],[6]],[(1,2),(3,4),(5,6)],mutable=True)1498sage: G.contract_edge((3,4),True)1499(1, [1, 1] [[2, 4], [3, 5]] [(3, 4)], [1, 2], {0: 0, 1: 1, 2: 1, 3: 2})15001501sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1502sage: G.contract_edge((1,2))1503Traceback (most recent call last):1504...1505ValueError: the graph is not mutable; use a copy instead1506"""1507if not self._mutable:1508raise ValueError("the graph is not mutable; use a copy instead")15091510v0 = self.vertex(e[0])1511v1 = self.vertex(e[1])15121513if v0 == v1: # contracting a loop1514if adddata:1515H = StableGraph([self._genera[v0]], [self._legs[v0][:]], [e])1516self._genera[v0] += 11517self._legs[v0].remove(e[0])1518self._legs[v0].remove(e[1])1519self._edges.remove(e)15201521if adddata:1522return (v0, H, [v0], {v:v for v in range(len(self._genera))})15231524else: # contracting an edge between different vertices1525if v0 > v1:1526v0, v1 = v1, v015271528if adddata:1529diccv = {v:v for v in range(v1)}1530diccv[v1] = v01531diccv.update({v:v-1 for v in range(v1+1, len(self._genera))})1532H = StableGraph([self._genera[v0],self._genera[v1]], [self._legs[v0][:], self._legs[v1][:]], [e])15331534g1 = self._genera.pop(v1)1535self._genera[v0] += g11536l1 = self._legs.pop(v1)1537self._legs[v0]+=l11538self._legs[v0].remove(e[0])1539self._legs[v0].remove(e[1])1540self._edges.remove(e)15411542if adddata:1543return (v0, H, [v0,v1], diccv)15441545def glue_vertex(self, i, Gr, divGr={}, divs={}, dil={}):1546r"""1547Glues the stable graph ``Gr`` at the vertex ``i`` of this stable graph15481549optional arguments: if divGr/dil are given they are supposed to be a1550dictionary, which will be cleared and updated with the1551renaming-convention to pass from leg/vertex-names in Gr to1552leg/vertex-names in the glued graph similarly, divs will be a1553dictionary assigning vertex numbers in the old self the corresponding1554number in the new self necessary condition:15551556- every leg of i is also a leg in Gr1557- every leg of Gr that is not a leg of i in self gets a new name1558"""1559if not self._mutable:1560raise ValueError("the graph is not mutable; use a copy instead")15611562divGr.clear()1563divs.clear()1564dil.clear()15651566# remove vertex i, legs corresponding to it and all self-edges1567selfedges_old = self.edges_between(i,i)1568self._genera.pop(i)1569legs_old = self._legs.pop(i)1570for e in selfedges_old:1571self._edges.remove(e)15721573# when gluing in Gr, make sure that new legs corresponding to edges inside Gr1574# or legs in Gr which are already attached to a vertex different from i in self get a new unique label in the glued graph1575m = max(self._maxleg, Gr._maxleg) #largest index used in either graph, new labels m+1, m+2, ...15761577Gr_new = Gr.copy()15781579a = []1580for l in Gr._legs:1581a += l1582a = set(a) # legs of Gr15831584b = set(legs_old) # legs of self at vertex i15851586# c=[]1587# for e in Gr._edges:1588# c+=[e[0],e[1]]1589# c=set(c) # legs of Gr that are in edge within Gr15901591# set of legs of Gr that need to be relabeled: all legs of Gr that are not attached to vertex i in self1592e = a - b15931594for l in e:1595m += 11596dil[l] = m15971598Gr_new.rename_legs(dil)15991600# vertex dictionaries1601for j in range(i):1602divs[j] = j1603for j in range(i+1, len(self._genera)+1):1604divs[j] = j-11605for j in range(len(Gr._genera)):1606divGr[j] = len(self._genera)+j16071608self._genera += Gr_new._genera1609self._legs += Gr_new._legs1610self._edges += Gr_new._edges16111612self.tidy_up()16131614# TODO: overlaps with relabel1615def reorder_vertices(self, vord):1616r"""1617Reorders vertices according to tuple given (permutation of range(len(self._genera)))1618"""1619if not self._mutable:1620raise ValueError("the graph is not mutable; use a copy instead")1621new_genera=[self._genera[j] for j in vord]1622new_legs=[self._legs[j] for j in vord]1623self._genera=new_genera1624self._legs=new_legs16251626def extract_subgraph(self, vertices, outgoing_legs=None, rename=True, mutable=False):1627r"""1628Extracts from self a subgraph induced by the list vertices16291630if the list outgoing_legs is given, the markings of the subgraph are1631called 1,2,..,m corresponding to the elements of outgoing_legs in this1632case, all edges involving outgoing edges should be cut returns a triple1633(Gamma,dicv,dicl), where16341635- Gamma is the induced subgraph16361637- dicv, dicl are (surjective) dictionaries associating vertex/leg1638labels in self to the vertex/leg labels in Gamma16391640if ``rename=False``, do not rename any legs when extracting16411642EXAMPLES::16431644sage: from admcycles import StableGraph16451646sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1647sage: gam.extract_subgraph([0])1648([3] [[1, 2, 3]] [], {0: 0}, {1: 1, 3: 2, 7: 3})1649"""1650attachedlegs = set(l for v in vertices for l in self._legs[v])1651if outgoing_legs is None:1652alllegs = attachedlegs.copy()1653for (e0,e1) in self._edges:1654if e0 in alllegs and e1 in alllegs:1655alllegs.remove(e0)1656alllegs.remove(e1)1657outgoing_legs=list(alllegs)16581659shift = len(outgoing_legs) + 11660if rename:1661dicl={outgoing_legs[i]:i+1 for i in range(shift-1)}1662else:1663dicl={l:l for l in self.leglist()}16641665genera = [self._genera[v] for v in vertices]1666legs = [[dicl.setdefault(l,l+shift) for l in self._legs[v]] for v in vertices]1667edges = [(dicl[e0],dicl[e1]) for (e0,e1) in self._edges if (e0 in attachedlegs and e1 in attachedlegs and (e0 not in outgoing_legs) and (e1 not in outgoing_legs))]1668dicv={vertices[i]:i for i in range(len(vertices))}16691670return (StableGraph(genera, legs, edges, mutable=mutable), dicv, dicl)16711672def boundary_pushforward(self, classes=None):1673r"""1674Computes the pushforward of a product of tautological classes (one for each vertex) under the1675boundary gluing map for this stable graph.16761677INPUT:16781679- ``classes`` -- list (default: `None`); list of tautclasses, one for each vertex of the stable1680graph. The genus of the ith class is assumed to be the genus of the ith vertex, the markings1681of the ith class are supposed to be 1, ..., ni where ni is the number of legs at the ith vertex.1682Note: the jth leg at vertex i corresponds to the marking j of the ith class.1683For classes=None, place the fundamental class at each vertex.16841685EXAMPLES::16861687sage: from admcycles import StableGraph, psiclass, fundclass1688sage: B=StableGraph([2,1],[[4,1,2],[3,5]],[(4,5)])1689sage: Bclass=B.boundary_pushforward() # class of undecorated boundary divisor1690sage: Bclass*Bclass1691Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]1692Polynomial : (-1)*psi_5^11693<BLANKLINE>1694Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]1695Polynomial : (-1)*psi_4^11696sage: si1=B.boundary_pushforward([fundclass(2,3),-psiclass(2,1,2)]); si11697Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]1698Polynomial : (-1)*psi_5^11699sage: si2=B.boundary_pushforward([-psiclass(1,2,3),fundclass(1,2)]); si21700Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]1701Polynomial : (-1)*psi_4^11702sage: (Bclass*Bclass-si1-si2).simplify()1703<BLANKLINE>17041705"""1706from .admcycles import prodtautclass1707return prodtautclass(self.copy(mutable=False), protaut=classes).pushforward()17081709def boundary_pullback(self,other):1710r"""1711pulls back the tautclass/decstratum other to self and returns a prodtautclass with gamma=self1712"""1713from .admcycles import tautclass, decstratum17141715if isinstance(other,tautclass):1716from .admcycles import prodtautclass1717result = prodtautclass(self.copy(mutable=False), [])1718for t in other.terms:1719result+=self.boundary_pullback(t)1720return result1721if isinstance(other,decstratum):1722# NOTE: this is using much more than just stable graphs1723# I would suggest to move it out of this class1724from .admcycles import (common_degenerations, prodtautclass,1725onekppoly, kppoly, kappacl, psicl)1726commdeg = common_degenerations(self, other.gamma, modiso=True)1727result = prodtautclass(self.copy(mutable=False), [])17281729for (Gamma,dicv1,dicl1,dicv2,dicl2) in commdeg:1730numvert = len(Gamma._genera)1731# first determine edges that are covered by self and other - for excess int. terms1732legcount = {l:0 for l in Gamma.leglist()}1733for l in dicl1.values():1734legcount[l] += 11735for l in dicl2.values():1736legcount[l] += 117371738excesspoly = onekppoly(numvert)1739for e in Gamma._edges:1740if legcount[e[0]] == 2:1741excesspoly*=( (-1)*(psicl(e[0],numvert) + psicl(e[1],numvert)) )17421743# partition vertices of Gamma according to where they go in self1744v1preim=[[] for v in range(len(self._genera))]1745for w in dicv1:1746v1preim[dicv1[w]].append(w)17471748graphpartition = [Gamma.extract_subgraph(v1preim[v],outgoing_legs=[dicl1[l] for l in self._legs[v]]) for v in range(len(self._genera))]17491750v2preim = [[] for v in range(len(other.gamma._genera))]1751for w in dicv2:1752v2preim[dicv2[w]].append(w)17531754resultpoly = kppoly([],[])1755# now the stage is set to go through the polynomial of other term by term, pull them back according to dicv2, dicl2 and also add the excess-intersection terms1756for (kappa,psi,coeff) in other.poly:1757# psi-classes are transferred by dicl2, but kappa-classes might be split up if dicv2 has multiple preimages of same vertex1758# multiply everything together in a kppoly on Gamma, then split up this polynomial according to graphpartition1759psipolydict = {dicl2[l]: psi[l] for l in psi}1760psipoly = kppoly([([[] for i in range(numvert)],psipolydict)],[1])17611762kappapoly = prod([prod([sum([kappacl(w,k+1,numvert) for w in v2preim[v] ])**kappa[v][k] for k in range(len(kappa[v]))]) for v in range(len(other.gamma._genera))])176317641765resultpoly += coeff*psipoly*kappapoly17661767resultpoly *= excesspoly1768#TODO: filter for terms that vanish by dimension reasons?17691770# now fiddle the terms of resultpoly apart and distribute them to graphpartition1771for (kappa,psi,coeff) in resultpoly:1772decstratlist = []1773for v in range(len(self._genera)):1774kappav = [kappa[w] for w in v1preim[v]]1775psiv = {graphpartition[v][2][l] : psi[l] for l in graphpartition[v][2] if l in psi}1776decstratlist.append(decstratum(graphpartition[v][0],kappa=kappav,psi=psiv))1777tempresu = prodtautclass(self,[decstratlist])1778tempresu *= coeff1779result += tempresu1780return result17811782def to_tautclass(self):1783r"""1784Returns pure boundary stratum associated to self; note: does not divide by automorphisms!1785"""1786from .admcycles import tautclass, decstratum1787return tautclass([decstratum(self)])17881789def _vertex_module(self):1790nv = len(self._genera)1791return FreeModule(ZZ, nv)17921793def _edge_module(self):1794ne = len(self._edges)1795return FreeModule(ZZ, ne)17961797# TODO: implement cache as this is used intensively in the flow code below1798# See https://gitlab.com/jo314schmitt/admcycles/issues/141799def spanning_tree(self, root=0):1800r"""1801Return a spanning tree.18021803INPUT:18041805- ``root`` - optional vertex for the root of the spanning tree18061807OUTPUT: a triple18081809- list of triples ``(vertex ancestor, sign, edge index)`` where the1810triple at position ``i`` correspond to vertex ``i``.18111812- the set of indices of extra edges not in the tree18131814- vertices sorted according to their heights in the tree (first element is the root1815and the end of the list are the leaves). In other words, a topological sort of1816the vertices with respect to the spanning tree.18171818EXAMPLES::18191820sage: from admcycles import StableGraph1821sage: G = StableGraph([0,0,0], [[1,2],[3,4,5],[6,7]], [(1,3),(4,6),(5,7)])1822sage: G.spanning_tree()1823([None, (0, -1, 0), (1, -1, 1)], {2}, [0, 1, 2])1824sage: G.spanning_tree(1)1825([(1, 1, 0), None, (1, -1, 1)], {2}, [1, 0, 2])1826sage: G.spanning_tree(2)1827([(1, 1, 0), (2, 1, 1), None], {2}, [2, 1, 0])1828"""1829from collections import deque18301831nv = len(self._genera)1832ne = len(self._edges)18331834out_edges = [[] for _ in range(nv)]1835for i,(lu,lv) in enumerate(self._edges):1836u = self.vertex(lu)1837v = self.vertex(lv)1838out_edges[u].append((v, -1, i))1839out_edges[v].append((u, 1, i))18401841ancestors = [None] * nv1842ancestors[root] = None1843unseen = [True] * nv1844unseen[root] = False1845todo = deque()1846todo.append(root)1847extra_edges = set(range(ne))1848vertices = [root]18491850while todo:1851u = todo.popleft()1852for v,s,i in out_edges[u]:1853if unseen[v]:1854ancestors[v] = (u,s,i)1855todo.append(v)1856unseen[v] = False1857extra_edges.remove(i)1858vertices.append(v)18591860return ancestors, extra_edges, vertices18611862def cycle_basis(self):1863r"""1864Return a basis of the cycles as vectors of length the number of edges in the graph.18651866The coefficient at a given index in a vector corresponds to the edge1867of this index in the list of edges of the stable graph. The coefficient is1868+1 or -1 depending on the orientation of the edge in the cycle.18691870EXAMPLES::18711872sage: from admcycles import StableGraph1873sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])1874sage: G.cycle_basis()1875[(1, 1, 0, 0), (1, 0, -1, 1)]1876"""1877ne = len(self._edges)1878V = self._edge_module()18791880ancestors, extra_edges, _ = self.spanning_tree()1881basis = []18821883for i in extra_edges:1884vec = [0] * ne18851886# contribution of the edge1887vec[i] = 118881889lu,lv = self._edges[i]1890u = self.vertex(lu)1891v = self.vertex(lv)18921893# contribution of the path from root to u1894while ancestors[u] is not None:1895u, s, i = ancestors[u]1896vec[i] -= s18971898# contribution of the path from v to root1899while ancestors[v] is not None:1900v, s, i = ancestors[v]1901vec[i] += s19021903basis.append(V(vec))19041905return basis19061907def cycle_space(self):1908r"""1909Return the subspace of the edge module generated by the cycles.19101911EXAMPLES::19121913sage: from admcycles import StableGraph1914sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])1915sage: C = G.cycle_space()1916sage: C1917Free module of degree 4 and rank 2 over Integer Ring1918Echelon basis matrix:1919[ 1 0 -1 1]1920[ 0 1 1 -1]1921sage: G.flow_divergence(C.random_element()).is_zero()1922True1923"""1924return self._edge_module().submodule(self.cycle_basis())19251926def flow_divergence(self, flow):1927r"""1928Return the divergence of the given ``flow``.19291930The divergence at a given vertex is the sum of the flow on the outgoing1931edges at this vertex.19321933EXAMPLES::19341935sage: from admcycles import StableGraph1936sage: G = StableGraph([1,0,0], [[1,2],[3,4,5,6],[7,8]], [(1,3),(4,2),(5,8),(7,6)])1937sage: G.flow_divergence([1,1,1,1])1938(0, 0, 0)1939sage: G.flow_divergence([1,0,0,0])1940(1, -1, 0)1941"""1942flow = self._edge_module()(flow)1943deriv = self._vertex_module()()1944for i, (lu, lv) in enumerate(self._edges):1945u = self.vertex(lu)1946v = self.vertex(lv)1947deriv[u] += flow[i]1948deriv[v] -= flow[i]19491950return deriv19511952def flow_solve(self, vertex_weights):1953r"""1954Return a solution for the flow equation with given vertex weights.19551956EXAMPLES::19571958sage: from admcycles import StableGraph19591960sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])1961sage: flow = G.flow_solve((-3, 2, 1))1962sage: flow1963(-2, 0, -1, 0)1964sage: G.flow_divergence(flow)1965(-3, 2, 1)1966sage: div = vector((-34, 27, 7))1967sage: flow = G.flow_solve(div)1968sage: G.flow_divergence(flow) == div1969True1970sage: C = G.cycle_space()1971sage: G.flow_divergence(flow + C.random_element()) == div1972True19731974sage: G = StableGraph([0,0,0], [[1],[2,3],[4]], [(1,2),(3,4)])1975sage: G.flow_solve((-1, 0, 1))1976(-1, -1)1977sage: G.flow_divergence((-1, -1))1978(-1, 0, 1)1979sage: G.flow_solve((-1, 3, -2))1980(-1, 2)1981sage: G.flow_divergence((-1, 2))1982(-1, 3, -2)19831984TESTS::19851986sage: V = ZZ**41987sage: vectors = [V((-1, 0, 0, 1)), V((-3, 1, -2, 4)), V((5, 2, -13, 6))]1988sage: G1 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (5,6)])1989sage: G2 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (6,5)])1990sage: G3 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(2,1), (3,4), (6,5)])1991sage: G4 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (4,3), (5,6)])1992sage: for G in [G1,G2,G3,G4]:1993....: for v in vectors:1994....: flow = G.flow_solve(v)1995....: assert G.flow_divergence(flow) == v, (v,flow,G)19961997sage: V = ZZ**61998sage: G = StableGraph([0,0,0,0,0,0], [[1,5], [2,3], [4,6,7,10], [8,9,11,13], [14,16], [12,15]], [(1,2),(4,3),(5,6),(7,8),(9,10),(12,11),(13,14),(15,16)])1999sage: for _ in range(10):2000....: v0 = V.random_element()2001....: for u in V.basis():2002....: v = v0 - sum(v0) * u2003....: flow = G.flow_solve(v)2004....: assert G.flow_divergence(flow) == v, (v, flow)2005"""2006# NOTE: if we compute the divergence matrix, one can also use solve_right2007# directly. It might be faster on some large instances.2008nv = len(self._genera)2009if len(vertex_weights) != nv or sum(vertex_weights) != 0:2010raise ValueError("vertex_weights must have length nv and sum up to zero")20112012ancestors, _, vertices = self.spanning_tree()2013vertices.reverse() # move leaves first and root last2014vertices.pop() # remove the root20152016new_vertex_weights = self._vertex_module()(vertex_weights)2017if new_vertex_weights is vertex_weights and vertex_weights.is_mutable():2018new_vertex_weights = vertex_weights.__copy__()2019vertex_weights = new_vertex_weights20202021V = self._edge_module()2022vec = V()2023for u in vertices:2024v, s, i = ancestors[u]2025vec[i] += s * vertex_weights[u]2026vertex_weights[v] += vertex_weights[u]20272028return vec20292030def plot(self, vord=None, vpos=None, eheight=None):2031r"""2032Return a graphics object in which ``self`` is plotted.20332034If ``vord`` is ``None``, the method uses the default vertex order.20352036If ``vord`` is ``[]``, the parameter ``vord`` is used to give back the vertex order.20372038EXAMPLES::20392040sage: from admcycles import *2041sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])2042sage: G.plot()2043Graphics object consisting of 12 graphics primitives20442045TESTS::20462047sage: from admcycles import *2048sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])2049sage: vertex_order = []2050sage: P = G.plot(vord=vertex_order)2051sage: vertex_order2052[0, 1]2053"""2054# some parameters2055mark_dx = 1 # x-distance of different markings2056mark_dy = 1 # y-distance of markings from vertices2057mark_rad = 0.2 # radius of marking-circles2058v_dxmin = 1 # minimal x-distance of two vertices2059ed_dy = 0.7 # max y-distance of different edges between same vertex-pair20602061vsize = 0.4 # (half the) size of boxes representing vertices20622063if not vord:2064default_vord = list(range(len(self._genera)))2065if vord is None:2066vord = default_vord2067else:2068vord += default_vord2069reord_self = self.copy()2070reord_self.reorder_vertices(vord)20712072if not vpos:2073default_vpos = [(0, 0)]2074for i in range(1, len(self._genera)):2075default_vpos+=[(default_vpos[i-1][0]+mark_dx*(len(reord_self.list_markings(i-1))+2 *len(reord_self.edges_between(i-1 ,i-1 ))+len(reord_self.list_markings(i))+2 *len(reord_self.edges_between(i,i)))/ZZ(2)+v_dxmin,0)]2076if vpos is None:2077vpos = default_vpos2078else:2079vpos += default_vpos20802081if not eheight:2082ned={(i,j): 0 for i in range(len(reord_self._genera)) for j in range(i+1 ,len(reord_self._genera))}2083default_eheight = {}2084for e in reord_self._edges:2085ver1=reord_self.vertex(e[0])2086ver2=reord_self.vertex(e[1])2087if ver1!=ver2:2088default_eheight[e]=abs(ver1-ver2)-1+ned[(min(ver1,ver2), max(ver1,ver2))]*ed_dy2089ned[(min(ver1,ver2), max(ver1,ver2))]+=120902091if eheight is None:2092eheight = default_eheight2093else:2094eheight.update(default_eheight)20952096# now the drawing starts2097# vertices2098vertex_graph = [polygon2d([[vpos[i][0]-vsize,vpos[i][1]-vsize],[vpos[i][0]+vsize,vpos[i][1]-vsize],[vpos[i][0]+vsize,vpos[i][1]+vsize],[vpos[i][0]-vsize,vpos[i][1]+vsize]], color='white',fill=True, edgecolor='black', thickness=1,zorder=2) + text('g='+repr(reord_self._genera[i]), vpos[i], fontsize=20 , color='black',zorder=3) for i in range(len(reord_self._genera))]20992100# non-self edges2101edge_graph=[]2102for e in reord_self._edges:2103ver1=reord_self.vertex(e[0])2104ver2=reord_self.vertex(e[1])2105if ver1!=ver2:2106x=(vpos[ver1][0]+vpos[ver2][0])/ZZ(2)2107y=(vpos[ver1][1]+vpos[ver2][1])/ZZ(2)+eheight[e]2108edge_graph+=[bezier_path([[vpos[ver1], (x,y) ,vpos[ver2] ]],color='black',zorder=1)]21092110marking_graph=[]21112112for v in range(len(reord_self._genera)):2113se_list=reord_self.edges_between(v,v)2114m_list=reord_self.list_markings(v)2115v_x0=vpos[v][0]-(2*len(se_list)+len(m_list)-1)*mark_dx/ZZ(2)21162117for e in se_list:2118edge_graph+=[bezier_path([[vpos[v], (v_x0,-mark_dy), (v_x0+mark_dx,-mark_dy) ,vpos[v] ]],zorder=1)]2119v_x0+=2*mark_dx2120for l in m_list:2121marking_graph+=[line([vpos[v],(v_x0,-mark_dy)],color='black',zorder=1)+circle((v_x0,-mark_dy),mark_rad, fill=True, facecolor='white', edgecolor='black',zorder=2)+text(repr(l),(v_x0,-mark_dy), fontsize=10, color='black',zorder=3)]2122v_x0+=mark_dx2123G = sum(marking_graph) + sum(edge_graph) + sum(vertex_graph)2124G.axes(False)2125return G21262127def _unicode_art_(self):2128"""2129Return unicode art for the stable graph ``self``.21302131EXAMPLES::21322133sage: from admcycles import *2134sage: A = StableGraph([1, 2],[[1, 2, 3], [4]],[(3, 4)])2135sage: unicode_art(A)2136╭────╮21373 42138╭┴─╮ ╭┴╮2139│1 │ │2│2140╰┬┬╯ ╰─╯21411221422143sage: A = StableGraph([333, 4444],[[1, 2, 3], [4]],[(3, 4)])2144sage: unicode_art(A)2145╭─────╮21463 42147╭┴──╮ ╭┴───╮2148│333│ │4444│2149╰┬┬─╯ ╰────╯21501221512152sage: B = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])2153sage: unicode_art(B)2154╭─────╮2155│╭╮ │2156135 22157╭┴┴┴╮ ╭┴╮2158│3 │ │5│2159╰───╯ ╰─╯21602161sage: C = StableGraph([3,5], [[3,1,5],[2,4]], [(1,2),(3,5)])2162sage: unicode_art(C)2163╭────╮2164╭─╮ │2165315 22166╭┴┴┴╮ ╭┴╮2167│3 │ │5│2168╰───╯ ╰┬╯216942170"""2171from sage.typeset.unicode_art import UnicodeArt2172N = self.num_verts()2173all_half_edges = self.halfedges()2174half_edges = {i: [j for j in legs_i if j in all_half_edges]2175for i, legs_i in zip(range(N), self._legs)}2176open_edges = {i: [j for j in legs_i if j not in all_half_edges]2177for i, legs_i in zip(range(N), self._legs)}2178left_box = [u' ', u'╭', u'│', u'╰', u' ']2179right_box = [u' ', u'╮ ', u'│ ', u'╯ ', u' ']2180positions = {}2181boxes = UnicodeArt()2182total_width = 02183for vertex in range(N):2184t = list(left_box)2185for v in half_edges[vertex]:2186w = str(v)2187positions[v] = total_width + len(t[0])2188t[0] += w2189t[1] += u'┴' + u'─' * (len(w) - 1)2190for v in open_edges[vertex]:2191w = str(v)2192t[4] += w2193t[3] += u'┬' + u'─' * (len(w) - 1)2194t[2] += str(self._genera[vertex])2195length = max(len(line) for line in t)2196for i in [0, 2, 4]:2197t[i] += u' ' * (length - len(t[i]))2198for i in [1, 3]:2199t[i] += u'─' * (length - len(t[i]))2200for i in range(5):2201t[i] += right_box[i]2202total_width += length + 22203boxes += UnicodeArt(t)2204num_edges = self.num_edges()2205matchings = [[u' '] * (1 + max(positions.values()))2206for _ in range(num_edges)]2207for i, (a, b) in enumerate(self.edges()):2208xa = positions[a]2209xb = positions[b]2210if xa > xb:2211xa, xb = xb, xa2212for j in range(xa + 1, xb):2213matchings[i][j] = u'─'2214matchings[i][xa] = u'╭'2215matchings[i][xb] = u'╮'2216for j in range(i + 1, num_edges):2217matchings[j][xa] = u'│'2218matchings[j][xb] = u'│'2219matchings = [u''.join(line) for line in matchings]2220return UnicodeArt(matchings + list(boxes))222122222223# Thes function is about to be deprecated. Instead use2224# - StableGraph.is_isomorphic2225# - StableGraph.automorphism_number22262227from sage.combinat.permutation import Permutations2228import itertools22292230#computes union of dictionaries2231def dicunion(*dicts):2232return dict(itertools.chain.from_iterable(dct.items() for dct in dicts))22332234def GraphIsom(G,H,check=False):2235#TODO: Insert quick hash-check if G,H can be isomorphic at all2236if (G.invariant() != H.invariant()):2237return []22382239Isomlist=[] # list of isomorphisms that will be returned2240IsoV={} # working vertex-dictionary2241IsoL={j:j for j in G.list_markings()} # working leg-dictionary22422243for j in G.list_markings():2244vG=G.vertex(j)2245vH=H.vertex(j)22462247# if vertices containing j have different genera or degrees in G, there is no automorphism2248# also if the markings give contradictory information where the vertices go, there is no automorphism2249if (G._genera[vG] != H._genera[vH]) or (len(G._legs[vG]) != len(H._legs[vH])) or (vG in IsoV and IsoV[vG] != vH):2250return []2251# otherwise add known association of vertices to IsoV2252IsoV[vG]=vH2253# Now vG and vH contain all information prescribed by markings, proceed to assign markingless vertices2254# Create dictionaries gdG, gdH associating to tuples (genus, degree) the indices of markingless vertices in G,H with those data2255gdG={}2256gdH={}225722582259for v in range(len(G._genera)):2260if v not in IsoV:2261gd=(G._genera[v],len(G._legs[v]))2262if gd in gdG:2263gdG[gd]+=[v]2264else:2265gdG[gd]=[v]2266for v in range(len(H._genera)):2267if not v in IsoV.values():2268gd=(H._genera[v],len(H._legs[v]))2269if gd in gdH:2270gdH[gd]+=[v]2271else:2272gdH[gd]=[v]227322742275if set(gdG) != set(gdH):2276return []22772278VertIm=[] # list (for all keys (g,d) of gdG) of lists of all possible dictionary-bijections from gdG(gd) to gdH(gd)227922802281for gd in gdG:2282if len(gdG[gd]) != len(gdH[gd]):2283return []2284P=Permutations(len(gdG[gd]))2285ind=list(range(len(gdG[gd])))2286VertIm+=[ [ {gdG[gd][i]:gdH[gd][p[i]-1 ] for i in ind} for p in P] ]22872288# Now VertIm is a list of the form [[{0:0, 1:1},{0:1, 1:0}], [{2:2}]]2289# Iterate over all possible combinations2290for VI in itertools.product(*VertIm):2291continueloop=False2292IsoV2=dicunion(IsoV,*VI) #IsoV2 is now complete dictionary of vertices22932294LegIm=[] # list (for all (ordered) pairs of vertices of G) of lists of dictionaries associating legs of edges connecting the pair to legs in H connecting the image pair22952296#TODO: possibly want quick check that now graphs are actually isomorphic22972298# first look at loops of G2299for v in range(len(G._genera)):2300EdG=G.edges_between(v,v)23012302EdH=H.edges_between(IsoV2[v],IsoV2[v])2303if len(EdG) != len(EdH):2304continueloop=True2305break23062307LegImvv=[]23082309# P gives the ways to biject from EdG to EdH, but additionally for each edge we have to decide to which of the two legs of the target edge it goes2310P=Permutations(len(EdG))2311ran=list(range(len(EdG)))2312choic=len(EdG)*[[0 ,1 ]]2313for p in P:2314for o in itertools.product(*choic):2315#Example: EdG=[(1,2),(3,4)], EdH=[(6,7),(9,11)], p=[2,1], o=[0,1] -> {1:9, 2:11, 3:7, 4:6}2316di=dicunion({EdG[i][0 ] : EdH[p[i]-1 ][o[i]] for i in ran},{EdG[i][1 ] : EdH[p[i]-1 ][1 -o[i]] for i in ran})2317LegImvv+=[di]2318LegIm+=[LegImvv]2319if continueloop:2320continue23212322# now look at edges from v to w2323for v in range(len(G._genera)):2324if continueloop:2325break2326for w in range(v+1 ,len(G._genera)):2327EdG=G.edges_between(v,w)2328EdH=H.edges_between(IsoV2[v],IsoV2[w])2329if len(EdG) != len(EdH):2330continueloop=True2331break233223332334LegImvw=[]23352336# P gives the ways to biject from EdG to EdH2337P=Permutations(len(EdG))2338ran=list(range(len(EdG)))2339for p in P:2340#Example: EdG=[(1,2),(3,4)], EdH=[(6,7),(9,11)], p=[2,1] -> {1:9, 2:11, 3:6, 4:7}2341di=dicunion({EdG[i][0 ] : EdH[p[i]-1 ][0 ] for i in ran},{EdG[i][1 ] : EdH[p[i]-1 ][1 ] for i in ran})2342LegImvw+=[di]2343LegIm+=[LegImvw]23442345if continueloop:2346continue23472348# if program runs to here, then IsoV2 is a bijection of vertices respecting the markings,2349# LegIm is a list of lists of dictionaries giving bijections of non-marking legs that respect2350# the map of vertices and edges; it remains to add the various elements to IsoL and return them2351Isomlist += [[IsoV2, dicunion(IsoL,*LegDics)] for LegDics in itertools.product(*LegIm)]23522353if check and Isomlist:2354return Isomlist23552356return Isomlist235723582359