unlisted
ubuntu2004import itertools12# pylint does not know sage3from sage.structure.sage_object import SageObject # pylint: disable=import-error4from sage.matrix.constructor import matrix # pylint: disable=import-error5from sage.misc.flatten import flatten # pylint: disable=import-error6from sage.misc.cachefunc import cached_method # pylint: disable=import-error7from sage.rings.rational_field import QQ # pylint: disable=import-error8from sage.arith.functions import lcm # pylint: disable=import-error91011class EmbeddedLevelGraph(SageObject):12"""13LevelGraph inside a generalised stratum.1415Note that the points of the enveloping GeneralisedStratum are of the form16(i,j) where i is the component and j the index of sig of that component,17while the points of the level graph are numbers 1,...,n.1819Thus, dmp is a dictionary mapping integers to tuples of integers.2021Attributes:2223* LG (LevelGraph): underlying LevelGraph24* X (GeneralisedStratum): enveloping stratum25* dmp (dict): (bijective!) dictionary marked points of LG -> points of stratum26* dmp_inv (dict): inverse of dmp27* dlevels (dict): (bijective!) dictionary levels of LG -> new level numbering28* dlevels_inv (dict): inverse of dlevels29* top (GeneralisedStratum): (if self is a BIC) top component30* bot (GeneralisedStratum): (if self is a BIC) bottom component31* clutch_dict (dict): (if self is a BIC) dictionary mapping points of top32stratum to points of bottom stratum where there is an edge in self.33* emb_top (dict): (if self is a BIC) dictionary mapping points of stratum top34to the corresponding points of the enveloping stratum.35* emb_bot (dict): (if self is a BIC) dictionary mapping points of stratum bot36to the corresponding points of the enveloping stratum.37* automorphisms (list of list of dicts): automorphisms38* codim (int): codimension of LevelGraph in stratum39number_of_levels (int): Number of levels of self.4041Note that attempting to access any of the attributes top, bot, clutch_dict,42emb_top or emb_bot will raise a ValueError if self is not a BIC.43"""44def __init__(self, X, LG, dmp, dlevels):45"""46Initialises EmbeddedLevelGraph.4748Args:49LG (LevelGraph): underlying LevelGraph50X (GeneralisedStratum): enveloping stratum51dmp (dictionary): (bijective!) dictionary marked points of LG -> points of stratum52dlevels (dictionary): (bijective!) dictionary levels of LG -> new level numbering53"""54self.LG = LG55self.X = X56self.dmp = dmp57self.dmp_inv = {value: key for key, value in dmp.items()}58self.add_vertices_at_infinity()59self.dlevels = dlevels60self.dlevels_inv = {value: key for key, value in dlevels.items()}61self._top = None62self._bot = None63self._clutch_dict = None64self._emb_top = None65self._emb_bot = None66self._automorphisms = None67self._level = {}68self._ell = None69self.codim = self.LG.codim()70self.number_of_levels = len(set(self.dlevels.keys()))7172def __repr__(self):73return "EmbeddedLevelGraph(LG=%r,dmp=%r,dlevels=%r)" % (74self.LG, self.dmp, self.dlevels)7576def __str__(self):77return (78"Embedded Level Graph consisting of %s with point dictionary %s and level dictionary %s" %79(self.LG, self.dmp, self.dlevels))8081def explain(self):82"""83A more user-friendly display of __str__ :-)84"""85def _list_print(L):86if len(L) > 1:87s = ['s ']88for x in L[:-2]:89s.append('%r, ' % x)90s.append('%r ' % L[-2])91s.append('and %r.' % L[-1])92return ''.join(s)93else:94return ' %r.' % L[0]9596def _num(i):97if i == 1:98return 'one edge'99else:100return '%r edges' % i101print("LevelGraph embedded into stratum %s with:" % self.X)102LG = self.LG103for l in range(LG.numberoflevels()):104internal_l = LG.internal_level_number(l)105print("On level %r:" % l)106for v in LG.verticesonlevel(internal_l):107print("* A vertex (number %r) of genus %r" % (v, LG.genus(v)))108levels_of_mps = list(109set(LG.level_number(LG.levelofleg(leg)) for leg in self.dmp))110print("The marked points are on level%s" %111_list_print(sorted(levels_of_mps)))112print("More precisely, we have:")113for leg in self.dmp:114print(115"* Marked point %r of order %r on vertex %r on level %r" %116(self.dmp[leg],117LG.orderatleg(leg),118LG.vertex(leg),119LG.level_number(120LG.levelofleg(leg))))121print("Finally, we have %s. More precisely:" % _num(len(LG.edges)))122edge_dict = {e: (LG.vertex(e[0]), LG.vertex(e[1])) for e in LG.edges}123edge_dict_inv = {}124for k, v in edge_dict.items():125if v in edge_dict_inv:126edge_dict_inv[v].append(k)127else:128edge_dict_inv[v] = [k]129for e in edge_dict_inv:130print("* %s between vertex %r (on level %r) and vertex %r (on level %r) with prong%s" %131(_num(len(edge_dict_inv[e])),132e[0], LG.level_number(LG.levelofvertex(e[0])),133e[1], LG.level_number(LG.levelofvertex(e[1])),134# _write_prongs()135_list_print([LG.prong(ee) for ee in edge_dict_inv[e]])))136137def __eq__(self, other):138if not isinstance(other, EmbeddedLevelGraph):139return False140return self.LG == other.LG and self.dmp == other.dmp and self.dlevels == other.dlevels141142@cached_method143def is_bic(self):144return self.LG.is_bic()145146@property147def ell(self):148"""149If self is a BIC: the lcm of the prongs.150151Raises:152RuntimeError: raised if self is not a BIC.153154Returns:155int: lcm of the prongs.156"""157if self._ell is None:158if not self.is_bic():159raise RuntimeError("ell only defined for BICs!")160self._ell = lcm(self.LG.prongs.values())161return self._ell162163# Dawei's positivity coefficients164@property165def b(self):166if self.X._h0 > 1:167raise ValueError('Cannot compute b on disconnected stratum.')168g = self.X._sig_list[0].g169val = 0170# super inefficient, but probably good enough for now:171# take the underlying StableGraph and, for each edge,172# contract all other edges and check which graph we end up with:173stgraph = self.LG.stgraph174for e in self.LG.edges:175ee = self.LG.edges[:]176ee.remove(e)177curr_graph = stgraph.copy()178for contr in ee:179curr_graph.contract_edge(contr)180assert curr_graph.edges() == [e]181if len(curr_graph.genera()) == 2:182# compact type:183i = min(curr_graph.genera())184val += QQ(6 * i * (g - i)) / QQ((g + 3) * self.LG.prong(e))185else:186# irreducible187assert len(curr_graph.genera()) == 1188val += QQ(g + 1) / QQ((g + 3) * self.LG.prong(e))189return self.ell * val190191@property192def c(self):193return self.ell * (self.bot.kappa_EKZ -194self.X.kappa_EKZ * QQ(self.bot.N) / QQ(self.X.N))195######196197@property198def top(self):199if self._top is None:200self.split()201return self._top202203@property204def bot(self):205if self._bot is None:206self.split()207return self._bot208209@property210def clutch_dict(self):211if self._clutch_dict is None:212self.split()213return self._clutch_dict214215@property216def emb_bot(self):217if self._emb_bot is None:218self.split()219return self._emb_bot220221@property222def emb_top(self):223if self._emb_top is None:224self.split()225return self._emb_top226227def add_vertices_at_infinity(self):228"""229We add the vertices at infinity to the underlying_graph of self.LG.230231These are given by the residue conditions.232233More precisely: Recall that the underlying_graph of self.LG has vertices234and edges of self.LG stored in the form UG_vertex = (vertex number, genus, 'LG')235and edges of the underlying graph are of the form: (UG_vertex, UG_vertex, edge name)236We now add vertices 'at level infinity' by adding, for each res_cond of self.X237238* a UG_vertex called (i, 0, 'res') (where i is the index of the condition in res_cond239we are currently considering) and edges so that each residue240condition corresponds to an edge from the corresponding pole to some241residue at 'level infinity'. We store these in the form:242* (res_vertex, UG_vertex, resiedgej)243Here UG_vertex is the vertex of self.LG, in the form (vertex number, genus, 'LG'),244that res_vertex is attached to and j is the leg of that vertex (as a leg of self.LG!)245corresponding to the pole that resi should be attached to.246"""247# remove any that might already be there:248existing_residues = [v for v in self.LG.underlying_graph.vertices(sort=True)249if v[2] == 'res']250for v in existing_residues:251self.LG.underlying_graph.delete_vertex(v)252# add a vertex for every residue condition:253# TODO: remove duplicates?254edges = []255for i, rc in enumerate(self.X.res_cond):256v_name = (i, 0, 'res')257for p in rc:258e_name = 'res%redge%r' % (i, self.dmp_inv[p])259v_on_graph = self.LG.vertex(self.dmp_inv[p])260edges.append((self.LG.UG_vertex(v_on_graph), v_name, e_name))261self.LG.underlying_graph.add_edges(edges)262263@property264@cached_method265def residue_matrix_from_RT(self):266"""267The matrix associated to the residue conditions imposed by the residue theorem268on each vertex of self.269270Returns:271SAGE Matrix: matrix of residue conditions given by RT272"""273poles_by_vertex = {}274for p in self.X._polelist:275vertex = self.LG.vertex(self.dmp_inv[p])276try:277poles_by_vertex[vertex].append(p)278except KeyError:279poles_by_vertex[vertex] = [p]280rows = []281for v in poles_by_vertex:282rows.append([int(p in poles_by_vertex[v])283for p in self.X._polelist])284return matrix(QQ, rows)285286@property287@cached_method288def full_residue_matrix(self):289"""290Residue matrix with GRC conditions and RT conditions (for each vertex).291292OUTPUT:293294A matrix with number of poles columns and a row for each condition.295"""296M = self.X.residue_matrix()297if M:298M = M.stack(self.residue_matrix_from_RT)299else:300M = self.residue_matrix_from_RT301return M302303def residue_zero(self, pole):304"""305Check if the residue at pole is forced zero by residue conditions.306307NOTE: We DO include the RT on the vertices in this check!308309Args:310pole (tuple): pole (as a point (i,j) of self.X)311312Returns:313bool: True if forced zero, False otherwise.314"""315# add the equation corresponding to the residue at pole to the residue matrix316# and see if the rank changes:317i = self.X._polelist.index(pole)318res_vec = [[int(i == j) for j in range(len(self.X._polelist))]]319RM = self.full_residue_matrix320# RM = self.X.residue_matrix()321if RM:322stacked = RM.stack(matrix(res_vec))323return stacked.rank() == self.full_residue_matrix.rank()324# return stacked.rank() == self.X.residue_matrix().rank()325else:326return False327328def level(self, l):329"""330The generalised stratum on level l.331332Note that this is cached, i.e. on first call, it is stored in the dictionary333_level.334335INPUT:336337l: integer338The relative level number (0,...,codim)339340OUTPUT:341342The LevelStratum that is343344* a list of Signatures (one for each vertex on the level)345* a list of residue conditions, i.e. a list [res_1,...,res_r]346where each res_k is a list of tuples [(i_1,j_1),...,(i_n,j_n)]347where each tuple (i,j) refers to the point j (i.e. index) on the348component i and such that the residues at these points add up349to 0.350* a dictionary of legs, i.e. n -> (i,j) where n is the original351number of the point (on the LevelGraph self) and i is the352number of the component, j the index of the point in the signature tuple.353354Note that LevelStratum is a GeneralisedStratum together with355a leg dictionary. Here, we provide an additional attribute:356357* leg_orbits, a nested list giving the orbits of the points on the level358under the automorphism group of self.359"""360try:361LS = self._level[l]362except KeyError:363# for the residue conditions: We add the residue conditions from364# the enveloping stratum:365# We do this by passing those poles with residue forced366# zero as those to be ignored in the residue calculations367# performed by the LevelGraph:368# We have to translate them to points on self:369# Note that self.LG knows the "level at infinity"370excluded_poles = tuple(self.dmp_inv[p] for p in flatten(371self.X.res_cond, max_level=1))372LS = self.LG.stratum_from_level(l, excluded_poles=excluded_poles)373# add automorphism info374LS.leg_orbits = []375seen = set()376for leg in LS._leg_dict:377if leg in seen:378continue379curr_orbit = [LS._leg_dict[leg]]380for _v_map, l_map in self.automorphisms:381curr_orbit.append(LS._leg_dict[l_map[leg]])382seen.update([l_map[leg]])383LS.leg_orbits.append(list(set(curr_orbit))384) # remove duplicates385self._level[l] = LS386return LS387388def legs_are_isomorphic(self, leg, other_leg):389"""390Check if leg and other_leg are in the same Aut-orbit of self.391392Args:393leg (int): leg on self.LG394other_leg (int): leg on self.LG395396Raises:397RuntimeError: If leg is not in any aut-orbit of the level it should be on.398399Returns:400bool: True if they are in the same orbit of self.level(levelofleg),401False, otherwise.402403EXAMPLES::404405Note the asymmetric banana graph.406407The symmetric one has isomorphisms.408409Legs are isomorphic to themselves.410411It's symmetric.412413"""414level = self.LG.level_number(self.LG.levelofleg(leg))415other_level = self.LG.level_number(self.LG.levelofleg(other_leg))416if level != other_level:417return False418assert level == other_level419emb_leg = self.level(level)._leg_dict[leg]420emb_other_leg = self.level(level)._leg_dict[other_leg]421for orbit in self.level(level).leg_orbits:422if emb_leg in orbit:423return emb_other_leg in orbit424else:425raise RuntimeError(426"leg %r not in any orbit %r of %r" %427(leg, self.level(level).leg_orbits, self.level(level)))428429@cached_method430def edge_orbit(self, edge):431"""432The edge orbit of edge in self.433434raises a ValueError if edge is not an edge of self.LG435436INPUT:437438edge: tuple439An edge of ``self.LG``, i.e. tuple (start leg, end leg), where start440leg should not be on a lower level than end leg.441442OUTPUT:443444The set of edges in automorphism orbit of ``edge``.445"""446if edge not in self.LG.edges:447raise ValueError("%r is not an edge of %r!" % (edge, self))448s = set([edge])449for v_map, l_map in self.automorphisms:450new_edge = (l_map[edge[0]], l_map[edge[1]])451s.add(new_edge)452return s453454def len_edge_orbit(self, edge):455"""456Length of the edge orbit of edge in self.457458Args:459edge (tuple): edge of self.LG, i.e. tuple (start leg, end leg), where460start leg should not be on a lower level than end leg.461462Raises:463ValueError: if edge is not an edge of self.LG464465Returns:466int: length of the aut-orbit of edge.467468EXAMPLES::469470471Prongs influence the orbit length.472473"""474return len(self.edge_orbit(edge))475476def automorphisms_stabilising_legs(self, leg_tuple):477stabs = []478for v_map, l_map in self.automorphisms:479for l in leg_tuple:480if l_map[l] != l:481break482else: # no break483stabs.append(l_map)484return stabs485486def delta(self, i):487"""488Squish all levels except for i.489490Note that delta(1) contracts everything except top-level and that the491argument is interpreted via internal_level_number (i.e. a relative level number).492493Moreover, dlevels is set to map to 0 and -1(!).494495Args:496i (int): Level not to be squished.497498Returns:499EmbeddedLevelGraph: Embedded BIC (result of applying delta to the500underlying LevelGraph)501"""502newLG = self.LG.delta(i, quiet=True)503newdmp = self.dmp.copy()504# level_number is (positive!) relative level number.505newdlevels = {l: -newLG.level_number(l) for l in newLG.levels}506return EmbeddedLevelGraph(self.X, newLG, newdmp, newdlevels)507508def squish_vertical(self, level):509"""510Squish level crossing below level 'level'.511512Note that in contrast to the levelgraph method, we work with relative513level numbers here!514515Args:516level (int): relative (!) level number.517518Returns:519EmbeddedLevelGraph: Result of squishing.520521EXAMPLES::522523sage: from admcycles.diffstrata import *524sage: X=GeneralisedStratum([Signature((4,))])525sage: p = X.enhanced_profiles_of_length(4)[0][0]526sage: g = X.lookup_graph(p)527528lookup_graph uses the sorted profile (note that these do not have to be reduced!):529530sage: assert any(g.squish_vertical(0).is_isomorphic(G) for G in X.lookup(p[1:]))531sage: assert any(g.squish_vertical(1).is_isomorphic(G) for G in X.lookup(p[:1]+p[2:]))532sage: assert any(g.squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]+p[3:]))533sage: assert any(g.squish_vertical(3).is_isomorphic(G) for G in X.lookup(p[:3]))534535Squishing outside the range of levels does nothing:536537sage: assert g.squish_vertical(4) == g538539Recursive squishing removes larger parts of the profile:540541sage: assert any(g.squish_vertical(3).squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]))542"""543newLG = self.LG.squish_vertical(544self.LG.internal_level_number(level), quiet=True)545newdmp = self.dmp.copy()546# level_number is (positive!) relative level number.547newdlevels = {l: -newLG.level_number(l) for l in newLG.levels}548return EmbeddedLevelGraph(self.X, newLG, newdmp, newdlevels)549550def split(self):551"""552Splits embedded BIC self into top and bottom component.553554Raises a ValueError if self is not a BIC.555556OUTPUT:557558A dictionary consising of559560* the GeneralisedStratum self.X561* the top component LevelStratum562* the bottom component LevelStratum563* the clutching dictionary mapping ex-half-edges on564top to their partners on bottom (both as points in the565respective strata!)566* a dictionary embedding top into the stratum of self567* a dictionary embedding bot into the stratum of self568569Note that clutch_dict, emb_top and emb_bot are dictionaries between570points of strata, i.e. after applying dmp to the points!571"""572if not self.is_bic():573raise ValueError(574"Error: %s is not a BIC! Cannot be split into Top and Bottom component!" %575self)576self._top = self.level(0)577self._bot = self.level(1)578# To construct emb_top and emb_bot, we have to combine self.dmp with the579# the leg_dicts of top and bot.580# More precisely: emb_top is the composition of the inverse of the leg_dict581# of top, i.e. top.stratum_number, and self.dmp582# (giving a map from the points of top to the points of the enveloping583# stratum of self) and the same for bot.584# We implement this by iterating over the marked points of self on top level,585# which are exactly the keys of self.dmp that are on top level.586# Note that we make extra sure that we didn't mess up the level numbering by587# using the relative level numbering (where the top level is guaranteed to be 0588# and the bottom level is 1 (positive!)).589self._emb_top = {self._top.stratum_number(l): self.dmp[l]590for l in iter(self.dmp)591if self.LG.level_number(self.LG.levelofleg(l)) == 0}592self._emb_bot = {self._bot.stratum_number(l): self.dmp[l]593for l in iter(self.dmp)594if self.LG.level_number(self.LG.levelofleg(l)) == 1}595# Because this is a BIC, all edges of self are cut in this process596# and this is exactly the dictionary we must remember597# WARNING: Here we assume that e[0] is on top level and e[1] is on bottom598# This is assured by tidy_up, e.g. after initialisation!599# Note that all these dictionaries map points of GeneralisedStrata to each600# other so we must take the corresponding stratum_number!601self._clutch_dict = {602self._top.stratum_number(603e[0]): self._bot.stratum_number(604e[1]) for e in self.LG.edges}605return {'X': self.X, 'top': self._top, 'bottom': self._bot,606'clutch_dict': self._clutch_dict,607'emb_dict_top': self._emb_top, 'emb_dict_bot': self._emb_bot, }608609def is_legal(self):610"""611Check the R-GRC for self.612613Returns:614bool: result of R-GRC.615"""616# Check if any levels are empty:617# Note that this can only happen if self.X has simple poles (as we never618# have horizontal edges)619if list(self.X.simple_poles()):620if any(self.level(l).is_empty()621for l in range(self.number_of_levels)):622return False623# poles are excluded if they are contained in _any_ residue condition of the stratum.624# In particular, they are _not_ excluded if they are only restrained by625# the RT on some component!626poles_in_rc_stratum = flatten(self.X.res_cond, max_level=1)627poles_in_rc_graph = tuple(self.dmp_inv[p] for p in poles_in_rc_stratum)628return self.LG.is_legal(excluded_poles=poles_in_rc_graph, quiet=True)629630def standard_markings(self):631r"""632Construct a dictionary for relabelling the markings. A standard labelling will label the legs633of markings first and then the half edges. If the generalised stratum has only one component,634the standard label of a marking will be exactly the position of that marking in the signature.635636EXAMPLES::637638sage: from admcycles.diffstrata.generalisedstratum import Stratum639sage: X=Stratum((1,1))640sage: X.bics[0]641EmbeddedLevelGraph(LG=LevelGraph([1, 0],[[1, 2], [3, 4, 5, 6]],[(1, 5), (2, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, -1],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -1: -1})642sage: X.bics[0].standard_markings()643{1: 3, 2: 4, 3: 1, 4: 2, 5: 5, 6: 6}644645"""646n_list = [0 for i in range(self.X._h0)] # list of number of markings on each component of the stratum647for t in self.dmp_inv:648n_list[t[0]] += 1649650# list such that the j-th entry is the sum of numbers of651# markings on the components of smaller indices652n_sums = [0] + [sum(n_list[i] for i in range(j))653for j in range(1, self.X._h0)]654new_leg_dict = {} # the mapping dict for relabelling the legs655h = 1656for i in range(1, len(self.LG.poleorders) + 1):657if i in self.dmp:658new_leg_dict[i] = (self.dmp[i][1] +659n_sums[self.dmp[i][0]] + 1)660else:661new_leg_dict[i] = len(self.dmp) + h662h = h + 1663return new_leg_dict664665def relabel(self, legdict, tidyup=True):666r"""667Relabel the EmbeddedLevelGraph by a given dictionary.668669INPUT:670671- legdict (dict): A dictionary indicating the map from old markings672to the new ones673674EXAMPLES::675sage: from admcycles.diffstrata.generalisedstratum import Stratum676sage: X = Stratum((1,1))677sage: dict1={1: 3, 2: 4, 3: 1, 4: 2, 5: 5, 6: 6}678sage: X.bics[0].relabel(dict1)679EmbeddedLevelGraph(LG=LevelGraph([1, 0],[[3, 4], [1, 2, 5, 6]],[(3, 5), (4, 6)],{1: 1, 2: 1, 3: 0, 4: 0, 5: -2, 6: -2},[0, -1],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0, -1: -1})680681"""682newLG = self.LG.relabel(legdict, tidyup)683new_dmp = {legdict[i]: j for i, j in self.dmp.items()}684if tidyup:685new_dmp = {a: b for a, b in sorted(new_dmp.items())}686newEmbLG = EmbeddedLevelGraph(self.X, newLG, new_dmp, self.dlevels)687688return newEmbLG689690def is_isomorphic(self, other):691"""692Check if self and other are isomorphic (as EmbeddedLevelGraphs).693694Args:695other (EmbeddedLevelGraph): Graph to check isomorphism.696697Returns:698bool: True if there exists at least one isomorphism.699"""700# TODO: Maybe include a way to check against unembedded LGs701# TODO: Check embedding!702if not isinstance(other, EmbeddedLevelGraph):703return False704try:705next(self.isomorphisms(other))706return True707except StopIteration:708return False709710@property711def automorphisms(self):712"""713The automorphisms of self (as automorphisms of the underlying LevelGraph,714respecting the embedding, see doc of isomorphisms).715716OUTPUT:717718A list of pairs ``(map_of_vertices, map_of_legs)``. Both ``maps_of_vertices``719and ``map_of_legs`` are dictionaries.720"""721if not self._automorphisms:722self._automorphisms = list(self.isomorphisms(self))723return self._automorphisms724725def isomorphisms(self, other):726"""727Generator yielding the "next" isomorphism of self and other.728729Note that while this gives an "isomorphism" from self.LG to other.LG, this730is not necessarily an isomorphism of the LevelGraphs (the numbered points may731be permuted if this is "fixed" by the embedding).732733INPUT:734735other: EmbeddedLevelGraph736The (potentially) isomorphic EmbeddedLevelGraph.737738OUTPUT:739740An iterator going through isomorphisms. Each isomorphism is encoded by a741pair of dictionaries ``(vertex_map, leg_map)`` The dictionaries742``vertex_map`` (respectively ``leg_map``) is to the mapping of the743vertices (resp. legs) of ``self.LG`` to the vertices (resp. legs) of744``other.LG``.745"""746# Isomorphisms of EmbeddedLevelGraphs:747# An isomorphism of EmbeddedLevelGraph is a set of compatible level isomorphisms.748# We iterate through the isomorphisms on each level and yield whenever we find749# compatible levelisomorphisms for all levels.750# Note that we use dlevels for this, as these should be compatible.751# There are (at least) two ways in which this can be optimised:752# * don't go through the entire product before checking edge compatibility!753# * choose a smart ordering of levels (e.g. number of vertices)754isom_vertices = {}755isom_legs = {}756for level_isos in itertools.product(757*[self._level_iso(other, l) for l in self.dlevels.values()]):758for level_iso_v, level_iso_l in level_isos:759isom_vertices.update(level_iso_v)760isom_legs.update(level_iso_l)761# check edge compatibility762for e in self.LG.edges:763if (isom_legs[e[0]], isom_legs[e[1]]) not in other.LG.edges:764break765else: # no break766yield isom_vertices.copy(), isom_legs.copy()767768def _level_iso(self, other, l):769"""770Generator yielding the "next" isomorphism of level l of self and other.771772Here, l is a value of dlevels (this should be compatible).773774Note that we require the graph to have no horizontal edges, i.e. the level775contains no edges!776777TODO: * Maybe add future horizontal support?778* Maybe use relative level number instead? (this seems to give weird behaviour779right now...)780781Args:782other (EmbeddedLevelGraph): The embedded graph we are checking for isomorphism.783l (int): Level number (embedded into the stratum, i.e. value of dlevels).784785Yields:786tuple: the next isomorphism of levels:787dict: vertices of self.LG -> vertices of other.LG788dict: legs of self.LG -> legs of other.LG789"""790# Isomorphisms of levels:791# An isomorphism of levels consist of792# * a map: vertices to vertices793# * a map: legs to legs794# respecting:795# * the genus,796# * the number of legs on every vertex,797# * the order at every leg,798# * the marked points of the stratum (via dmp).799####800# First, we extract the data for level l from self and other.801# Note that we do not use stratum_from_level to avoid all the overhead.802# TODO: All this should be cached!!803l_self = self.LG.internal_level_number(l)804l_other = other.LG.internal_level_number(l)805# we need to be careful to distinguish the indices in the list of genera806# of the LevelGraph from the actual genera.807vv_self_idx = self.LG.verticesonlevel(l_self) # list of indices808vv_other_idx = other.LG.verticesonlevel(l_other) # list of indices809if len(vv_self_idx) != len(vv_other_idx):810return811vv_self = [self.LG.genus(i) for i in vv_self_idx] # list of genera812vv_other = [other.LG.genus(i) for i in vv_other_idx] # list of genera813# extract the legs: (nested lists)814legs_self = [self.LG.legsatvertex(v) for v in vv_self_idx]815legs_other = [other.LG.legsatvertex(v) for v in vv_other_idx]816# build dictionary: leg -> index in vv817leg_dict_self = {l: i for i, legs in enumerate(818legs_self) for l in legs}819leg_dict_other = {l: i for i, legs in enumerate(820legs_other) for l in legs}821if len(leg_dict_self) != len(leg_dict_other):822return823# for quick checks, we create sorted lists of the orders at each vertex824order_sorted_self = [sorted([self.LG.orderatleg(l) for l in legs])825for legs in legs_self]826order_sorted_other = [sorted([other.LG.orderatleg(l) for l in legs])827for legs in legs_other]828# We create the two maps as dictionaries:829# index of vv_self -> index of vv_other830isom_vert = {}831# leg number (on self.LG) -> leg number (on other.LG)832isom_legs = {}833# We also want to keep track of whom we've already mapped:834# source is a dictionary: genus -> list of indices of vv_self835source = {}836for i, g in enumerate(vv_self):837try:838source[g].append(i)839except KeyError:840source[g] = [i]841# target is a dictionary: genus -> list of indices of vv_other842target = {}843for i, g in enumerate(vv_other):844try:845target[g].append(i)846except KeyError:847target[g] = [i]848# for the legs we build copies of the nested lists to manipulate849legs_source = [legs[:] for legs in legs_self]850legs_target = [legs[:] for legs in legs_other]851# Next, we exclude some deal-breakers:852# * The same genera must appear.853if sorted(vv_self) != sorted(vv_other):854return855# * The same embedded points have to be on this level (they have to be856# mapped to each other!)857# In particular, this gives a part of the leg map (and thus also of the858# vertex map).859for p_self, p in self.dmp.items(860): # p is the "shared" point of the stratum861p_other = other.dmp_inv[p]862# If neither point is on this level, we continue:863if not (p_self in leg_dict_self or p_other in leg_dict_other):864continue865# The vertex of p_self must map to that of p_other.866# Three things can fail here:867# * only one of the two points is on this level.868if ((p_self in leg_dict_self and p_other not in leg_dict_other) or (869p_self not in leg_dict_self and p_other in leg_dict_other)):870return871v_self = leg_dict_self[p_self]872v_other = leg_dict_other[p_other]873# * the points are on incompatible vertices (genus or numbers/orders of legs!)874if (vv_self[v_self] != vv_other[v_other] or875len(legs_self[v_self]) != len(legs_other[v_other]) or876order_sorted_self[v_self] != order_sorted_other[v_other]):877return878# * two points are on the same vertex in one case, but on different vertices879# in the other. I.e. v_self is already being mapped somewhere other than v_other880# or v_other is already being mapped to (by someone else)881try:882if isom_vert[v_self] != v_other:883return884except KeyError: # v_self not being mapped yet, i.e. still in source885g = vv_other[v_other]886if v_other in target[g]: # make sure v_other is still a possible target887isom_vert[v_self] = v_other888source[g].remove(v_self)889target[g].remove(v_other)890else:891return892# now we can safely map the legs:893isom_legs[p_self] = p_other894# and remove them from source and target (so they won't be895# reassigned later)896legs_source[v_self].remove(p_self)897legs_target[v_other].remove(p_other)898# Next, we construct maps of the remaining vertices.899# For this, we use a small recursive function:900curr_v_map = {}901legal_v_maps = []902903def vertex_maps(sl, tl):904if not sl:905# all entries of tl should be None at this point:906assert all(tv is None for tv in tl)907legal_v_maps.append(curr_v_map.copy())908return909curr_v = sl.pop()910curr_legs = len(legs_self[curr_v])911# try to map curr_v to tl:912for i, tv in enumerate(tl):913# we temporarily set "hit" targets to None so we don't have to worry914# about indexing...915if tv is None:916continue917# check if legs _can_ be compatible:918if (curr_legs != len(legs_other[tv]) or919order_sorted_self[curr_v] != order_sorted_other[tv]):920continue921tl[i] = None922curr_v_map[curr_v] = tv923vertex_maps(sl, tl)924# undo925del curr_v_map[curr_v]926tl[i] = tv927# undo928sl.append(curr_v)929# the function for the legs is almost the same, just the condition is930# different:931curr_l_map = {}932legal_l_maps = []933934def leg_maps(sl, tl):935if not sl:936# all entries of tl should be None at this point:937assert all(tleg is None for tleg in tl)938legal_l_maps.append(curr_l_map.copy())939return940curr_l = sl.pop()941# try to map curr_l to tl:942for i, tleg in enumerate(tl):943# we temporarily set "hit" targets to None so we don't have to worry944# about indexing...945if tleg is None:946continue947# check if orders are compatible:948if self.LG.orderatleg(curr_l) == other.LG.orderatleg(tleg):949tl[i] = None950curr_l_map[curr_l] = tleg951leg_maps(sl, tl)952# undo953del curr_l_map[curr_l]954tl[i] = tleg955# undo956sl.append(curr_l)957# Now we build the list of all vertex isomorphisms going through the958# vertices by genus959v_isom_list = []960for g in source:961legal_v_maps = [] # will get filled by vertex_maps962vertex_maps(source[g], target[g])963v_isom_list.append(legal_v_maps[:]) # copy!964# v_isom_list is now a nested list of maps for each genus.965# the product consists of tuples, one map for every genus.966for v_maps in itertools.product(*v_isom_list):967for v_map in v_maps:968# this overwrites exactly the vertices in source.969isom_vert.update(v_map)970# Finally, the returned vertex map should use the indexing of the971# LevelGraph, not of the level:972return_isom_vert = {vv_self_idx[k]: vv_other_idx[v]973for k, v in isom_vert.items()}974# Now we build all leg maps, again as a product of all maps at vertices.975# Note: This also included the previously assigned vertices (with976# marked points...)977l_isom_list = []978for v in isom_vert:979# Construct leg maps:980# We work with legs_source and legs_target, i.e. the list981# of legs with the marked points removed.982legal_l_maps = []983leg_maps(legs_source[v], legs_target[isom_vert[v]])984l_isom_list.append(legal_l_maps[:]) # copy!985for l_maps in itertools.product(*l_isom_list):986for l_map in l_maps:987isom_legs.update(l_map)988yield return_isom_vert.copy(), isom_legs.copy()989990991