unlisted
ubuntu2004# -*- coding: utf-8 -*-1r"""2Stable graphs3"""4import itertools5from collections import defaultdict67from sage.misc.misc_c import prod89from sage.structure.sage_object import SageObject1011from sage.combinat.permutation import Permutations12from sage.combinat.subset import Subsets13from sage.rings.integer_ring import ZZ14from sage.modules.free_module import FreeModule15from sage.functions.other import floor16from sage.graphs.graph import Graph17from sage.rings.all import Integer18from sage.arith.all import factorial19from sage.groups.perm_gps.permgroup_named import SymmetricGroup2021from sage.plot.polygon import polygon2d22from sage.plot.text import text23from sage.plot.bezier_path import bezier_path24from sage.plot.line import line25from sage.plot.circle import circle2627from .superseded import deprecated_function_alias28from .moduli import MODULI_ST, MODULI_TL, MODULI_CT, MODULI_RT, MODULI_SM293031class StableGraph(SageObject):32r"""33Stable graph.3435A stable graph is a graph (with allowed loops and multiple edges) that36has "genus" and "marking" decorations at its vertices. It is represented37as a list of genera of its vertices, a list of legs at each vertex and a38list of pairs of legs forming edges.3940The markings (the legs that are not part of edges) are allowed to have41repetitions.4243EXAMPLES:4445We create a stable graph with two vertices of genera 3,5 joined by an edge46with a self-loop at the genus 3 vertex::4748sage: from admcycles import StableGraph49sage: StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])50[3, 5] [[1, 3, 5], [2]] [(1, 2), (3, 5)]5152The markings can have repetitions::5354sage: StableGraph([1,0], [[1,2], [3,2]], [(1,3)])55[1, 0] [[1, 2], [3, 2]] [(1, 3)]5657It is also possible to create graphs which are not necessarily stable::5859sage: StableGraph([1,0], [[1], [2,3]], [(1,2)])60[1, 0] [[1], [2, 3]] [(1, 2)]6162sage: StableGraph([0], [[1]], [])63[0] [[1]] []6465If the input is invalid a ``ValueError`` is raised::6667sage: StableGraph([0, 0], [[1], [2], [3]], [])68Traceback (most recent call last):69...70ValueError: genera and legs must have the same length7172sage: StableGraph([0, 'hello'], [[1], [2]], [(1,2)])73Traceback (most recent call last):74...75ValueError: genera must be a list of non-negative integers7677sage: StableGraph([0, 0], [[1,2], [3]], [(3,4)])78Traceback (most recent call last):79...80ValueError: the edge (3, 4) uses invalid legs8182sage: StableGraph([0, 0], [[2,3], [2]], [(2,3)])83Traceback (most recent call last):84...85ValueError: the edge (2, 3) uses invalid legs86"""87__slots__ = ['_genera', '_legs', '_edges', '_maxleg', '_mutable',88'_graph_cache', '_canonical_label_cache', '_hash']8990def __init__(self, genera, legs, edges, mutable=False, check=True):91"""92INPUT:9394- ``genera`` -- (list) List of genera of the vertices of length m.9596- ``legs`` -- (list) List of length m, where ith entry is list of legs97attached to vertex i.9899- ``edges`` -- (list) List of edges of the graph. Each edge is a 2-tuple of legs.100101- ``mutable`` - (boolean, default to ``False``) whether this stable graph102should be mutable103104- ``check`` - (boolean, default to ``True``) whether some additional sanity105checks are performed on input106"""107if check:108if not isinstance(genera, list) or \109not isinstance(legs, list) or \110not isinstance(edges, list):111raise TypeError('genera, legs, edges must be lists')112113if len(genera) != len(legs):114raise ValueError('genera and legs must have the same length')115116if not all(isinstance(g, (int, Integer)) and g >= 0 for g in genera):117raise ValueError("genera must be a list of non-negative integers")118119mlegs = defaultdict(int)120for v, l in enumerate(legs):121if not isinstance(l, list):122raise ValueError("legs must be a list of lists")123for i in l:124if not isinstance(i, (int, Integer)) or i <= 0:125raise ValueError("legs must be positive integers")126mlegs[i] += 1127128for e in edges:129if not isinstance(e, tuple) or len(e) != 2:130raise ValueError("invalid edge {}".format(e))131if mlegs.get(e[0], 0) != 1 or mlegs.get(e[1], 0) != 1:132raise ValueError("the edge {} uses invalid legs".format(e))133134self._genera = genera135self._legs = legs136self._edges = edges137self._maxleg = max([max(j + [0]) for j in self._legs]) # highest leg-label that occurs138self._mutable = bool(mutable)139self._graph_cache = None140self._canonical_label_cache = None141self._hash = None142143def set_immutable(self):144r"""145Set this graph immutable.146"""147self._mutable = False148149def is_mutable(self):150r"""151Return whether this graph is mutable.152"""153return self._mutable154155def __hash__(self):156r"""157EXAMPLES::158159sage: from admcycles import StableGraph160sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])161sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])162sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])163sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])164sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])165sage: hash(G1) == hash(G2)166True167sage: (hash(G1) == hash(G3) or hash(G1) == hash(G4) or168....: hash(G1) == hash(G5) or hash(G3) == hash(G4) or169....: hash(G3) == hash(G5) or hash(G4) == hash(G5))170False171"""172if self._mutable:173raise TypeError("mutable stable graph are not hashable")174if self._hash is None:175self._hash = hash((tuple(self._genera),176tuple(tuple(x) for x in self._legs),177tuple(self._edges)))178return self._hash179180def copy(self, mutable=True):181r"""182Return a copy of this graph.183184When it is asked for an immutable copy of an immutable graph the185current graph is returned without copy.186187INPUT:188189- ``mutable`` - (boolean, default ``True``) whether the returned graph must190be mutable191192EXAMPLES::193194sage: from admcycles import StableGraph195sage: G = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])196sage: H = G.copy()197sage: H == G198True199sage: H.is_mutable()200True201sage: H is G202False203204sage: G.copy(mutable=False) is G205True206"""207if not self.is_mutable() and not mutable:208# avoid copy when immutability holds for both209return self210G = StableGraph.__new__(StableGraph)211G._genera = self._genera[:]212G._legs = [x[:] for x in self._legs]213G._edges = [x[:] for x in self._edges]214G._maxleg = self._maxleg215G._mutable = mutable216G._graph_cache = None217G._canonical_label_cache = None218G._hash = None219return G220221def __repr__(self):222return repr(self._genera) + ' ' + repr(self._legs) + ' ' + repr(self._edges)223224def __lt__(self, other):225r"""226TESTS::227228sage: from admcycles import StableGraph229sage: g0 = StableGraph([1], [[1, 2, 3]], [])230sage: g1 = StableGraph([0], [[5, 6, 1, 2, 3]], [(5, 6)])231sage: g2 = StableGraph([0, 1], [[1, 2, 5], [3, 6]], [(5, 6)])232sage: g3 = StableGraph([0, 1], [[1, 3, 5], [2, 6]], [(5, 6)])233sage: g4 = StableGraph([0, 1], [[2, 3, 5], [1, 6]], [(5, 6)])234sage: sorted([g3, g1, g2, g4, g0]) == sorted([g2, g0, g4, g1, g3]) == [g0, g1, g2, g3, g4]235True236237sage: g0 < g0238False239sage: g0 <= g0240True241242sage: g1 > g0 and g1 >= g0 and g2 > g1 and g2 >= g1243True244sage: g1 < g0 or g1 <= g0 or g2 < g1 or g2 <= g1245False246"""247if type(self) is not type(other):248raise TypeError('incomparable elements')249250# first sort by number of edges251sne = self.num_edges()252one = other.num_edges()253if sne < one:254return True255if one < sne:256return False257258# then by number of vertices259snv = self.num_verts()260onv = other.num_verts()261if snv < onv:262return True263elif snv > onv:264return False265266# then use plain comparison of the data267if self._genera < other._genera:268return True269elif self._genera > other._genera:270return False271elif self._legs < other._legs:272return True273elif self._legs > other._legs:274return False275elif self._edges < other._edges:276return True277elif self._edges > other._edges:278return False279else:280# equality case281return False282283def __eq__(self, other):284r"""285TESTS::286287sage: from admcycles import StableGraph288sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])289sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])290sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])291sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])292sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])293sage: G1 == G2294True295sage: G1 == G3 or G1 == G4 or G1 == G5296False297"""298if type(self) is not type(other):299return False300return self._genera == other._genera and \301self._legs == other._legs and \302self._edges == other._edges303304def __ne__(self, other):305return not (self == other)306307def __le__(self, other):308return self == other or self < other309310def __gt__(self, other):311return other < self312313def __ge__(self, other):314return self == other or other < self315316def _graph(self):317if not self._mutable and self._graph_cache is not None:318return self._graph_cache319320# TODO: it should be much faster to build this graph...321from collections import defaultdict322323legs = defaultdict(list)324325# the multiplicity of edges is stored as integral labels326# on the graph327G = Graph(len(self._genera), loops=True, multiedges=False, weighted=True)328for li, lj in self._edges:329i = self.vertex(li)330j = self.vertex(lj)331if G.has_edge(i, j):332G.set_edge_label(i, j, G.edge_label(i, j) + 1)333else:334G.add_edge(i, j, 1)335336if i < j:337legs[i, j].append((li, lj))338else:339legs[j, i].append((lj, li))340341vertex_partition = defaultdict(list)342for i, (g, l) in enumerate(zip(self._genera, self._legs)):343l = sorted(self.list_markings(i))344inv = (g,) + tuple(l)345vertex_partition[inv].append(i)346347vertex_data = sorted(vertex_partition)348partition = [vertex_partition[k] for k in vertex_data]349350output = (G, vertex_data, dict(legs), partition)351if not self._mutable:352self._graph_cache = output353return output354355def _canonical_label(self):356if not self._mutable and self._canonical_label_cache is not None:357return self._canonical_label_cache358G, _, _, partition = self._graph()359360# NOTE: we explicitly set algorithm='sage' since bliss got confused361# with edge labeled graphs. See362# https://trac.sagemath.org/ticket/28531363H, phi = G.canonical_label(partition=partition,364edge_labels=True,365certificate=True,366algorithm='sage')367368output = (H, phi)369if not self._mutable:370self._canonical_label_cache = output371return output372373def set_canonical_label(self, certificate=False):374r"""375Set canonical label to this stable graph.376377The canonical labeling is such that two isomorphic graphs get relabeled378the same way. If certificate is true return a pair `dicv`, `dicl` of379mappings from the vertices and legs to the canonical ones.380381EXAMPLES::382383sage: from admcycles import StableGraph384385sage: S = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)])386387sage: T = S.copy()388sage: T.set_canonical_label()389sage: T # random390[1, 2, 3] [[1, 4, 6], [2, 5, 8], [3, 7, 9]] [(4, 5), (6, 7), (8, 9)]391sage: assert S.is_isomorphic(T)392393Check that isomorphic graphs get relabelled the same way::394395sage: S0 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)], mutable=False)396sage: S1 = S0.copy()397sage: S2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)], mutable=True)398sage: S3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)], mutable=True)399sage: S1.set_canonical_label()400sage: S2.set_canonical_label()401sage: S3.set_canonical_label()402sage: assert S1 == S2 == S3403sage: assert S0.is_isomorphic(S1)404405sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=False)406sage: S1 = S0.copy()407sage: S2 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)408sage: S3 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)409sage: S1.set_canonical_label()410sage: S2.set_canonical_label()411sage: S3.set_canonical_label()412sage: assert S1 == S2 == S3413sage: assert S0.is_isomorphic(S1)414415sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=True)416sage: S1 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)417sage: S2 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)418sage: for S in [S0, S1, S2]:419....: T = S.copy()420....: assert S == T421....: vm, lm = T.set_canonical_label(certificate=True)422....: S.relabel(vm, lm, inplace=True)423....: S.tidy_up()424....: assert S == T, (S, T)425426427TESTS::428429sage: from admcycles import StableGraph430sage: S0 = StableGraph([2], [[]], [], mutable=True)431sage: S0.set_canonical_label()432433sage: S0 = StableGraph([0,1,0], [[1,2,4], [3], [5,6,7]], [(2,3), (4,5), (6,7)])434sage: S1 = S0.copy()435sage: S1.set_canonical_label()436sage: assert S0.is_isomorphic(S1)437sage: S2 = S1.copy()438sage: S1.set_canonical_label()439sage: assert S1 == S2440441sage: S0 = StableGraph([1,0], [[2,3], [5,7,9]], [(2,5), (7,9)], mutable=True)442sage: S1 = StableGraph([1,0], [[1,3], [2,7,9]], [(1,2), (7,9)], mutable=True)443sage: S0.set_canonical_label()444sage: S1.set_canonical_label()445sage: assert S0 == S1446"""447if not self._mutable:448raise ValueError("the graph must be mutable; use inplace=False")449450G, vdat, legs, part = self._graph()451H, dicv = self._canonical_label()452invdicv = {j: i for i, j in dicv.items()}453if certificate:454dicl = {}455456# vdat: list of the possible (g, l0, l1, ...)457# where l0, l1, ... are the markings458# legs: dico: (i,j) -> list of edges given by pair of legs459460new_genera = [self._genera[invdicv[i]] for i in range(self.num_verts())]461assert new_genera == sorted(self._genera)462new_legs = [sorted(self.list_markings(invdicv[i])) for i in range(self.num_verts())]463464can_edges = []465for e0, e1, mult in G.edges(sort=False):466f0 = dicv[e0]467f1 = dicv[e1]468inv = False469if f0 > f1:470f0, f1 = f1, f0471inv = True472can_edges.append((f0, f1, e0, e1, mult, inv))473can_edges.sort()474475nl = self.num_legs()476marks = set(self.list_markings())477m0 = (max(marks) if marks else 0) + 1478non_markings = range(m0, m0 + nl - len(marks))479assert len(non_markings) == 2 * sum(mult for _, _, _, _, mult, _ in can_edges)480481k = 0482new_edges = []483for f0, f1, e0, e1, mult, inv in can_edges:484assert f0 <= f1485new_legs[f0].extend(non_markings[i] for i in range(k, k + 2 * mult, 2))486new_legs[f1].extend(non_markings[i] for i in range(k + 1, k + 2 * mult, 2))487new_edges.extend((non_markings[i], non_markings[i + 1]) for i in range(k, k + 2 * mult, 2))488489if certificate:490assert len(legs[e0, e1]) == mult, (len(legs[e0, e1]), mult)491if inv:492assert dicv[e0] == f1493assert dicv[e1] == f0494for i, (l1, l0) in enumerate(legs[e0, e1]):495dicl[l0] = non_markings[k + 2 * i]496dicl[l1] = non_markings[k + 2 * i + 1]497else:498assert dicv[e0] == f0499assert dicv[e1] == f1500for i, (l0, l1) in enumerate(legs[e0, e1]):501dicl[l0] = non_markings[k + 2 * i]502dicl[l1] = non_markings[k + 2 * i + 1]503504k += 2 * mult505506self._genera = new_genera507self._legs = new_legs508self._edges = new_edges509510if certificate:511return (dicv, dicl)512513def relabel(self, vm, lm, inplace=False, mutable=False):514r"""515INPUT:516517- ``vm`` -- (dictionary) a vertex map (from the label of this graph to518the new labels). If a vertex number is missing it will remain untouched.519520- ``lm`` -- (dictionary) a leg map (idem). If a leg label is missing it521will remain untouched.522523- ``inplace`` -- (boolean, default ``False``) whether to relabel the524graph inplace525526- ``mutable`` -- (boolean, default ``False``) whether to make the527resulting graph immutable. Ignored when ``inplace=True``528529EXAMPLES::530531sage: from admcycles import StableGraph532sage: G = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])533sage: vm = {0:1, 1:0}534sage: lm = {1:3, 2:5, 3:7, 4:9, 5:15}535sage: G.relabel(vm, lm)536[5, 3] [[5], [3, 7, 15, 9]] [(3, 5), (7, 15)]537538sage: G.relabel(vm, lm, mutable=False).is_mutable()539False540sage: G.relabel(vm, lm, mutable=True).is_mutable()541True542543sage: H = G.copy()544sage: H.relabel(vm, lm, inplace=True)545sage: H == G.relabel(vm, lm)546True547"""548# TODO: more inplace operations when possible549m = len(self._genera)550genera = [None] * m551legs = [None] * m552for i, (g, l) in enumerate(zip(self._genera, self._legs)):553j = vm.get(i, i) # new label554genera[j] = g555legs[j] = [lm.get(k, k) for k in l]556557edges = [(lm.get(i, i), lm.get(j, j)) for i, j in self._edges]558559if inplace:560if not self._mutable:561raise ValueError('the graph is not mutable; use a copy instead')562self._genera = genera563self._legs = legs564self._edges = edges565else:566return StableGraph(genera, legs, edges, mutable)567568def is_isomorphic(self, other, certificate=False):569r"""570Test whether this stable graph is isomorphic to ``other``.571572INPUT:573574- ``certificate`` - if set to ``True`` return also a vertex mapping and575a legs mapping576577EXAMPLES::578579sage: from admcycles import StableGraph580581sage: G1 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])582sage: G2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)])583sage: G3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)])584sage: G1.is_isomorphic(G2) and G1.is_isomorphic(G3)585True586587Graphs with distinct markings are not isomorphic::588589sage: G4 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,4)])590sage: G1.is_isomorphic(G4) or G2.is_isomorphic(G4) or G3.is_isomorphic(G4)591False592593Graph with marking multiplicities::594595sage: H1 = StableGraph([0], [[1,1]], [])596sage: H2 = StableGraph([0], [[1,1]], [])597sage: H1.is_isomorphic(H2)598True599600sage: H3 = StableGraph([0,0], [[1,2,4],[1,3]], [(2,3)])601sage: H4 = StableGraph([0,0], [[1,2],[1,3,4]], [(2,3)])602sage: H3.is_isomorphic(H4)603True604605TESTS::606607sage: from admcycles import StableGraph608609sage: G = StableGraph([0, 0], [[5, 8, 4, 3], [9, 2, 1]], [(8, 9)])610sage: H = StableGraph([0, 0], [[1, 2, 6], [3, 4, 5, 7]], [(6, 7)])611sage: G.is_isomorphic(H)612True613sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)614sage: assert ans615sage: G.relabel(vm, lm)616[0, 0] [[6, 2, 1], [5, 7, 4, 3]] [(7, 6)]617618sage: G = StableGraph([0, 0], [[4, 8, 2], [3, 9, 1]], [(8, 9)])619sage: H = StableGraph([0, 0], [[1, 3, 5], [2, 4, 6]], [(5, 6)])620sage: G.is_isomorphic(H)621True622sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)623sage: assert ans624sage: G.relabel(vm, lm)625[0, 0] [[3, 5, 1], [4, 6, 2]] [(6, 5)]626627sage: G = StableGraph([0, 1, 1], [[1, 3, 5], [2, 4], [6]], [(1, 2), (3, 4), (5, 6)])628sage: H = StableGraph([1, 0, 1], [[1], [2, 3, 5], [4, 6]], [(1, 2), (3, 4), (5, 6)])629sage: _ = G.is_isomorphic(H, certificate=True)630631Check for https://gitlab.com/modulispaces/admcycles/issues/22::632633sage: g1 = StableGraph([0, 0], [[1, 3], [2,4]], [(1, 2), (3, 4)])634sage: g2 = StableGraph([0, 0], [[1], [2]], [(1, 2)])635sage: g1.is_isomorphic(g2)636False637638Check for https://gitlab.com/modulispaces/admcycles/issues/24::639640sage: 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)])641sage: 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)])642sage: gr1.is_isomorphic(gr2)643False644"""645if type(self) != type(other):646raise TypeError647648sG, svdat, slegs, spart = self._graph()649oG, ovdat, olegs, opart = other._graph()650651# first compare vertex data partitions652if svdat != ovdat or any(len(p) != len(q) for p, q in zip(spart, opart)):653return (False, None) if certificate else False654655# next, graph isomorphism656# NOTE: since canonical label maps the first atom of the partition to657# {0, ..., m_1 - 1} the second to {m_1, ..., m_1 + m_2 - 1}, etc we are658# guaranteed that the partitions match when the graphs are isomorphic.659sH, sphi = self._canonical_label()660oH, ophi = other._canonical_label()661if sH != oH:662return (False, None) if certificate else False663664if certificate:665ophi_inv = {j: i for i, j in ophi.items()}666vertex_map = {i: ophi_inv[sphi[i]] for i in range(len(self._genera))}667668legs_map = {}669# legs that are not part of edges are untouched670# (slot zero in the list is the genus)671for l in svdat:672for i in l[1:]:673legs_map[i] = i674675# pair of legs that form edges have moved676for (i, j), sl in slegs.items():677m = sG.edge_label(i, j)678assert len(sl) == m, (m, sl, self, other)679680ii = ophi_inv[sphi[i]]681jj = ophi_inv[sphi[j]]682if ii <= jj:683ol = olegs[ii, jj]684else:685ol = [(b, a) for (a, b) in olegs[jj, ii]]686assert len(ol) == m, (m, sl, ol, self, other)687688for (a, b), (c, d) in zip(sl, ol):689legs_map[a] = c690legs_map[b] = d691692return True, (vertex_map, legs_map)693694else:695return True696697def vertex_automorphism_group(self, *args, **kwds):698r"""699Return the action of the automorphism group on the vertices.700701All arguments provided are forwarded to the method `automorphism_group`702of Sage graphs.703704EXAMPLES::705706sage: from admcycles import StableGraph707708sage: G = StableGraph([0], [[1, 2, 3, 4, 5, 6]], [(3, 4), (5, 6)])709sage: G.vertex_automorphism_group()710Permutation Group with generators [()]711712sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])713sage: G.vertex_automorphism_group()714Permutation Group with generators [(0,1)]715sage: G = StableGraph([0, 0], [[4, 1, 2], [1, 3, 4]], [(3, 2)])716sage: G.vertex_automorphism_group()717Permutation Group with generators [(0,1)]718719sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],720....: [(2,3),(4,5),(6,1)])721sage: A = G.vertex_automorphism_group()722sage: A723Permutation Group with generators [(1,2), (0,1)]724sage: A.cardinality()7256726727Using extra arguments::728729sage: G.vertex_automorphism_group(algorithm='sage')730Permutation Group with generators [(1,2), (0,1)]731sage: G.vertex_automorphism_group(algorithm='bliss') # optional - bliss732Permutation Group with generators [(1,2), (0,1)]733"""734G, _, _, partition = self._graph()735return G.automorphism_group(partition=partition,736edge_labels=True,737*args, **kwds)738739def leg_automorphism_induce(self, g, A=None, check=True):740r"""741Given a leg automorphism ``g`` return its action on the vertices.742743Note that there is no check that the element ``g`` is a valid automorphism.744745INPUT:746747- ``g`` - a permutation acting on the legs748749- ``A`` - ambient permutation group for the result750751- ``check`` - (default ``True``) parameter forwarded to the constructor752of the permutation group acting on vertices.753754EXAMPLES::755756sage: from admcycles import StableGraph757sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],758....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])759sage: Averts = G.vertex_automorphism_group()760sage: Alegs = G.leg_automorphism_group()761sage: g = Alegs('(1,14)(2,13)(3,4)(5,10)(6,9)(7,8)(11,12)(15,16)')762sage: assert G.leg_automorphism_induce(g) == Averts('(0,3)(1,2)')763sage: g = Alegs('(3,11)(4,12)(5,13)(6,14)')764sage: assert G.leg_automorphism_induce(g) == Averts('')765sage: g = Alegs('(1,11,13,15,9,3,5,7)(2,12,14,16,10,4,6,8)')766sage: assert G.leg_automorphism_induce(g) == Averts('(0,1,2,3)')767768TESTS::769770sage: G = StableGraph([3], [[]], [])771sage: S = SymmetricGroup([0])772sage: G.leg_automorphism_induce(S(''))773()774"""775if A is None:776A = SymmetricGroup(list(range(len(self._genera))))777if len(self._genera) == 1:778return A('')779p = [self.vertex(g(self._legs[u][0])) for u in range(len(self._genera))]780return A(p, check=check)781782def vertex_automorphism_lift(self, g, A=None, check=True):783r"""784Provide a canonical lift of vertex automorphism to leg automorphism.785786Note that there is no check that ``g`` defines a valid automorphism of787the vertices.788789INPUT:790791- ``g`` - permutation automorphism of the vertices792793- ``A`` - an optional permutation group in which the result belongs to794795- ``check`` -- (default ``True``) parameter forwarded to the constructor796of the permutation group797798EXAMPLES::799800sage: from admcycles import StableGraph801sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],802....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])803sage: Averts = G.vertex_automorphism_group()804sage: G.vertex_automorphism_lift(Averts(''))805()806sage: G.vertex_automorphism_lift(Averts('(0,3)(1,2)'))807(1,6)(2,5)(3,4)(7,8)(9,14)(10,13)(11,12)(15,16)808sage: G.vertex_automorphism_lift(Averts('(0,1,2,3)'))809(1,3,5,7)(2,4,6,8)(9,11,13,15)(10,12,14,16)810"""811p = sorted(sum(self._legs, []))812if A is None:813A = SymmetricGroup(p)814leg_pos = {j: i for i, j in enumerate(p)}815816G, _, edge_to_legs, partition = self._graph()817818# promote automorphisms of G as automorphisms of the stable graph819for u, v, lab in G.edges(sort=True):820gu = g(u)821gv = g(v)822823if u < v:824start = edge_to_legs[u, v]825else:826start = [(t, s) for s, t in edge_to_legs[v, u]]827828if gu < gv:829end = edge_to_legs[gu, gv]830else:831end = [(t, s) for s, t in edge_to_legs[gv, gu]]832833for (lu, lv), (glu, glv) in zip(start, end):834p[leg_pos[lu]] = glu835p[leg_pos[lv]] = glv836837return A(p, check=check)838839def leg_automorphism_group_vertex_stabilizer(self, *args, **kwds):840r"""841Return the group of automorphisms of this stable graph stabilizing the vertices.842843All arguments are forwarded to the subgroup method of the Sage symmetric group.844845EXAMPLES::846847sage: from admcycles import StableGraph848sage: G = StableGraph([0],[[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])849sage: G.leg_automorphism_group_vertex_stabilizer()850Subgroup 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)851852sage: G = StableGraph([1,1],[[1,2],[3,4]], [(1,3),(2,4)])853sage: G.leg_automorphism_group_vertex_stabilizer()854Subgroup generated by [(1,2)(3,4)] of (Symmetric group of order 4! as a permutation group)855"""856legs = sorted(sum(self._legs, []))857858G, _, edge_to_legs, partition = self._graph()859860S = SymmetricGroup(legs)861gens = []862for u, v, lab in G.edges(sort=True):863if lab == 1 and u != v:864continue865866if u < v:867multiedge = edge_to_legs[u, v]868else:869multiedge = edge_to_legs[v, u]870871if lab >= 2:872(lu0, lv0) = multiedge[0]873(lu1, lv1) = multiedge[1]874gens.append(S.element_class([(lu0, lu1), (lv0, lv1)], S, check=False))875if lab >= 3:876gens.append(S.element_class([tuple(x for x, y in multiedge),877tuple(y for x, y in multiedge)], S, check=False))878if u == v:879(lu, lv) = multiedge[0]880gens.append(S.element_class([(lu, lv)], S, check=False))881882return S.subgroup(gens, *args, **kwds)883884def leg_automorphism_group(self, *args, **kwds):885r"""886Return the action of the automorphism group on the legs.887888The arguments provided to this function are forwarded to the889constructor of the subgroup of the symmetric group.890891EXAMPLES::892893sage: from admcycles import StableGraph894895A triangle::896897sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],898....: [(2,3),(4,5),(6,1)])899sage: Alegs = G.leg_automorphism_group()900sage: assert Alegs.cardinality() == G.automorphism_number()901902A vertex with four loops::903904sage: G = StableGraph([0], [[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])905sage: Alegs = G.leg_automorphism_group()906sage: assert Alegs.cardinality() == G.automorphism_number()907sage: a = Alegs.random_element()908909Using extra arguments::910911sage: G = StableGraph([0,0,0], [[6,1,7,8],[2,3,9,10],[4,5,11,12]],912....: [(1,2), (3,4), (5,6), (7,8), (9,10), (11,12)])913sage: G.leg_automorphism_group()914Subgroup 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)915sage: G.leg_automorphism_group(canonicalize=False)916Subgroup 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)917"""918legs = sorted(sum(self._legs, []))919G, _, edge_to_legs, partition = self._graph()920# NOTE: this is too much generators. It is enough to move around the multiple921# edges in each orbit of the vertex automorphism group.922S = SymmetricGroup(legs)923gens = []924for g in self.vertex_automorphism_group().gens():925gens.append(self.vertex_automorphism_lift(g, S))926for g in self.leg_automorphism_group_vertex_stabilizer().gens():927gens.append(g)928929return S.subgroup(gens, *args, **kwds)930931def automorphism_number(self):932r"""933Return the size of the automorphism group (acting on legs).934935EXAMPLES::936937sage: from admcycles import StableGraph938sage: G = StableGraph([0, 2], [[1, 2, 4], [3, 5]], [(4, 5)])939sage: G.automorphism_number()9401941942sage: G = StableGraph([0, 0, 0], [[1,2,7,8], [3,4,9,10], [5,6,11,12]],943....: [(2,3),(4,5),(6,1),(8,9),(10,11),(12,7)])944sage: G.automorphism_number()94548946sage: G.leg_automorphism_group().cardinality()94748948949sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])950sage: G.automorphism_number()951Traceback (most recent call last):952...953NotImplementedError: automorphism_number not valid for repeated marking954955TESTS::956957sage: from sage.combinat.permutation import Permutations958sage: glist=[1,1,2,2]959sage: for p in Permutations([0,1,2,3]):960....: gr = StableGraph([0]+[glist[i] for i in p],[[1,2,3,4],[5],[6],[7],[8]],[(1,5),(2,6),(3,7),(4,8)])961....: assert gr.automorphism_number() == 4, (gr, gr.automorphism_number())962"""963G, _, _, _ = self._graph()964aut = self.vertex_automorphism_group().cardinality()965966# edge automorphism967for i, j, lab in G.edges(sort=False):968aut *= factorial(lab)969if i == j:970aut *= 2**lab971972# explicitly forbid marking multiplicity973markings = self.list_markings()974if len(markings) != len(set(markings)):975raise NotImplementedError("automorphism_number not valid for repeated marking")976977return aut978979def g(self):980r"""981Return the genus of this stable graph982983EXAMPLES::984985sage: from admcycles import StableGraph986sage: G = StableGraph([1,2],[[1,2], [3,4]], [(1,3),(2,4)])987sage: G.g()9884989"""990return sum(self._genera) + len(self._edges) - len(self._genera) + ZZ.one()991992def n(self):993r"""994Return the number of legs of the stable graph.995996EXAMPLES::997998sage: from admcycles import StableGraph999sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)]);G.n()100021001"""1002return len(self.list_markings())10031004def genera(self, i=None, copy=True):1005"""1006Return the list of genera of the stable graph.10071008If an integer argument is given, return the genus at this vertex.10091010EXAMPLES::10111012sage: from admcycles import StableGraph1013sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])1014sage: G.genera()1015[1, 2]1016sage: G.genera(0), G.genera(1)1017(1, 2)1018"""1019if i is None:1020return self._genera[:] if copy else self._genera1021else:1022return self._genera[i]10231024def edges(self, copy=True):1025r"""1026Return the list of edges.10271028By default, this returns a copy of the corresponding list,1029set copy=False to obtain the internal variable of the graph.10301031EXAMPLES::10321033sage: from admcycles import StableGraph1034sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7)])1035sage: L = Gamma.edges(); L1036[(2, 3), (4, 5), (6, 7)]1037sage: L.append((8,9)); L1038[(2, 3), (4, 5), (6, 7), (8, 9)]1039sage: Gamma # not changed by above operation1040[1, 1] [[1, 2, 3, 4, 5, 6], [7, 8, 9]] [(2, 3), (4, 5), (6, 7)]1041"""1042return self._edges[:] if copy else self._edges10431044def num_verts(self):1045r"""1046Return the number of vertices.10471048EXAMPLES::10491050sage: from admcycles import StableGraph1051sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])1052sage: Gamma.num_verts()105321054"""1055return len(self._genera)10561057def legs(self, v=None, copy=True):1058r"""1059Return the list of legs at vertex v, or the whole list of1060legs if v is not specified.10611062By default, this returns a copy of the corresponding list(s),1063set copy=False to obtain the internal variable of the graph.10641065EXAMPLES::10661067sage: from admcycles import StableGraph1068sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7)])1069sage: L = Gamma.legs(0); L1070[1, 2, 3, 4, 5, 6]1071sage: L.append(10); L1072[1, 2, 3, 4, 5, 6, 10]1073sage: Gamma # not changed by above operation1074[1, 1] [[1, 2, 3, 4, 5, 6], [7, 8, 9]] [(2, 3), (4, 5), (6, 7)]1075sage: Gamma.legs()1076[[1, 2, 3, 4, 5, 6], [7, 8, 9]]1077"""1078if v is None:1079return [l[:] for l in self._legs] if copy else self._legs1080else:1081return self._legs[v][:] if copy else self._legs[v]10821083# TODO: deprecate1084numvert = num_verts10851086def num_edges(self):1087r"""1088Return the number of edges.10891090EXAMPLES::10911092sage: from admcycles import StableGraph1093sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])1094sage: Gamma.num_edges()109541096"""1097return len(self._edges)10981099def num_loops(self):1100r"""1101Return the number of loops (ie edges attached twice to a vertex).11021103EXAMPLES::11041105sage: from admcycles import StableGraph1106sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])1107sage: Gamma.num_loops()110831109"""1110n = 01111for h0, h1 in self._edges:1112n += self.vertex(h0) == self.vertex(h1)1113return n11141115def num_legs(self, i=None):1116r"""1117Return the number of legs at vertex i, or the total number of legs1118if i is not specified.11191120EXAMPLES::11211122sage: from admcycles import StableGraph1123sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)])1124sage: Gamma.num_legs(0)112541126sage: Gamma.num_legs()112761128"""1129if i is None:1130return sum(len(l) for l in self._legs)1131else:1132return len(self._legs[i])11331134def _graph_(self):1135r"""1136Return the Sage graph object encoding the stable graph11371138This inserts a vertex with label (i,j) in the middle of the edge (i,j).1139Also inserts a vertex with label ('L',i) at the end of each leg l.11401141EXAMPLES::11421143sage: from admcycles import StableGraph1144sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])1145sage: SageGr = Gr._graph_()1146sage: SageGr1147Multi-graph on 5 vertices1148sage: SageGr.vertices(sort=False) # random1149[(1, 2), 0, 1, (3, 5), ('L', 7)]1150"""1151G = Graph(multiedges=True)1152for i, j in self._edges:1153G.add_vertex((i, j))1154G.add_edge((i, j), self.vertex(i))1155G.add_edge((i, j), self.vertex(j))1156for i in self.list_markings():1157G.add_edge(('L', i), self.vertex(i))1158return G11591160def dim(self, v=None):1161"""1162Return dimension of moduli space at vertex v.11631164If v=None, return dimension of entire stratum parametrized by graph.11651166EXAMPLES::11671168sage: from admcycles import StableGraph1169sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])1170sage: Gr.dim(0), Gr.dim(1), Gr.dim()1171(10, 13, 23)1172"""1173if v is None:1174return sum([self.dim(v) for v in range(len(self._genera))])1175return 3 * self._genera[v] - 3 + len(self._legs[v])11761177# TODO: deprecate and remove1178def invariant(self):1179r"""1180Return a graph-invariant in form of a tuple of integers or tuples11811182At the moment this assumes that we only compare stable graph with same total1183g and set of markings1184currently returns sorted list of (genus,degree) for vertices11851186IDEAS:11871188* number of self-loops for each vertex11891190EXAMPLES::11911192sage: from admcycles import StableGraph1193sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])1194sage: Gr.invariant()1195((3, 4, (7,)), (5, 1, ()))1196"""1197return tuple(sorted([(self._genera[v], len(self._legs[v]), tuple(sorted(self.list_markings(v)))) for v in range(len(self._genera))]))11981199def tidy_up(self):1200r"""1201Sort legs and edges.12021203EXAMPLES::12041205sage: from admcycles import StableGraph1206sage: S = StableGraph([1, 3, 2], [[1, 5, 4], [2, 3, 6], [9, 8, 7]], [(9,3), (1,2)], mutable=True)1207sage: S.tidy_up()1208sage: S1209[1, 3, 2] [[1, 4, 5], [2, 3, 6], [7, 8, 9]] [(1, 2), (3, 9)]1210"""1211if not self._mutable:1212raise ValueError("the graph is immutable; use a copy instead")1213for e in range(len(self._edges)):1214if self._edges[e][0] > self._edges[e][1]:1215self._edges[e] = (self._edges[e][1], self._edges[e][0])1216for legs in self._legs:1217legs.sort()1218self._edges.sort()1219self._maxleg = max([max(j + [0]) for j in self._legs])12201221def vertex(self, l):1222r"""1223Return vertex number of leg l.12241225EXAMPLES::12261227sage: from admcycles import StableGraph1228sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)])1229sage: Gamma.vertex(1)123001231sage: Gamma.vertex(6)123211233"""1234for v in range(len(self._legs)):1235if l in self._legs[v]:1236return v1237return -1 # self does not have leg l12381239def list_markings(self, v=None):1240r"""1241Return the list of markings (non-edge legs) of self at vertex v.12421243EXAMPLES::12441245sage: from admcycles import StableGraph12461247sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1248sage: gam.list_markings(0)1249(3, 7)1250sage: gam.list_markings()1251(3, 7, 4)1252"""1253if v is None:1254return tuple([j for v in range(len(self._genera))1255for j in self.list_markings(v)])1256s = set(self._legs[v])1257for e in self._edges:1258s -= set(e)1259return tuple(s)12601261def leglist(self):1262r"""1263Return the list of legs12641265EXAMPLES::12661267sage: from admcycles import StableGraph12681269sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1270sage: gam.leglist()1271[1, 3, 7, 2, 4]1272"""1273return [j for leg in self._legs for j in leg]12741275def halfedges(self):1276r"""1277Return the tuple containing all half-edges, i.e. legs belonging to an edge12781279EXAMPLES::12801281sage: from admcycles import StableGraph12821283sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1284sage: gam.halfedges()1285(1, 2)1286"""1287return tuple(j for ed in self._edges for j in ed)12881289def edges_between(self, i, j):1290r"""1291Return 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 once12921293EXAMPLES::12941295sage: from admcycles import StableGraph12961297sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1298sage: gam.edges_between(0, 1)1299[(1, 2)]1300sage: gam.edges_between(0, 0)1301[]1302"""1303if i == j:1304return [e for e in self._edges if (e[0] in self._legs[i] and e[1] in self._legs[j])]1305else:1306return [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])]13071308def forget_markings(self, markings):1309r"""1310Erase all legs in the list markings, do not check if this gives well-defined graph13111312EXAMPLES::13131314sage: from admcycles import StableGraph1315sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)], mutable=True)1316sage: Gamma.forget_markings([1,3,8])1317[0, 2] [[2, 4], [6]] [(4, 6)]1318"""1319if not self._mutable:1320raise ValueError("the graph is not mutable; use a copy instead")1321for m in markings:1322self._legs[self.vertex(m)].remove(m)1323return self13241325def leginversion(self, l):1326r"""1327Returns l' if l and l' form an edge, otherwise returns l13281329EXAMPLES::13301331sage: from admcycles import StableGraph13321333sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1334sage: gam.leginversion(1)133521336"""1337for a, b in self._edges:1338if a == l:1339return b1340if b == l:1341return a1342return l13431344def stabilize(self):1345r"""1346Stabilize this stable graph.13471348(all vertices with genus 0 have at least three markings) and returns1349``(dicv,dicl,dich)`` where13501351- dicv a dictionary sending new vertex numbers to the corresponding old1352vertex numbers13531354- dicl a dictionary sending new marking names to the label of the last1355half-edge that they replaced during the stabilization this happens1356for instance if a g=0 vertex with marking m and half-edge l1357(belonging to edge (l,l')) is stabilized: l' is replaced by m, so1358dicl[m]=l'13591360- dich a dictionary sending half-edges that vanished in the1361stabilization process to the last half-edge that replaced them this1362happens if a g=0 vertex with two half-edges a,b (whose corresponding1363half-edges are c,d) is stabilized: we obtain an edge (c,d) and a ->1364d, b -> d we assume here that a stabilization exists (all connected1365components have 2g-2+n>0)13661367EXAMPLES::13681369sage: from admcycles import StableGraph13701371sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1372sage: gam.stabilize()1373({0: 0, 1: 1}, {}, {})13741375sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1376sage: gam.stabilize()1377Traceback (most recent call last):1378...1379ValueError: the graph is not mutable; use a copy instead13801381sage: g = StableGraph([0,0,0], [[1,5,6],[2,3],[4,7,8]], [(1,2),(3,4),(5,6),(7,8)], mutable=True)1382sage: g.stabilize()1383({0: 0, 1: 2}, {}, {2: 4, 3: 1})1384sage: h = StableGraph([0, 0, 0], [[1], [2,3], [4]], [(1,2), (3,4)], mutable=True)1385sage: h.stabilize()1386({0: 2}, {}, {})1387"""1388if not self._mutable:1389raise ValueError("the graph is not mutable; use a copy instead")1390markings = self.list_markings()1391stable = False1392verteximages = list(range(len(self._genera)))1393dicl = {}1394dich = {}1395while not stable:1396numvert = len(self._genera)1397count = 01398stable = True1399while count < numvert:1400if self._genera[count] == 0 and len(self._legs[count]) == 1:1401stable = False1402e0 = self._legs[count][0]1403e1 = self.leginversion(e0)1404v1 = self.vertex(e1)1405self._genera.pop(count)1406verteximages.pop(count)1407numvert -= 11408self._legs[v1].remove(e1)1409self._legs.pop(count)1410try:1411self._edges.remove((e0, e1))1412except ValueError:1413self._edges.remove((e1, e0))1414elif self._genera[count] == 0 and len(self._legs[count]) == 2:1415stable = False1416e0 = self._legs[count][0]1417e1 = self._legs[count][1]14181419if e1 in markings:1420swap = e01421e0 = e11422e1 = swap14231424e1prime = self.leginversion(e1)1425v1 = self.vertex(e1prime)1426self._genera.pop(count)1427verteximages.pop(count)1428numvert -= 114291430if e0 in markings:1431dicl[e0] = e1prime1432self._legs[v1].remove(e1prime)1433self._legs[v1].append(e0)1434self._legs.pop(count)1435try:1436self._edges.remove((e1, e1prime))1437except ValueError:1438self._edges.remove((e1prime, e1))1439else:1440e0prime = self.leginversion(e0)1441self._legs.pop(count)1442try:1443self._edges.remove((e0, e0prime))1444except ValueError:1445self._edges.remove((e0prime, e0))1446try:1447self._edges.remove((e1, e1prime))1448except ValueError:1449self._edges.remove((e1prime, e1))1450self._edges.append((e0prime, e1prime))14511452# update dich1453dich[e0] = e1prime1454dich[e1] = e0prime1455dich.update({h: e1prime for h in dich if dich[h] == e0})1456dich.update({h: e0prime for h in dich if dich[h] == e1})1457else:1458count += 11459return ({i: verteximages[i] for i in range(len(verteximages))}, dicl, dich)14601461def degenerations(self, v=None, mutable=False):1462r"""1463Run through the list of all possible degenerations of this graph.14641465A degeneration happens by adding an edge at v or splitting it1466into two vertices connected by an edge the new edge always1467comes last in the list of edges.14681469If v is None, return all degenerations at all vertices.14701471EXAMPLES::14721473sage: from admcycles import StableGraph14741475sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1476sage: list(gam.degenerations(1))1477[[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)],1478[3, 0, 5] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],1479[3, 1, 4] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],1480[3, 1, 4] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],1481[3, 1, 4] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],1482[3, 1, 4] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],1483[3, 2, 3] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],1484[3, 2, 3] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],1485[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],1486[3, 2, 3] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)]]14871488All these graphs are immutable (or mutable when ``mutable=True``)::14891490sage: any(g.is_mutable() for g in gam.degenerations())1491False1492sage: all(g.is_mutable() for g in gam.degenerations(mutable=True))1493True14941495TESTS::14961497sage: from admcycles import StableGraph1498sage: G = StableGraph([2,2,2], [[1],[2,3],[4]], [(1,2),(3,4)])1499sage: sum(1 for v in range(3) for _ in G.degenerations(v))150081501sage: sum(1 for _ in G.degenerations())150281503"""1504if v is None:1505for v in range(self.num_verts()):1506for h in self.degenerations(v, mutable):1507yield h1508return15091510g = self._genera[v]1511l = len(self._legs[v])15121513# for positive genus: add loop to v and decrease genus1514if g > 0:1515G = self.copy()1516G.degenerate_nonsep(v)1517if not mutable:1518G.set_immutable()1519yield G15201521# now all graphs with separating edge : separate in (g1,M) and (g-g1,legs[v]-M), take note of symmetry and stability1522for g1 in range(floor(g / 2) + 1):1523for M in Subsets(set(self._legs[v])):1524if (g1 == 0 and len(M) < 2) or \1525(g == g1 and l - len(M) < 2) or \1526(2 * g1 == g and l > 0 and (not self._legs[v][0] in M)):1527continue1528G = self.copy()1529G.degenerate_sep(v, g1, M)1530if not mutable:1531G.set_immutable()1532yield G15331534def newleg(self):1535r"""1536Create two new leg-indices that can be used to create an edge15371538This modifies ``self._maxleg``.15391540EXAMPLES::15411542sage: from admcycles import StableGraph15431544sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1545sage: gam.newleg()1546(8, 9)1547"""1548self._maxleg += 21549return (self._maxleg - 1, self._maxleg)15501551def rename_legs(self, dic, shift=None, inplace=True, return_dicts=False, mutable=False, tidyup=True):1552r"""1553Rename the markings according to the dictionary ``dic``.15541555Return a new stable graph which is isomorphic to self up to the1556change of markings given in ``dic``.15571558If ``return_dicts`` is set to ``True``, return a triple1559``(new_stable_graph, markings_relabelling, all_legs_relabelling)``1560where15611562- ``markings_relabelling`` is the dictionary whose keys are the marking of1563the current graph and the values are the new markings15641565- ``all_legs_relabelling`` is a dictionary of all the marking relabelling1566(this argument could be forwarded to :meth:`StableGraph.relabel`)15671568INPUT:15691570dic : dictionary1571the relabeling old label -> new label15721573shift : integer1574if provided, perform the corresponding shift on the non-marked legs15751576inplace : boolean (default ``True``)1577whether to do an inplace modification or return a new stable graph15781579return_dicts : boolean1580whether to return the extra relabelling information15811582mutable : boolean (default ``False``)1583whether the return graph should be mutable (ignored when ``inplace=True``)15841585EXAMPLES::15861587sage: from admcycles import StableGraph15881589sage: g = StableGraph([0], [[1,2]], [], mutable=True)1590sage: g.rename_legs({1: 3})1591[0] [[2, 3]] []15921593sage: g = StableGraph([0, 0], [[1,3,4],[2,5]], [(4,5)], mutable=True)1594sage: gg, rel, extra = g.rename_legs({1:2, 2:1}, return_dicts=True)1595sage: gg1596[0, 0] [[2, 3, 4], [1, 5]] [(4, 5)]1597sage: rel == {1: 2, 2: 1, 3: 3}1598True15991600A graph involving some changes in non-marking legs::16011602sage: g = StableGraph([0, 0], [[1, 4], [2, 3, 5]], [(4, 5)])1603sage: g.rename_legs({1: 2, 2: 4, 3: 6}, inplace=False)1604[0, 0] [[1, 2], [4, 5, 6]] [(1, 5)]1605sage: gg, markings_dic, legs_dic = g.rename_legs({1: 2, 2: 4, 3: 6}, inplace=False, return_dicts=True)1606sage: gg1607[0, 0] [[1, 2], [4, 5, 6]] [(1, 5)]1608sage: markings_dic == {1: 2, 2: 4, 3: 6}1609True1610sage: legs_dic == {1: 2, 2: 4, 3: 6, 4: 1, 5: 5}1611True16121613Using the ``shift`` argument::16141615sage: g = StableGraph([0], [[1, 2, 3]], [(2, 3)], mutable=True)1616sage: g.rename_legs({1: 5}, shift=1, inplace=False)1617[0] [[3, 4, 5]] [(3, 4)]1618sage: gg, markings_dic, legs_dic = g.rename_legs({1: 5}, shift=1, inplace=False, return_dicts=True)1619sage: gg1620[0] [[3, 4, 5]] [(3, 4)]1621sage: markings_dic == {1: 5}1622True1623sage: legs_dic == {1: 5, 2: 3, 3: 4}1624True16251626This method forbids renaming internal legs (for that purpose use :meth:`relabel`)::16271628sage: g = StableGraph([0], [[1,2,3]], [(2,3)], mutable=True)1629sage: g.rename_legs({2:5})1630Traceback (most recent call last):1631...1632ValueError: non-marking legs [2] in dic (use the relabel method instead)1633"""1634# 1. compute an admissible relabelling of the legs1635markings = self.list_markings()1636markings_relabelling = {i: dic.get(i, i) for i in markings}1637bad_legs = [i for i in dic if i not in markings]1638if bad_legs:1639raise ValueError('non-marking legs {} in dic (use the relabel method instead)'.format(bad_legs))16401641if shift is not None:1642# replace v[j] by markings_relabelling[v[j]] if v[j] in markings_relabelling and leave at v[j] otherwise1643all_legs_relabelling = {i: i + shift for e in self._edges for i in e}1644all_legs_relabelling.update(markings_relabelling)1645else:1646image_markings = tuple(sorted(markings_relabelling.values()))1647for i in range(len(image_markings) - 1):1648if image_markings[i + 1] == image_markings[i]:1649raise ValueError('repeated marking in the image')1650if markings == image_markings:1651# no need to modify any non-marking leg1652all_legs_relabelling = {i: i for e in self._edges for i in e}1653all_legs_relabelling.update(markings_relabelling)1654else:1655# find a minimal relabelling avoiding collisions by recycling the forgotten markings1656k = 01657forget = [i for i in markings if i not in image_markings]1658all_legs = []1659for l in self._legs:1660all_legs.extend(l)1661all_legs_relabelling = {i: i for e in self._edges for i in e}1662for i in image_markings:1663if i in markings or i not in all_legs_relabelling:1664continue1665# exchange i with an unused marking1666all_legs_relabelling[i] = forget[k]1667k += 11668all_legs_relabelling.update(markings_relabelling)16691670# 2. actually perform the relabelling1671if inplace:1672result = self1673result.relabel({}, all_legs_relabelling, inplace=True)1674if tidyup: # by default it will sort the list of legs and edges1675result.tidy_up()1676else:1677result = self.relabel({}, all_legs_relabelling, inplace=False, mutable=True)1678if tidyup: # by default it will sort the list of legs and edges1679result.tidy_up()1680if not mutable:1681result.set_immutable()16821683# 3. return the result1684return (result, markings_relabelling, all_legs_relabelling) if return_dicts else result16851686def degenerate_sep(self, v, g1, M):1687r"""1688degenerate vertex v into two vertices with genera g1 and g(v)-g1 and legs M and complement16891690add new edge (e[0],e[1]) such that e[0] is in new vertex v, e[1] in last vertex, which is added16911692EXAMPLES::16931694sage: from admcycles import StableGraph16951696sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1697sage: G.degenerate_sep(1, 2, [4])1698sage: G1699[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)]17001701sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1702sage: G.degenerate_sep(1, 2, [4])1703Traceback (most recent call last):1704...1705ValueError: the graph is not mutable; use a copy instead1706"""1707if not self._mutable:1708raise ValueError("the graph is not mutable; use a copy instead")1709g = self._genera[v]1710oldleg = self._legs[v]1711e = self.newleg()17121713self._genera[v] = g11714self._genera += [g - g1]1715self._legs[v] = list(M) + [e[0]]1716self._legs += [list(set(oldleg) - set(M)) + [e[1]]]1717self._edges += [e]17181719def degenerate_nonsep(self, v):1720"""1721EXAMPLES::17221723sage: from admcycles import StableGraph17241725sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)1726sage: G.degenerate_nonsep(1)1727sage: G1728[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)]17291730sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1731sage: G.degenerate_nonsep(1)1732Traceback (most recent call last):1733...1734ValueError: the graph is not mutable; use a copy instead1735"""1736if not self._mutable:1737raise ValueError("the graph is not mutable; use a copy instead")1738e = self.newleg()1739self._genera[v] -= 11740self._legs[v] += [e[0], e[1]]1741self._edges += [e]17421743def contract_edge(self, e, adddata=False):1744r"""1745Contracts the edge e=(e0,e1).17461747This method returns nothing by default. But if ``adddata`` is set to1748``True``, then it returns a tuple ``(av, edgegraph, vnum)`` where17491750- ``av`` is the the vertex in the modified graph on which previously1751the edge ``e`` had been attached17521753- ``edgegraph`` is a stable graph induced in self by the edge e1754(1-edge graph, all legs at the ends of this edge are considered as1755markings)17561757- ``vnum`` is a list of one or two vertices e was attached before the1758contraction (in the order in which they appear)17591760- ``diccv`` is a dictionary mapping old vertex numbers to new vertex1761numbers17621763EXAMPLES::17641765sage: from admcycles import StableGraph17661767sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1768sage: G.contract_edge((1,2))1769sage: G1770[8] [[3, 7, 4]] [(3, 7)]1771sage: G.contract_edge((3,7))1772sage: G1773[9] [[4]] []17741775sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1776sage: G2.contract_edge((3,7))1777sage: G2.contract_edge((1,2))1778sage: G == G21779True17801781sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)1782sage: G2.contract_edge((3,7), adddata=True)1783(0, [3] [[1, 3, 7]] [(3, 7)], [0], {0: 0, 1: 1})17841785sage: G = StableGraph([1,1,1,1],[[1],[2,4],[3,5],[6]],[(1,2),(3,4),(5,6)],mutable=True)1786sage: G.contract_edge((3,4),True)1787(1, [1, 1] [[2, 4], [3, 5]] [(3, 4)], [1, 2], {0: 0, 1: 1, 2: 1, 3: 2})17881789sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1790sage: G.contract_edge((1,2))1791Traceback (most recent call last):1792...1793ValueError: the graph is not mutable; use a copy instead1794"""1795if not self._mutable:1796raise ValueError("the graph is not mutable; use a copy instead")17971798v0 = self.vertex(e[0])1799v1 = self.vertex(e[1])18001801if v0 == v1: # contracting a loop1802if adddata:1803H = StableGraph([self._genera[v0]], [self._legs[v0][:]], [e])1804self._genera[v0] += 11805self._legs[v0].remove(e[0])1806self._legs[v0].remove(e[1])1807self._edges.remove(e)18081809if adddata:1810return (v0, H, [v0], {v: v for v in range(len(self._genera))})18111812else: # contracting an edge between different vertices1813if v0 > v1:1814v0, v1 = v1, v018151816if adddata:1817diccv = {v: v for v in range(v1)}1818diccv[v1] = v01819diccv.update({v: v - 1 for v in range(v1 + 1, len(self._genera))})1820H = StableGraph([self._genera[v0], self._genera[v1]], [self._legs[v0][:], self._legs[v1][:]], [e])18211822g1 = self._genera.pop(v1)1823self._genera[v0] += g11824l1 = self._legs.pop(v1)1825self._legs[v0] += l11826self._legs[v0].remove(e[0])1827self._legs[v0].remove(e[1])1828self._edges.remove(e)18291830if adddata:1831return (v0, H, [v0, v1], diccv)18321833def glue_vertex(self, i, Gr, divGr={}, divs={}, dil={}):1834r"""1835Glues the stable graph ``Gr`` at the vertex ``i`` of this stable graph18361837optional arguments: if divGr/dil are given they are supposed to be a1838dictionary, which will be cleared and updated with the1839renaming-convention to pass from leg/vertex-names in Gr to1840leg/vertex-names in the glued graph similarly, divs will be a1841dictionary assigning vertex numbers in the old self the corresponding1842number in the new self necessary condition:18431844- every leg of i is also a leg in Gr1845- every leg of Gr that is not a leg of i in self gets a new name18461847EXAMPLES::18481849sage: from admcycles import StableGraph1850sage: Gamma = StableGraph([2,1], [[1,2,3,4],[5]], [(2,3),(4,5)], mutable=True)1851sage: Gr = StableGraph([0,1,1], [[1,2,5],[6,3,7],[8,4]], [(5,6),(7,8)])1852sage: divGr=dict()1853sage: divs=dict()1854sage: dil=dict()1855sage: Gamma.glue_vertex(0, Gr, divGr, divs, dil)1856sage: Gamma1857[1, 0, 1, 1] [[5], [1, 2, 10], [3, 11, 12], [4, 9]] [(4, 5), (9, 12), (10, 11)]1858sage: divGr1859{0: 1, 1: 2, 2: 3}1860sage: divs1861{1: 0}1862sage: dil1863{5: 10, 6: 11, 7: 12, 8: 9}1864"""1865if not self._mutable:1866raise ValueError("the graph is not mutable; use a copy instead")18671868divGr.clear()1869divs.clear()1870dil.clear()18711872# remove vertex i, legs corresponding to it and all self-edges1873selfedges_old = self.edges_between(i, i)1874self._genera.pop(i)1875legs_old = self._legs.pop(i)1876for e in selfedges_old:1877self._edges.remove(e)18781879# when gluing in Gr, make sure that new legs corresponding to edges inside Gr1880# or legs in Gr which are already attached to a vertex different from i in self get a new unique label in the glued graph1881m = max(self._maxleg, Gr._maxleg) # largest index used in either graph, new labels m+1, m+2, ...18821883Gr_new = Gr.copy()18841885a = []1886for l in Gr._legs:1887a += l1888a = set(a) # legs of Gr1889b = set(legs_old) # legs of self at vertex i18901891# set of legs of Gr that need to be relabeled: all legs of Gr that are not attached to vertex i in self1892e = a - b1893for l in e:1894m += 11895dil[l] = m1896Gr_new.relabel({}, dil, inplace=True)18971898# vertex dictionaries1899for j in range(i):1900divs[j] = j1901for j in range(i + 1, len(self._genera) + 1):1902divs[j] = j - 11903for j in range(len(Gr._genera)):1904divGr[j] = len(self._genera) + j19051906self._genera += Gr_new._genera1907self._legs += Gr_new._legs1908self._edges += Gr_new._edges19091910self.tidy_up()19111912# TODO: overlaps with relabel1913def reorder_vertices(self, vord):1914r"""1915Reorders vertices according to tuple given (permutation of range(len(self._genera)))1916"""1917if not self._mutable:1918raise ValueError("the graph is not mutable; use a copy instead")1919new_genera = [self._genera[j] for j in vord]1920new_legs = [self._legs[j] for j in vord]1921self._genera = new_genera1922self._legs = new_legs19231924def extract_subgraph(self, vertices, outgoing_legs=None, rename=True, mutable=False):1925r"""1926Extracts from self a subgraph induced by the list vertices19271928if the list outgoing_legs is given, the markings of the subgraph are1929called 1,2,..,m corresponding to the elements of outgoing_legs in this1930case, all edges involving outgoing edges should be cut returns a triple1931(Gamma,dicv,dicl), where19321933- Gamma is the induced subgraph19341935- dicv, dicl are (surjective) dictionaries associating vertex/leg1936labels in self to the vertex/leg labels in Gamma19371938if ``rename=False``, do not rename any legs when extracting19391940EXAMPLES::19411942sage: from admcycles import StableGraph19431944sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])1945sage: gam.extract_subgraph([0])1946([3] [[1, 2, 3]] [], {0: 0}, {1: 1, 3: 2, 7: 3})1947"""1948attachedlegs = set(l for v in vertices for l in self._legs[v])1949if outgoing_legs is None:1950alllegs = attachedlegs.copy()1951for (e0, e1) in self._edges:1952if e0 in alllegs and e1 in alllegs:1953alllegs.remove(e0)1954alllegs.remove(e1)1955outgoing_legs = list(alllegs)19561957shift = len(outgoing_legs) + 11958if rename:1959dicl = {outgoing_legs[i]: i + 1 for i in range(shift - 1)}1960else:1961dicl = {l: l for l in self.leglist()}19621963genera = [self._genera[v] for v in vertices]1964legs = [[dicl.setdefault(l, l + shift) for l in self._legs[v]] for v in vertices]1965edges = [(dicl[e0], dicl[e1]) for (e0, e1) in self._edges if (1966e0 in attachedlegs and e1 in attachedlegs and (e0 not in outgoing_legs) and (e1 not in outgoing_legs))]1967dicv = {vertices[i]: i for i in range(len(vertices))}19681969return (StableGraph(genera, legs, edges, mutable=mutable), dicv, dicl)19701971def vanishes(self, moduli):1972if moduli == MODULI_ST:1973return False1974elif moduli == MODULI_TL:1975return self.num_edges() - self.num_loops() != self.num_verts() - 11976elif moduli == MODULI_CT:1977return self.num_edges() != self.num_verts() - 11978elif moduli == MODULI_RT:1979return (self.num_edges() != self.num_verts() - 1) or \1980sum(bool(g) for g in self._genera) != 11981elif moduli == MODULI_SM:1982return bool(self._edges)1983else:1984raise ValueError('unknown moduli')19851986def boundary_pushforward(self, classes=None):1987r"""1988Computes the pushforward of a product of tautological classes (one for each vertex) under the1989boundary gluing map for this stable graph.19901991INPUT:19921993- ``classes`` -- list (default: `None`); list of tautclasses, one for each vertex of the stable1994graph. The genus of the ith class is assumed to be the genus of the ith vertex, the markings1995of the ith class are supposed to be 1, ..., ni where ni is the number of legs at the ith vertex.1996Note: the jth leg at vertex i corresponds to the marking j of the ith class.1997For classes=None, place the fundamental class at each vertex.19981999EXAMPLES::20002001sage: from admcycles import StableGraph, TautologicalRing2002sage: B=StableGraph([2,1],[[4,1,2],[3,5]],[(4,5)])2003sage: Bclass=B.boundary_pushforward() # class of undecorated boundary divisor2004sage: Bclass*Bclass2005Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]2006Polynomial : -psi_5 - psi_42007sage: R1 = TautologicalRing(2,3)2008sage: R2 = TautologicalRing(1,2)2009sage: si1=B.boundary_pushforward([R1.fundamental_class(),-R2.psi(2)]); si12010Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]2011Polynomial : -psi_52012sage: si2=B.boundary_pushforward([-R1.psi(1), R2.fundamental_class()]); si22013Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]2014Polynomial : -psi_42015sage: a = Bclass*Bclass-si1-si22016sage: a.simplify()201702018"""2019from .admcycles import prodtautclass2020return prodtautclass(self.copy(mutable=False), protaut=classes).pushforward()20212022# TODO: adapt to TautologicalRing2023def boundary_pullback(self, other):2024r"""2025Pulls back the TautologicalClass or decstratum other to self and2026returns a prodtautclass with gamma=self.20272028EXAMPLES::20292030sage: from admcycles import StableGraph, psiclass2031sage: G = StableGraph([0, 2], [[1, 2, 4, 3], [5]], [(3, 5)])2032sage: H = StableGraph([2],[[1,2,4]],[])2033sage: a = H.boundary_pushforward([psiclass(3,2,3)]); a2034Graph : [2] [[1, 2, 4]] []2035Polynomial : psi_42036sage: G.boundary_pullback(a)2037Outer graph : [0, 2] [[1, 2, 4, 3], [5]] [(3, 5)]2038Vertex 0 :2039Graph : [0] [[1, 2, 3, 4]] []2040Polynomial : psi_32041Vertex 1 :2042Graph : [2] [[1]] []2043Polynomial : 12044"""2045from .tautological_ring import TautologicalClass2046from .admcycles import decstratum20472048if isinstance(other, TautologicalClass):2049from .admcycles import prodtautclass2050result = prodtautclass(self.copy(mutable=False), [])2051for t in other._terms.values():2052result += self.boundary_pullback(t)2053return result2054elif isinstance(other, decstratum):2055# NOTE: this is using much more than just stable graphs2056# I would suggest to move it out of this class2057from .admcycles import (common_degenerations, prodtautclass,2058onekppoly, kppoly, kappacl, psicl)2059rename = not (set(self.list_markings()) == set(range(1, self.n() + 1)))2060commdeg = common_degenerations(self, other.gamma, modiso=True, rename=rename)2061result = prodtautclass(self.copy(mutable=False), [])20622063for (Gamma, dicv1, dicl1, dicv2, dicl2) in commdeg:2064numvert = len(Gamma._genera)2065# first determine edges that are covered by self and other - for excess int. terms2066legcount = {l: 0 for l in Gamma.leglist()}2067for l in dicl1.values():2068legcount[l] += 12069for l in dicl2.values():2070legcount[l] += 120712072excesspoly = onekppoly(numvert)2073for e in Gamma._edges:2074if legcount[e[0]] == 2:2075excesspoly *= ((-1) * (psicl(e[0], numvert) + psicl(e[1], numvert)))20762077# partition vertices of Gamma according to where they go in self2078v1preim = [[] for v in range(len(self._genera))]2079for w in dicv1:2080v1preim[dicv1[w]].append(w)20812082graphpartition = [Gamma.extract_subgraph(2083v1preim[v], outgoing_legs=[dicl1[l] for l in self._legs[v]]) for v in range(len(self._genera))]20842085v2preim = [[] for v in range(len(other.gamma._genera))]2086for w in dicv2:2087v2preim[dicv2[w]].append(w)20882089resultpoly = kppoly([], [])2090# 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 terms2091for (kappa, psi, coeff) in other.poly:2092# psi-classes are transferred by dicl2, but kappa-classes might be split up if dicv2 has multiple preimages of same vertex2093# multiply everything together in a kppoly on Gamma, then split up this polynomial according to graphpartition2094psipolydict = {dicl2[l]: psi[l] for l in psi}2095psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])20962097kappapoly = prod([prod([sum([kappacl(w, k + 1, numvert) for w in v2preim[v]])**kappa[v][k]2098for k in range(len(kappa[v]))]) for v in range(len(other.gamma._genera))])20992100resultpoly += coeff * psipoly * kappapoly21012102resultpoly *= excesspoly2103# TODO: filter for terms that vanish by dimension reasons?21042105# now fiddle the terms of resultpoly apart and distribute them to graphpartition2106for (kappa, psi, coeff) in resultpoly:2107decstratlist = []2108for v in range(len(self._genera)):2109kappav = [kappa[w] for w in v1preim[v]]2110psiv = {graphpartition[v][2][l]: psi[l] for l in graphpartition[v][2] if l in psi}2111decstratlist.append(decstratum(graphpartition[v][0], kappa=kappav, psi=psiv))2112tempresu = prodtautclass(self, [decstratlist])2113tempresu *= coeff2114result += tempresu2115return result2116else:2117raise TypeError('invalid input other={}'.format(other))21182119def to_tautological_class(self, R=None):2120r"""2121Return the pure boundary stratum associated to this stable graph.21222123Note: does not divide by automorphisms!21242125EXAMPLES::21262127sage: from admcycles import StableGraph21282129sage: g = StableGraph([0,0], [[1,3,4], [2,5,6]], [(3,5),(4,6)])2130sage: g.to_tautological_class()2131Graph : [0, 0] [[1, 3, 4], [2, 5, 6]] [(3, 5), (4, 6)]2132Polynomial : 121332134TESTS::21352136sage: from admcycles import StableGraph21372138sage: g = StableGraph([0,0], [[1,3,4], [2,5,6]], [(3,5),(4,6)])2139sage: g.to_tautclass()2140doctest:...: DeprecationWarning: to_tautclass is deprecated. Please use to_tautological_class instead.2141See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.2142Graph : [0, 0] [[1, 3, 4], [2, 5, 6]] [(3, 5), (4, 6)]2143Polynomial : 12144"""2145if R is None:2146from .tautological_ring import TautologicalRing2147R = TautologicalRing(self.g(), self.n())2148return R(self)21492150to_tautclass = deprecated_function_alias(109, to_tautological_class)21512152def _vertex_module(self):2153nv = len(self._genera)2154return FreeModule(ZZ, nv)21552156def _edge_module(self):2157ne = len(self._edges)2158return FreeModule(ZZ, ne)21592160# TODO: implement cache as this is used intensively in the flow code below2161# See https://gitlab.com/modulispaces/admcycles/issues/142162def spanning_tree(self, root=0):2163r"""2164Return a spanning tree.21652166INPUT:21672168- ``root`` - optional vertex for the root of the spanning tree21692170OUTPUT: a triple21712172- list of triples ``(vertex ancestor, sign, edge index)`` where the2173triple at position ``i`` correspond to vertex ``i``.21742175- the set of indices of extra edges not in the tree21762177- vertices sorted according to their heights in the tree (first element is the root2178and the end of the list are the leaves). In other words, a topological sort of2179the vertices with respect to the spanning tree.21802181EXAMPLES::21822183sage: from admcycles import StableGraph2184sage: G = StableGraph([0,0,0], [[1,2],[3,4,5],[6,7]], [(1,3),(4,6),(5,7)])2185sage: G.spanning_tree()2186([None, (0, -1, 0), (1, -1, 1)], {2}, [0, 1, 2])2187sage: G.spanning_tree(1)2188([(1, 1, 0), None, (1, -1, 1)], {2}, [1, 0, 2])2189sage: G.spanning_tree(2)2190([(1, 1, 0), (2, 1, 1), None], {2}, [2, 1, 0])2191"""2192from collections import deque21932194nv = len(self._genera)2195ne = len(self._edges)21962197out_edges = [[] for _ in range(nv)]2198for i, (lu, lv) in enumerate(self._edges):2199u = self.vertex(lu)2200v = self.vertex(lv)2201out_edges[u].append((v, -1, i))2202out_edges[v].append((u, 1, i))22032204ancestors = [None] * nv2205ancestors[root] = None2206unseen = [True] * nv2207unseen[root] = False2208todo = deque()2209todo.append(root)2210extra_edges = set(range(ne))2211vertices = [root]22122213while todo:2214u = todo.popleft()2215for v, s, i in out_edges[u]:2216if unseen[v]:2217ancestors[v] = (u, s, i)2218todo.append(v)2219unseen[v] = False2220extra_edges.remove(i)2221vertices.append(v)22222223return ancestors, extra_edges, vertices22242225def cycle_basis(self):2226r"""2227Return a basis of the cycles as vectors of length the number of edges in the graph.22282229The coefficient at a given index in a vector corresponds to the edge2230of this index in the list of edges of the stable graph. The coefficient is2231+1 or -1 depending on the orientation of the edge in the cycle.22322233EXAMPLES::22342235sage: from admcycles import StableGraph2236sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])2237sage: G.cycle_basis()2238[(1, 1, 0, 0), (1, 0, -1, 1)]2239"""2240ne = len(self._edges)2241V = self._edge_module()22422243ancestors, extra_edges, _ = self.spanning_tree()2244basis = []22452246for i in extra_edges:2247vec = [0] * ne22482249# contribution of the edge2250vec[i] = 122512252lu, lv = self._edges[i]2253u = self.vertex(lu)2254v = self.vertex(lv)22552256# contribution of the path from root to u2257while ancestors[u] is not None:2258u, s, i = ancestors[u]2259vec[i] -= s22602261# contribution of the path from v to root2262while ancestors[v] is not None:2263v, s, i = ancestors[v]2264vec[i] += s22652266basis.append(V(vec))22672268return basis22692270def cycle_space(self):2271r"""2272Return the subspace of the edge module generated by the cycles.22732274EXAMPLES::22752276sage: from admcycles import StableGraph2277sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])2278sage: C = G.cycle_space()2279sage: C2280Free module of degree 4 and rank 2 over Integer Ring2281Echelon basis matrix:2282[ 1 0 -1 1]2283[ 0 1 1 -1]2284sage: G.flow_divergence(C.random_element()).is_zero()2285True2286"""2287return self._edge_module().submodule(self.cycle_basis())22882289def flow_divergence(self, flow):2290r"""2291Return the divergence of the given ``flow``.22922293The divergence at a given vertex is the sum of the flow on the outgoing2294edges at this vertex.22952296EXAMPLES::22972298sage: from admcycles import StableGraph2299sage: G = StableGraph([1,0,0], [[1,2],[3,4,5,6],[7,8]], [(1,3),(4,2),(5,8),(7,6)])2300sage: G.flow_divergence([1,1,1,1])2301(0, 0, 0)2302sage: G.flow_divergence([1,0,0,0])2303(1, -1, 0)2304"""2305flow = self._edge_module()(flow)2306deriv = self._vertex_module()()2307for i, (lu, lv) in enumerate(self._edges):2308u = self.vertex(lu)2309v = self.vertex(lv)2310deriv[u] += flow[i]2311deriv[v] -= flow[i]23122313return deriv23142315def flow_solve(self, vertex_weights):2316r"""2317Return a solution for the flow equation with given vertex weights.23182319EXAMPLES::23202321sage: from admcycles import StableGraph23222323sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])2324sage: flow = G.flow_solve((-3, 2, 1))2325sage: flow2326(-2, 0, -1, 0)2327sage: G.flow_divergence(flow)2328(-3, 2, 1)2329sage: div = vector((-34, 27, 7))2330sage: flow = G.flow_solve(div)2331sage: G.flow_divergence(flow) == div2332True2333sage: C = G.cycle_space()2334sage: G.flow_divergence(flow + C.random_element()) == div2335True23362337sage: G = StableGraph([0,0,0], [[1],[2,3],[4]], [(1,2),(3,4)])2338sage: G.flow_solve((-1, 0, 1))2339(-1, -1)2340sage: G.flow_divergence((-1, -1))2341(-1, 0, 1)2342sage: G.flow_solve((-1, 3, -2))2343(-1, 2)2344sage: G.flow_divergence((-1, 2))2345(-1, 3, -2)23462347TESTS::23482349sage: V = ZZ**42350sage: vectors = [V((-1, 0, 0, 1)), V((-3, 1, -2, 4)), V((5, 2, -13, 6))]2351sage: G1 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (5,6)])2352sage: G2 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (6,5)])2353sage: G3 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(2,1), (3,4), (6,5)])2354sage: G4 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (4,3), (5,6)])2355sage: for G in [G1,G2,G3,G4]:2356....: for v in vectors:2357....: flow = G.flow_solve(v)2358....: assert G.flow_divergence(flow) == v, (v,flow,G)23592360sage: V = ZZ**62361sage: 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)])2362sage: for _ in range(10):2363....: v0 = V.random_element()2364....: for u in V.basis():2365....: v = v0 - sum(v0) * u2366....: flow = G.flow_solve(v)2367....: assert G.flow_divergence(flow) == v, (v, flow)2368"""2369# NOTE: if we compute the divergence matrix, one can also use solve_right2370# directly. It might be faster on some large instances.2371nv = len(self._genera)2372if len(vertex_weights) != nv or sum(vertex_weights) != 0:2373raise ValueError("vertex_weights must have length nv and sum up to zero")23742375ancestors, _, vertices = self.spanning_tree()2376vertices.reverse() # move leaves first and root last2377vertices.pop() # remove the root23782379new_vertex_weights = self._vertex_module()(vertex_weights)2380if new_vertex_weights is vertex_weights and vertex_weights.is_mutable():2381new_vertex_weights = vertex_weights.__copy__()2382vertex_weights = new_vertex_weights23832384V = self._edge_module()2385vec = V()2386for u in vertices:2387v, s, i = ancestors[u]2388vec[i] += s * vertex_weights[u]2389vertex_weights[v] += vertex_weights[u]23902391return vec23922393def plot(self, vord=None, vpos=None, eheight=None):2394r"""2395Return a graphics object in which ``self`` is plotted.23962397If ``vord`` is ``None``, the method uses the default vertex order.23982399If ``vord`` is ``[]``, the parameter ``vord`` is used to give back the vertex order.24002401EXAMPLES::24022403sage: from admcycles import *2404sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])2405sage: G.plot()2406Graphics object consisting of 12 graphics primitives24072408TESTS::24092410sage: from admcycles import *2411sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])2412sage: vertex_order = []2413sage: P = G.plot(vord=vertex_order)2414sage: vertex_order2415[0, 1]2416"""2417# some parameters2418mark_dx = 1 # x-distance of different markings2419mark_dy = 1 # y-distance of markings from vertices2420mark_rad = 0.2 # radius of marking-circles2421v_dxmin = 1 # minimal x-distance of two vertices2422ed_dy = 0.7 # max y-distance of different edges between same vertex-pair24232424vsize = 0.4 # (half the) size of boxes representing vertices24252426if not vord:2427default_vord = list(range(len(self._genera)))2428if vord is None:2429vord = default_vord2430else:2431vord += default_vord2432reord_self = self.copy()2433reord_self.reorder_vertices(vord)24342435if not vpos:2436default_vpos = [(0, 0)]2437for i in range(1, len(self._genera)):2438default_vpos += [(default_vpos[i - 1][0] + mark_dx * (len(reord_self.list_markings(i - 1)) + 2 * len(reord_self.edges_between(2439i - 1, i - 1)) + len(reord_self.list_markings(i)) + 2 * len(reord_self.edges_between(i, i))) / ZZ(2) + v_dxmin, 0)]2440if vpos is None:2441vpos = default_vpos2442else:2443vpos += default_vpos24442445if not eheight:2446ned = {(i, j): 0 for i in range(len(reord_self._genera)) for j in range(i + 1, len(reord_self._genera))}2447default_eheight = {}2448for e in reord_self._edges:2449ver1 = reord_self.vertex(e[0])2450ver2 = reord_self.vertex(e[1])2451if ver1 != ver2:2452default_eheight[e] = abs(ver1 - ver2) - 1 + ned[(min(ver1, ver2), max(ver1, ver2))] * ed_dy2453ned[(min(ver1, ver2), max(ver1, ver2))] += 124542455if eheight is None:2456eheight = default_eheight2457else:2458eheight.update(default_eheight)24592460# now the drawing starts2461# vertices2462vertex_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]],2463color='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))]24642465# non-self edges2466edge_graph = []2467for e in reord_self._edges:2468ver1 = reord_self.vertex(e[0])2469ver2 = reord_self.vertex(e[1])2470if ver1 != ver2:2471x = (vpos[ver1][0] + vpos[ver2][0]) / ZZ(2)2472y = (vpos[ver1][1] + vpos[ver2][1]) / ZZ(2) + eheight[e]2473edge_graph += [bezier_path([[vpos[ver1], (x, y), vpos[ver2]]], color='black', zorder=1)]24742475marking_graph = []24762477for v in range(len(reord_self._genera)):2478se_list = reord_self.edges_between(v, v)2479m_list = reord_self.list_markings(v)2480v_x0 = vpos[v][0] - (2 * len(se_list) + len(m_list) - 1) * mark_dx / ZZ(2)24812482for e in se_list:2483edge_graph += [bezier_path([[vpos[v], (v_x0, -mark_dy),2484(v_x0 + mark_dx, -mark_dy), vpos[v]]], zorder=1)]2485v_x0 += 2 * mark_dx2486for l in m_list:2487marking_graph += [line([vpos[v], (v_x0, -mark_dy)], color='black', zorder=1) + circle((v_x0, -mark_dy), mark_rad, fill=True,2488facecolor='white', edgecolor='black', zorder=2) + text(repr(l), (v_x0, -mark_dy), fontsize=10, color='black', zorder=3)]2489v_x0 += mark_dx2490G = sum(marking_graph) + sum(edge_graph) + sum(vertex_graph)2491G.axes(False)2492return G24932494def _unicode_art_(self):2495"""2496Return unicode art for the stable graph ``self``.24972498EXAMPLES::24992500sage: from admcycles import *2501sage: A = StableGraph([1, 2],[[1, 2, 3], [4]],[(3, 4)])2502sage: unicode_art(A)2503╭────╮25043 42505╭┴─╮ ╭┴╮2506│1 │ │2│2507╰┬┬╯ ╰─╯25081225092510sage: A = StableGraph([333, 4444],[[1, 2, 3], [4]],[(3, 4)])2511sage: unicode_art(A)2512╭─────╮25133 42514╭┴──╮ ╭┴───╮2515│333│ │4444│2516╰┬┬─╯ ╰────╯25171225182519sage: B = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])2520sage: unicode_art(B)2521╭─────╮2522│╭╮ │2523135 22524╭┴┴┴╮ ╭┴╮2525│3 │ │5│2526╰───╯ ╰─╯25272528sage: C = StableGraph([3,5], [[3,1,5],[2,4]], [(1,2),(3,5)])2529sage: unicode_art(C)2530╭────╮2531╭─╮ │2532315 22533╭┴┴┴╮ ╭┴╮2534│3 │ │5│2535╰───╯ ╰┬╯253642537"""2538from sage.typeset.unicode_art import UnicodeArt2539N = self.num_verts()2540all_half_edges = self.halfedges()2541half_edges = {i: [j for j in legs_i if j in all_half_edges]2542for i, legs_i in zip(range(N), self._legs)}2543open_edges = {i: [j for j in legs_i if j not in all_half_edges]2544for i, legs_i in zip(range(N), self._legs)}2545left_box = [u' ', u'╭', u'│', u'╰', u' ']2546right_box = [u' ', u'╮ ', u'│ ', u'╯ ', u' ']2547positions = {}2548boxes = UnicodeArt()2549total_width = 02550for vertex in range(N):2551t = list(left_box)2552for v in half_edges[vertex]:2553w = str(v)2554positions[v] = total_width + len(t[0])2555t[0] += w2556t[1] += u'┴' + u'─' * (len(w) - 1)2557for v in open_edges[vertex]:2558w = str(v)2559t[4] += w2560t[3] += u'┬' + u'─' * (len(w) - 1)2561t[2] += str(self._genera[vertex])2562length = max(len(line) for line in t)2563for i in [0, 2, 4]:2564t[i] += u' ' * (length - len(t[i]))2565for i in [1, 3]:2566t[i] += u'─' * (length - len(t[i]))2567for i in range(5):2568t[i] += right_box[i]2569total_width += length + 22570boxes += UnicodeArt(t)2571num_edges = self.num_edges()2572matchings = [[u' '] * (1 + max(positions.values()))2573for _ in range(num_edges)]2574for i, (a, b) in enumerate(self.edges()):2575xa = positions[a]2576xb = positions[b]2577if xa > xb:2578xa, xb = xb, xa2579for j in range(xa + 1, xb):2580matchings[i][j] = u'─'2581matchings[i][xa] = u'╭'2582matchings[i][xb] = u'╮'2583for j in range(i + 1, num_edges):2584matchings[j][xa] = u'│'2585matchings[j][xb] = u'│'2586matchings = [u''.join(line) for line in matchings]2587return UnicodeArt(matchings + list(boxes))258825892590# This function is about to be deprecated. Instead use2591# - StableGraph.is_isomorphic2592# - StableGraph.automorphism_number259325942595# computes union of dictionaries25962597def dicunion(*dicts):2598return dict(itertools.chain.from_iterable(dct.items() for dct in dicts))259926002601def GraphIsom(G, H, check=False):2602# TODO: Insert quick hash-check if G,H can be isomorphic at all2603if (G.invariant() != H.invariant()):2604return []26052606Isomlist = [] # list of isomorphisms that will be returned2607IsoV = {} # working vertex-dictionary2608IsoL = {j: j for j in G.list_markings()} # working leg-dictionary26092610for j in G.list_markings():2611vG = G.vertex(j)2612vH = H.vertex(j)26132614# if vertices containing j have different genera or degrees in G, there is no automorphism2615# also if the markings give contradictory information where the vertices go, there is no automorphism2616if (G._genera[vG] != H._genera[vH]) or (len(G._legs[vG]) != len(H._legs[vH])) or (vG in IsoV and IsoV[vG] != vH):2617return []2618# otherwise add known association of vertices to IsoV2619IsoV[vG] = vH2620# Now vG and vH contain all information prescribed by markings, proceed to assign markingless vertices2621# Create dictionaries gdG, gdH associating to tuples (genus, degree) the indices of markingless vertices in G,H with those data2622gdG = {}2623gdH = {}26242625for v in range(len(G._genera)):2626if v not in IsoV:2627gd = (G._genera[v], len(G._legs[v]))2628if gd in gdG:2629gdG[gd] += [v]2630else:2631gdG[gd] = [v]2632for v in range(len(H._genera)):2633if v not in IsoV.values():2634gd = (H._genera[v], len(H._legs[v]))2635if gd in gdH:2636gdH[gd] += [v]2637else:2638gdH[gd] = [v]26392640if set(gdG) != set(gdH):2641return []26422643# list (for all keys (g,d) of gdG) of lists of all possible dictionary-bijections from gdG(gd) to gdH(gd)2644VertIm = []26452646for gd in gdG:2647if len(gdG[gd]) != len(gdH[gd]):2648return []2649P = Permutations(len(gdG[gd]))2650ind = list(range(len(gdG[gd])))2651VertIm += [[{gdG[gd][i]:gdH[gd][p[i] - 1] for i in ind} for p in P]]26522653# Now VertIm is a list of the form [[{0:0, 1:1},{0:1, 1:0}], [{2:2}]]2654# Iterate over all possible combinations2655for VI in itertools.product(*VertIm):2656continueloop = False2657IsoV2 = dicunion(IsoV, *VI) # IsoV2 is now complete dictionary of vertices26582659# 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 pair2660LegIm = []26612662# TODO: possibly want quick check that now graphs are actually isomorphic26632664# first look at loops of G2665for v in range(len(G._genera)):2666EdG = G.edges_between(v, v)26672668EdH = H.edges_between(IsoV2[v], IsoV2[v])2669if len(EdG) != len(EdH):2670continueloop = True2671break26722673LegImvv = []26742675# 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 goes2676P = Permutations(len(EdG))2677ran = list(range(len(EdG)))2678choic = len(EdG) * [[0, 1]]2679for p in P:2680for o in itertools.product(*choic):2681# 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}2682di = dicunion({EdG[i][0]: EdH[p[i] - 1][o[i]]2683for i in ran}, {EdG[i][1]: EdH[p[i] - 1][1 - o[i]] for i in ran})2684LegImvv += [di]2685LegIm += [LegImvv]2686if continueloop:2687continue26882689# now look at edges from v to w2690for v in range(len(G._genera)):2691if continueloop:2692break2693for w in range(v + 1, len(G._genera)):2694EdG = G.edges_between(v, w)2695EdH = H.edges_between(IsoV2[v], IsoV2[w])2696if len(EdG) != len(EdH):2697continueloop = True2698break26992700LegImvw = []27012702# P gives the ways to biject from EdG to EdH2703P = Permutations(len(EdG))2704ran = list(range(len(EdG)))2705for p in P:2706# Example: EdG=[(1,2),(3,4)], EdH=[(6,7),(9,11)], p=[2,1] -> {1:9, 2:11, 3:6, 4:7}2707di = dicunion({EdG[i][0]: EdH[p[i] - 1][0]2708for i in ran}, {EdG[i][1]: EdH[p[i] - 1][1] for i in ran})2709LegImvw += [di]2710LegIm += [LegImvw]27112712if continueloop:2713continue27142715# if program runs to here, then IsoV2 is a bijection of vertices respecting the markings,2716# LegIm is a list of lists of dictionaries giving bijections of non-marking legs that respect2717# the map of vertices and edges; it remains to add the various elements to IsoL and return them2718Isomlist += [[IsoV2, dicunion(IsoL, *LegDics)] for LegDics in itertools.product(*LegIm)]27192720if check and Isomlist:2721return Isomlist27222723return Isomlist272427252726