Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
180 views
unlisted
ubuntu2004
1
from copy import deepcopy
2
3
# pylint does not know sage
4
from sage.structure.sage_object import SageObject # pylint: disable=import-error
5
from sage.rings.integer_ring import ZZ # pylint: disable=import-error
6
from sage.misc.flatten import flatten # pylint: disable=import-error
7
from sage.functions.generalized import sign # pylint: disable=import-error
8
from sage.misc.cachefunc import cached_method # pylint: disable=import-error
9
from sage.graphs.graph import Graph # pylint: disable=import-error
10
from sage.plot.text import text # pylint: disable=import-error
11
12
from admcycles.admcycles import GraphIsom
13
from admcycles.stable_graph import StableGraph as stgraph
14
from admcycles.diffstrata.sig import kSignature
15
16
17
class KLevelGraph(SageObject):
18
r"""
19
Create a (stable) level graph for a k-differential.
20
21
.. NOTE::
22
23
This is a low-level class and should NEVER be invoked directly!
24
Preferably, ``EmbeddedLevelGraphs`` should be used and these should be
25
generated automatically by ``Stratum`` (or ``GeneralisedStratum``).
26
27
.. NOTE::
28
29
We do not inherit from stgraph/StableGraph anymore, as KLevelGraphs
30
should be static objects!
31
32
Extends admcycles stgraph to represent a level graph as a stgraph,
33
i.e. a list of genera, a list of legs and a list of edges, plus a list of
34
poleorders for each leg and a list of levels for each vertex.
35
36
Note that the class will warn if the data is not admissible, i.e. if the
37
graph is not stable or the pole orders at separating nodes across levels do
38
not add up to -2 or -1 on the same level (unless this is suppressed with
39
quiet=True).
40
41
INPUT:
42
43
genera : list
44
List of genera of the vertices of length m.
45
46
legs : list
47
List of length m, where ith entry is list of legs attached to vertex i.
48
By convention, legs are unique positive integers.
49
50
edges : list
51
List of edges of the graph. Each edge is a 2-tuple of legs.
52
53
poleorders : dictionary
54
Dictionary of the form leg number : poleorder
55
56
levels : list
57
List of length m, where the ith entry corresponds to the level of
58
vertex i.
59
By convention, top level is 0 and levels go down by 1, in particular
60
they are NEGATIVE numbers!
61
62
k : int
63
The order of the differential.
64
65
quiet : optional boolean (default = False)
66
Suppresses most error messages.
67
68
ALTERNATIVELY, the pole orders can be supplied as a list by calling
69
KLevelGraph.fromPOlist:
70
poleorders : list
71
List of order of the zero (+) or pole (-) at each leg. The ith element
72
of the list corresponds to the order at the leg with the marking i+1
73
(because lists start at 0 and the legs are positive integers).
74
75
EXAMPLES:
76
77
Creating a level graph with three components on different levels of genus 1,
78
3 and 0. The bottom level has a horizontal node.::
79
80
sage: from admcycles.diffstrata.klevelgraph import KLevelGraph
81
sage: G = KLevelGraph.fromPOlist([1,3,0],[[1,2],[3,4,5],[6,7,8,9]],[(2,3),(5,6),(7,8)],[2,-2,0,6,-2,0,-1,-1,0],[-2,-1,0],1); G # doctest: +SKIP
82
KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,True)
83
84
or alternatively::
85
86
sage: KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,quiet=True)
87
KLevelGraph([1, 3, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9]],[(3, 2), (6, 5), (7, 8)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0},[-2, -1, 0],1,True)
88
89
Creating a level graph with two components on different levels of genus 1 and 1 and a quadratic differential.::
90
91
sage: KLevelGraph.fromPOlist([1,1],[[1],[2,3]],[(1,2)],[0,-4,4],[0,-1],2)
92
KLevelGraph([1, 1],[[1], [2, 3]],[(1, 2)],{1: 0, 2: -4, 3: 4},[0, -1],2,True)
93
94
We get a warning if the graph has non-stable components: (not any more ;-))::
95
96
sage: G = KLevelGraph.fromPOlist([1,3,0,0],[[1,2],[3,4,5],[6,7,8,9],[10]],[(2,3),(5,6),(7,8),(9,10)],[2,-2,0,6,-2,0,-1,-1,0,-2],[-3,-2,0,-1],1); G # doctest: +SKIP
97
Warning: Component 3 is not stable: g = 0 but only 1 leg(s)!
98
Warning: Graph not stable!
99
KLevelGraph([1, 3, 0, 0],[[1, 2], [3, 4, 5], [6, 7, 8, 9], [10]],[(3, 2), (6, 5), (7, 8), (9, 10)],{1: 2, 2: -2, 3: 0, 4: 6, 5: -2, 6: 0, 7: -1, 8: -1, 9: 0, 10: -2},[-3, -2, 0, -1],1,True)
100
"""
101
def __init__(self, genera, legs, edges,
102
poleorders, levels, k, quiet=False):
103
checks = False
104
if len(genera) != len(legs):
105
raise ValueError('genera and legs must have the same length')
106
self.genera = genera
107
self.legs = legs
108
self.edges = edges
109
self.poleorders = poleorders
110
self.levels = levels
111
# the signature consists of the marked points that are not half-edges
112
sig_list = tuple(poleorders[l] for l in self.list_markings())
113
self.sig = kSignature(sig_list, k)
114
# associated stgraph...
115
self._stgraph = None
116
self._init_more() # initiate some things and make sure all is in order
117
if checks:
118
self.checkadmissible(quiet)
119
self._has_long_edge = None
120
self._is_long = {}
121
self._is_bic = None
122
123
@classmethod
124
def fromPOlist(cls, genera, legs, edges,
125
poleordersaslist, levels, k, quiet=False):
126
"""
127
This gives a KLevelGraph where the poleorders are given as a list, not
128
directly as a dictionary.
129
"""
130
# translate poleorder list to dictionary:
131
sortedlegs = sorted(flatten(legs))
132
if len(sortedlegs) != len(poleordersaslist):
133
raise ValueError("Numbers of legs and pole orders do not agree!" +
134
" Legs: " + repr(len(sortedlegs)) +
135
" Pole orders: " + repr(len(poleordersaslist)))
136
poleorderdict = {sortedlegs[i]: poleordersaslist[i]
137
for i in range(len(sortedlegs))}
138
return cls(genera, legs, edges, poleorderdict, levels, k, quiet)
139
140
def __repr__(self):
141
return "KLevelGraph(" + self.input_as_string() + ")"
142
143
def __hash__(self):
144
return hash(repr(self))
145
146
def __str__(self):
147
return (
148
"KLevelGraph " +
149
repr(
150
self.genera) +
151
' ' +
152
repr(
153
self.legs) +
154
' ' +
155
repr(
156
self.edges) +
157
' ' +
158
repr(
159
self.poleorders) +
160
' ' +
161
repr(
162
self.levels) +
163
' ' +
164
repr(
165
self.k))
166
167
def input_as_string(self):
168
"""
169
return a string that can be given as argument to __init__ to give self
170
"""
171
return (repr(self.genera) + ',' + repr(self.legs) + ',' +
172
repr(self.edges) + ',' + repr(self.poleorders) + ',' +
173
repr(self.levels) + ',' + repr(self.sig.k) + ',True')
174
175
def __eq__(self, other):
176
if not isinstance(other, KLevelGraph):
177
return False
178
return (
179
self.genera == other.genera) and (
180
self.levels == other.levels) and (
181
self.legs == other.legs) and (
182
set(
183
self.edges) == set(
184
other.edges)) and (
185
self.poleorders == other.poleorders) and (
186
self.sig.k == other.sig.k)
187
188
# reimplementing the admcycles stuff we use:
189
@cached_method
190
def g(self):
191
genus = sum(self.genera) + len(self.edges) - \
192
len(self.genera) + ZZ.one()
193
assert genus == self.sig.g, "Signature %r does not match %r!" % (
194
self.sig, self)
195
return genus
196
197
@property
198
def stgraph(self):
199
if self._stgraph is None:
200
self._stgraph = stgraph([int(g) for g in self.genera],
201
[[int(l) for l in leg_list]
202
for leg_list in self.legs],
203
self.edges)
204
return self._stgraph
205
206
@cached_method
207
def list_markings(self, v=None):
208
r"""
209
Return the list of markings (non-edge legs) of self at vertex v.
210
"""
211
if v is None:
212
return tuple([j for v in range(len(self.genera))
213
for j in self.list_markings(v)])
214
s = set(self.legs[v])
215
for e in self.edges:
216
s -= set(e)
217
return tuple(s)
218
219
##########################################################
220
# Interface:
221
# --- provide a series of methods that translate various data
222
# (maybe this can be optimised by smart caching later, so only
223
# these should be used...)
224
# up to now, we have:
225
# * vertex : (overloaded from admcycles)
226
# INPUT : leg
227
# returns : vertex of leg (index of genera)
228
# * vertex_list :
229
# returns : list of vertices (index of genera)
230
# * edges_list :
231
# returns : list of edges
232
# * levelofvertex :
233
# INPUT : index of genera
234
# returns : level number (negative)
235
# * levelofleg :
236
# INPUT : leg label (given as positive integer)
237
# returns : level number (negative)
238
# * orderatleg :
239
# INPUT : leg label (given as positive integer)
240
# returns : poleorder (integer)
241
# * ordersonvertex :
242
# INPUT : index of genera
243
# returns : list of poleorders (integers)
244
# * verticesonlevel :
245
# INPUT : level number (negative)
246
# returns : list of indices of genera
247
# * edgesatvertex :
248
# INPUT : index of genera
249
# returns : list of edges
250
# * legsatvertex :
251
# INPUT : index of genera
252
# returns : list of legs
253
# * simplepolesatvertex :
254
# INPUT : index of genera
255
# returns : list of legs with pole order -1
256
# * genus :
257
# INPUT : index of genera
258
# returns : genus
259
# * codim :
260
# returns : codimension
261
# * is_bic :
262
# returns : boolean
263
# * edgesatlevel :
264
# INPUT : level number (negative)
265
# returns : list of edges with at least one node at that level (or horizontal)
266
# * horizontaledgesatlevel :
267
# INPUT : level number (negative)
268
# returns : list of horizontal edges
269
# * nextlowerlevel :
270
# INPUT : level number
271
# returns : level number of next lower level
272
# * lowest_level :
273
# returns : level number of lowest level
274
# * is_horizontal :
275
# INPUT : edge (or none for whole graph)
276
# returns : boolean success
277
# * has_loop :
278
# INPUT : vertex
279
# returns : boolean success
280
# * edgesgoingupfromlevel :
281
# INPUT : level
282
# returns : list of edges with e[1] on level
283
# * verticesabove :
284
# INPUT : level
285
# returns : list of vertices with level > l
286
# * edgesabove :
287
# INPUT : level
288
# returns : list of edges with level of each vertex > l
289
# * crosseslevel :
290
# INPUT : edge, level
291
# returns : boolean
292
# * edgesgoingpastlevel :
293
# INPUT : level
294
# returns : list of edges with start level > l and
295
# end level < l
296
#########################################################
297
@cached_method
298
def vertex(self, leg):
299
"""
300
The vertex (as index of genera) where leg is located.
301
302
Args:
303
leg (int): leg
304
305
Returns:
306
int: index of genera
307
"""
308
return self.legdict[leg]
309
310
@cached_method
311
def vertex_list(self):
312
return list(range(len(self.genera)))
313
314
@cached_method
315
def edges_list(self):
316
return self.edges
317
318
@cached_method
319
def levelofvertex(self, v):
320
return self.levels[v]
321
322
@cached_method
323
def levelofleg(self, leg):
324
return self.levelofvertex(self.vertex(leg))
325
326
@cached_method
327
def orderatleg(self, leg):
328
return self.poleorders[leg]
329
330
@cached_method
331
def ordersonvertex(self, v):
332
return [self.orderatleg(leg) for leg in self.legsatvertex(v)]
333
334
@cached_method
335
def verticesonlevel(self, level):
336
return [v for v in range(len(self.genera))
337
if self.levelofvertex(v) == level]
338
339
@cached_method
340
def edgesatvertex(self, v):
341
return [e for e in self.edges if self.vertex(e[0]) == v or
342
self.vertex(e[1]) == v]
343
344
@cached_method
345
def legsatvertex(self, v):
346
return self.legs[v]
347
348
@cached_method
349
def is_halfedge(self, leg):
350
"""
351
Check if leg has an edge attached to it.
352
"""
353
return any(e[0] == leg or e[1] == leg for e in self.edges)
354
355
@cached_method
356
def simplepolesatvertex(self, v):
357
return [l for l in self.legsatvertex(v) if self.orderatleg(l) == -1]
358
359
@cached_method
360
def genus(self, v=None):
361
"""
362
Return the genus of vertex v.
363
364
If v is None, return genus of the complete KLevelGraph.
365
"""
366
if v is None:
367
return self.g() # from admcycles
368
else:
369
return self.genera[v]
370
371
@cached_method
372
def numberoflevels(self):
373
return len(set(self.levels))
374
375
def is_bic(self):
376
if self._is_bic is None:
377
self._is_bic = self.numberoflevels() == 2 and not self.is_horizontal()
378
return self._is_bic
379
380
@cached_method
381
def codim(self):
382
"""
383
Return the codim = No. of levels of self + horizontal edges.
384
"""
385
return (self.numberoflevels() - 1 +
386
sum(1 for e in self.edges if self.is_horizontal(e)))
387
388
@cached_method
389
def edgesatlevel(self, level):
390
"""
391
Return list of edges with at least one node at level.
392
"""
393
return [e for e in self.edges if self.levelofleg(e[0]) == level or
394
self.levelofleg(e[1]) == level]
395
396
@cached_method
397
def horizontaledgesatlevel(self, level):
398
return [e for e in self.edgesatlevel(level) if self.is_horizontal(e)]
399
400
@cached_method
401
def nextlowerlevel(self, l):
402
"""
403
Return the next lower level number or False if no legal level
404
(e.g. lowest level) is given as input.
405
406
Point of discussion: Is it better to tidy up the level numbers
407
to be directly ascending or not?
408
409
Pro tidy: * this becomes obsolete ;)
410
* easier to check isomorphisms?
411
Con tidy: * we "see" where we came from
412
* easier to undo/glue back in after cutting
413
"""
414
try:
415
llindex = self.sortedlevels.index(l) - 1
416
except ValueError:
417
return False
418
if llindex == -1:
419
return False
420
else:
421
return self.sortedlevels[llindex]
422
423
@cached_method
424
def internal_level_number(self, i):
425
"""
426
Return the internal i-th level, e.g.
427
428
self.levels = [0,-2,-5,-3]
429
430
then
431
432
internal_level_number(0) is 0
433
internal_level_number(1) is -2
434
internal_level_number(2) is -3
435
internal_level_number(3) is -4
436
437
Returns None if the level does not exist.
438
"""
439
reference_levels = list(reversed(sorted(list(set(self.levels)))))
440
i = abs(i)
441
if i >= len(reference_levels):
442
return None
443
else:
444
return reference_levels[i]
445
446
@cached_method
447
def level_number(self, l):
448
"""
449
Return the relative level number (positive!) of l, e.g.
450
451
self.levels = [0,-2,-5,-3]
452
453
then
454
455
level_number(0) is 0
456
level_number(-2) is 1
457
level_number(-3) is 2
458
level_number(-5) is 3
459
460
Returns None if the level does not exist.
461
"""
462
reference_levels = list(reversed(sorted(list(set(self.levels)))))
463
try:
464
return reference_levels.index(l)
465
except ValueError:
466
return None
467
468
@cached_method
469
def is_long(self, e):
470
"""
471
Check if edge e is long, i.e. passes through more than one level.
472
"""
473
try:
474
return self._is_long[e]
475
except KeyError:
476
il = abs(self.level_number(self.levelofleg(
477
e[0])) - self.level_number(self.levelofleg(e[1]))) > 1
478
self._is_long[e] = il
479
return il
480
481
@cached_method
482
def has_long_edge(self):
483
if self._has_long_edge is None:
484
for e in self.edges:
485
if self.is_long(e):
486
self._has_long_edge = True
487
break
488
else:
489
self._has_long_edge = False
490
return self._has_long_edge
491
492
@cached_method
493
def lowest_level(self):
494
return self.sortedlevels[0]
495
496
@cached_method
497
def is_horizontal(self, e=None):
498
"""
499
Check if edge e is a horizontal edge or if self has a horizontal edge.
500
"""
501
if e is None:
502
return any(self.is_horizontal(e) for e in self.edges)
503
if e not in self.edges:
504
print("Warning: " + repr(e) + " is not an edge of this graph!")
505
return False
506
return self.levelofleg(e[0]) == self.levelofleg(e[1])
507
508
@cached_method
509
def has_loop(self, v):
510
"""
511
Check if vertex v has a loop.
512
"""
513
return any(self.vertex(e[0]) == self.vertex(e[1])
514
for e in self.edgesatvertex(v))
515
516
@cached_method
517
def edgesgoingupfromlevel(self, l):
518
"""
519
Return the edges going up from level l.
520
521
This uses that e[0] is not on a lower level than e[1]!
522
"""
523
return [e for e in self.edges if self.levelofleg(e[1]) == l and
524
not self.is_horizontal(e)]
525
526
@cached_method
527
def verticesabove(self, l):
528
"""
529
Return list of all vertices above level l.
530
531
If l is None, return all vertices.
532
"""
533
if l is None:
534
return list(range(len(self.genera)))
535
return [v for v in range(len(self.genera))
536
if self.levelofvertex(v) > l]
537
538
@cached_method
539
def edgesabove(self, l):
540
"""
541
Return a list of all edges above level l, i.e. start and end vertex
542
have level > l.
543
"""
544
return [e for e in self.edges
545
if self.levelofleg(e[0]) > l and self.levelofleg(e[1]) > l]
546
547
@cached_method
548
def crosseslevel(self, e, l):
549
"""
550
Check if e crosses level l (i.e. starts > l and ends <= l)
551
"""
552
return self.levelofleg(e[0]) > l and self.levelofleg(e[1]) <= l
553
554
@cached_method
555
def edgesgoingpastlevel(self, l):
556
"""
557
Return list of edges that go from above level l to below level l.
558
"""
559
return [e for e in self.edges
560
if self.levelofleg(e[0]) > l > self.levelofleg(e[1])]
561
#########################################################
562
# end interface
563
#########################################################
564
565
#########################################################
566
# Checks
567
#########################################################
568
@cached_method
569
def checkadmissible(self, quiet=False):
570
"""
571
Run all kinds of checks on the level graph. Currently:
572
* Check if orders on each komponent add up to k*(2g-2)
573
* Check if graph is stable
574
* Check if orders at edges sum to -k*2
575
* Check if orders respect level crossing
576
"""
577
admissible = True
578
if not self.checkorders(quiet):
579
if not quiet:
580
print("Warning: Illegal orders!")
581
admissible = False
582
if not self.is_stable(quiet):
583
if not quiet:
584
print("Warning: Graph not stable!")
585
admissible = False
586
if not self.checkedgeorders(quiet):
587
if not quiet:
588
print("Warning: Illegal orders at a node!")
589
admissible = False
590
return admissible
591
592
@cached_method
593
def checkorders(self, quiet=False):
594
"""
595
Check if the orders add up to k*(2g-2) on each component.
596
"""
597
for v, g in enumerate(self.genera):
598
if sum(self.ordersonvertex(v)) != self.sig.k * (2 * g - 2):
599
if not quiet:
600
print("Warning: Component " +
601
repr(v) +
602
" orders add up to " +
603
repr(sum(self.ordersonvertex(v))) +
604
" but component is of genus " +
605
repr(g))
606
return False
607
return True
608
609
@cached_method
610
def is_stable(self, quiet=False):
611
"""
612
Check if graph is stable.
613
"""
614
# total graph:
615
e = 2 * self.genus() - 2 + len(self.leglist)
616
if e < 0:
617
if not quiet:
618
print("Warning: Total graph not stable! 2g-2+n = " + repr(e))
619
return False
620
621
# components:
622
for v in range(len(self.genera)):
623
if 3 * self.genus(v) - 3 + len(self.legsatvertex(v)) < 0:
624
if not quiet:
625
print("Warning: Component " +
626
repr(v) +
627
" is not stable: g = " +
628
repr(self.genus(v)) +
629
" but only " +
630
repr(len(self.legsatvertex(v))) +
631
" leg(s)!")
632
return False
633
return True
634
635
@cached_method
636
def checkedgeorders(self, quiet=False):
637
"""
638
Check that the orders at nodes (i.e. edges) sum to -k*2 and that lower
639
level has lower order.
640
"""
641
for e in self.edges:
642
orders = self.orderatleg(e[0]) + self.orderatleg(e[1])
643
if orders != -self.sig.k * 2:
644
if not quiet:
645
print(
646
"Warning: Orders at edge " +
647
repr(e) +
648
" add up to " +
649
repr(orders) +
650
" instead of -k*2!")
651
return False
652
# iff the pole order at e[0] is > the poleorder at e[1], e[0]
653
# should be on a higher level than e[1]
654
if (sign(self.orderatleg(e[0]) - self.orderatleg(e[1])) !=
655
sign(self.levelofleg(e[0]) - self.levelofleg(e[1]))):
656
if not quiet:
657
print("Warning: Orders at edge " + repr(e) +
658
" do not respect level crossing!")
659
print("Poleorders are",
660
(self.orderatleg(e[0]), self.orderatleg(e[1])),
661
"but levels are",
662
(self.levelofleg(e[0]), self.levelofleg(e[1]))
663
)
664
return False
665
return True
666
667
#################################################################
668
# End checks
669
#################################################################
670
671
#################################################################
672
# Cleanup functions
673
# USE ONLY IN __init__!!! KLevelGraphs should be static!!!
674
#################################################################
675
def _init_more(self):
676
"""
677
This should be used _only_ for initialisation!
678
679
Make sure everything is in order, in particular:
680
* sortedlevels is fine
681
* leglist is fine
682
* legdict is fine
683
* maxleg is fine
684
* maxlevel is fine
685
* underlying graph is fine
686
"""
687
self.sortedlevels = sorted(self.levels)
688
# legs is a nested list:
689
self.leglist = flatten(self.legs)
690
# legs as dictionary
691
self.legdict = {l: v for v in range(len(self.genera))
692
for l in self.legs[v]}
693
self.maxleg = max([max(j + [0]) for j in self.legs])
694
# maxlevel is named a bit misleading, as it is the LOWEST level
695
# (the max of the absolute values...)
696
self.maxlevel = max([abs(l) for l in self.levels])
697
# construct the "underlying graph" (as a sage graph)
698
# the edges are labeled by their "real" name,
699
# the vertices are of the form (i,g_i,'LG')
700
# NOTE: In the case of an EmbeddedLevelGraph, vertices "at infinity" might
701
# be added to this!
702
self.underlying_graph = Graph([[self.UG_vertex(i) for i in range(len(self.genera))],
703
[(self.UG_vertex(self.vertex(e[0])), self.UG_vertex(self.vertex(e[1])), e)
704
for e in self.edges]],
705
format='vertices_and_edges', loops=True, multiedges=True)
706
707
def UG_vertex(self, v):
708
"""
709
Vertex of the underlying graph corresponding to v.
710
711
Args:
712
v (int): vertex number (index of self.genera)
713
714
Returns:
715
tuple: tuple encoding the vertex of the Underlying Graph.
716
"""
717
return (v, self.genera[v], 'LG')
718
719
#################################################################
720
# Extract a subgraph
721
#################################################################
722
def extract(self, vertices, edges):
723
"""
724
Extract the subgraph of self (as a KLevelGraph) consisting of vertices
725
(as list of indices) and edges (as list of edges).
726
727
Returns the levelgraph consisting of vertices, edges and all legs on
728
vertices (of self) with their original poleorders and the original
729
level structure.
730
"""
731
newvertices = deepcopy([self.genera[i] for i in vertices])
732
newlegs = deepcopy([self.legs[i] for i in vertices])
733
newedges = deepcopy(edges)
734
newpoleorders = deepcopy({l: self.orderatleg(l)
735
for l in flatten(newlegs)})
736
newlevels = deepcopy([self.levels[i] for i in vertices])
737
return KLevelGraph(newvertices, newlegs, newedges,
738
newpoleorders, newlevels, self.sig.k)
739
740
def levelGraph_from_subgraph(self, G):
741
"""
742
Returns the KLevelGraph associated to a subgraph of underlying_graph
743
(with the level structure induced by self)
744
"""
745
vertex_list = [v[0] for v in G.vertices(sort=True) if v[2] == 'LG']
746
return self.extract(vertex_list, G.edge_labels())
747
748
def UG_vertices_above_level(self, l):
749
"""
750
The vertices above (internal) level l (including possible vertices at infinity).
751
752
Used for checking residue conditions.
753
754
Args:
755
l (int): internal level number
756
757
Returns:
758
list: list of vertices of self.underlying_graph
759
"""
760
# note that verticesabove checks for >l
761
vertices_above = [self.UG_vertex(i) for i in self.verticesabove(l)]
762
vertices_above += [v for v in self.underlying_graph.vertices(sort=True)
763
if v[2] == 'res']
764
return vertices_above
765
766
def UG_above_level(self, l):
767
"""
768
Subgraph of Underlying Graph (including vertices at infinity) (strictly) above
769
(relative!) level l.
770
771
Args:
772
l (int): relative level number
773
774
Returns:
775
SAGE Graph: subgraph of self.underlying_graph with all vertices strictly
776
above level l.
777
"""
778
internal_l = self.internal_level_number(l)
779
vertices = self.UG_vertices_above_level(internal_l)
780
abovegraph = self.underlying_graph.subgraph(vertices)
781
return abovegraph
782
783
def UG_without_infinity(self):
784
"""
785
Subgraph of Underlying Graph without the vertices at infinity.
786
787
Returns:
788
SAGE graph
789
"""
790
vertices = [self.UG_vertex(i) for i, _ in enumerate(self.genera)]
791
return self.underlying_graph.subgraph(vertices)
792
793
#################################################################
794
# Squishing ---- i.e. contracting horizontal edges / levels
795
#################################################################
796
797
def _squish_horizontal(self, e):
798
"""
799
Squish the horizontal edge e and returns the raw data for the new graph.
800
"""
801
# make sure v <= w
802
[v, w] = sorted([self.vertex(e[0]), self.vertex(e[1])])
803
804
if not self.is_horizontal(e):
805
print("Warning: " + repr(e) +
806
" is not a horizontal edge -- Not contracting.")
807
return (self.genera, self.legs, self.edges,
808
self.poleorders, self.levels)
809
810
newgenera = deepcopy(self.genera)
811
newlegs = deepcopy(self.legs)
812
newedges = deepcopy(self.edges)
813
newpoleorders = deepcopy(self.poleorders)
814
newlevels = deepcopy(self.levels)
815
if v == w:
816
# --horizontal loop getting contracted---
817
# add genus to node:
818
newgenera[v] += 1
819
else:
820
# --horizontal edge getting contracted---
821
# transfer genus and legs from w to v:
822
newgenera[v] += newgenera[w]
823
newlegs[v] += newlegs[w]
824
# remove all traces of w
825
newgenera.pop(w)
826
newlegs.pop(w)
827
newlevels.pop(w)
828
# remove edge:
829
newedges.remove(e)
830
# remove legs
831
newlegs[v].remove(e[0])
832
newlegs[v].remove(e[1])
833
# remove poleorders
834
del newpoleorders[e[1]]
835
del newpoleorders[e[0]]
836
837
return [newgenera, newlegs, newedges, newpoleorders, newlevels]
838
839
def squish_horizontal(self, e):
840
"""
841
Squish the horizontal edge e and return the new graph.
842
843
EXAMPLES::
844
845
sage: from admcycles.diffstrata.klevelgraph import KLevelGraph
846
sage: G = KLevelGraph.fromPOlist([1,1], [[1,2],[3,4]], [(2,3)], [1,-1,-1,1],[0,0],1)
847
sage: G.squish_horizontal((2,3))
848
KLevelGraph([2],[[1, 4]],[],{1: 1, 4: 1},[0],1,True)
849
"""
850
# Python2 only allows keyword arguments after *() arguments, so we need
851
# to repack
852
dat = self._squish_horizontal(e) + [self.sig.k]
853
return KLevelGraph(*dat, quiet=True)
854
855
def squish_vertical_slow(self, l, adddata=False, quiet=False):
856
"""
857
Squish the level l (and the next lower level!) and return the new graph.
858
If addata=True is specified, we additionally return a boolean that tells
859
us if a level was squished and the legs that no longer exist.
860
861
More precisely, adddata=True returns a tuple (G,boolean,legs) where
862
* G is the (possibly non-contracted) graph and
863
* boolean tells us if a level was squished
864
* legs is the (possibly empty) list of legs that don't exist anymore
865
866
Implementation:
867
At the moment, we remember all the edges (ee) that connect level l to
868
level l-1. We then create a new KLevelGraph with the same data as self,
869
the only difference being that all vertices of level l-1 have now been
870
assigned level l. We then remove all (now horizontal!) edges in ee.
871
872
In particular, all points and edges retain their numbering (only the level might have changed).
873
874
WARNING: Level l-1 does not exist anymore afterwards!!!
875
876
Downside: At the moment we get a warning for each edge in ee, as these
877
don't have legal pole orders for being horizontal (so checkedgeorders
878
complains each time the constructor is called :/).
879
"""
880
if l is None:
881
return self
882
ll = self.nextlowerlevel(l)
883
if ll is False:
884
if not quiet:
885
print("Warning: Illegal level to contract: " + repr(l))
886
if adddata:
887
return (self, False, None)
888
else:
889
return self
890
891
if not quiet:
892
print("Squishing levels", l, "and", ll)
893
894
vv = self.verticesonlevel(ll)
895
# edges that go to next lower level, i.e. will be contracted
896
ee = [e for e in self.edgesatlevel(l)
897
if self.levelofleg(e[1]) == ll and not self.is_horizontal(e)]
898
899
if not quiet:
900
print("Contracting edges", ee)
901
902
newgenera = deepcopy(self.genera)
903
newlegs = deepcopy(self.legs)
904
newedges = deepcopy(self.edges)
905
newpoleorders = deepcopy(self.poleorders)
906
newlevels = deepcopy(self.levels)
907
908
# raise level
909
for v in vv:
910
newlevels[v] = l
911
returngraph = KLevelGraph(
912
newgenera,
913
newlegs,
914
newedges,
915
newpoleorders,
916
newlevels,
917
self.sig.k,
918
True)
919
# contract edges that are now horizontal:
920
for e in ee:
921
returngraph = returngraph.squish_horizontal(e)
922
923
if adddata:
924
return (returngraph, True, flatten(ee))
925
else:
926
return returngraph
927
928
def _squish_vertical(self, l, quiet=True):
929
"""
930
Squish the level l (and the next lower level!) and return the raw data for the new graph.
931
932
Implementation:
933
At the moment, we remember all the edges (ee) that connect level l to
934
level l-1. We then create a new KLevelGraph with the same data as self,
935
the only difference being that all vertices of level l-1 have now been
936
assigned level l. We then remove all edges in ee.
937
938
(In contrast to the old squish_vertical_slow, this is now done in
939
one step and not recursively so we avoid a lot of the copying of graphs.)
940
941
In particular, all points and edges retain their numbering (only the level might have changed).
942
943
WARNING: Level l-1 does not exist anymore afterwards!!!
944
945
Args:
946
l (int): (internal) level number
947
quiet (bool, optional): No output. Defaults to True.
948
949
Returns:
950
tuple: raw data of the new graph
951
"""
952
if l is None:
953
return self
954
ll = self.nextlowerlevel(l)
955
levelset = set([l, ll])
956
if ll is False:
957
if not quiet:
958
print("Warning: Illegal level to contract: " + repr(l))
959
else:
960
return (self.genera, self.legs, self.edges,
961
self.poleorders, self.levels)
962
vertices = self.verticesonlevel(l) + self.verticesonlevel(ll)
963
# edges that go to next lower level, i.e. will be contracted
964
edges = [e for e in self.edges if set(
965
[self.levelofleg(e[0]), self.levelofleg(e[1])]) == levelset]
966
# we use two dictionaries for quick lookup while we reorder the info:
967
genus_of_vertex = {v: self.genus(v) for v in vertices}
968
vertex_of_leg = {
969
leg: v for v in vertices for leg in self.legsatvertex(v)}
970
legs_of_vertex = {v: self.legsatvertex(v)[:] for v in vertices}
971
deleted_legs = []
972
while edges:
973
start, end = edges.pop()
974
v = vertex_of_leg[start]
975
w = vertex_of_leg[end]
976
del vertex_of_leg[start]
977
del vertex_of_leg[end]
978
legs_of_vertex[v].remove(start)
979
legs_of_vertex[w].remove(end)
980
deleted_legs.extend([start, end])
981
if v == w:
982
# if e (now) connects a vertex to itself, increase its genus
983
genus_of_vertex[v] += 1
984
else:
985
# if e connects two different vertices, combine their data
986
# (moving everything from w to v)
987
genus_of_vertex[v] += genus_of_vertex[w]
988
del genus_of_vertex[w]
989
for leg in legs_of_vertex[w]:
990
vertex_of_leg[leg] = v
991
legs_of_vertex[v].append(leg)
992
del legs_of_vertex[w]
993
# Build new graph with this data:
994
# We use sets for more efficient lookup:
995
vertices = set(vertices)
996
deleted_legs = set(deleted_legs)
997
998
newgenera = []
999
newlegs = []
1000
newlevels = []
1001
# we copy the untouched vertices and insert the new ones:
1002
for v in range(len(self.genera)):
1003
if v in vertices:
1004
if v in genus_of_vertex:
1005
newgenera.append(genus_of_vertex[v])
1006
newlegs.append(legs_of_vertex[v][:])
1007
newlevels.append(l)
1008
# otherwise it has been deleted
1009
else:
1010
newgenera.append(self.genus(v))
1011
newlegs.append(self.legsatvertex(v)[:])
1012
newlevels.append(self.levelofvertex(v))
1013
1014
# for the edges, we remove those with a deleted edge:
1015
newedges = []
1016
for start, end in self.edges:
1017
if start in deleted_legs:
1018
assert end in deleted_legs
1019
continue
1020
assert end not in deleted_legs
1021
newedges.append((start, end))
1022
# for the new poleorders, we only have to remove the deleted legs:
1023
newpoleorders = {
1024
leg: p for leg,
1025
p in self.poleorders.items() if leg not in deleted_legs}
1026
1027
return [newgenera, newlegs, newedges, newpoleorders, newlevels]
1028
1029
def squish_vertical(self, l, quiet=True):
1030
"""
1031
Squish the level l (and the next lower level!) and return the new graph.
1032
1033
In particular, all points and edges retain their numbering (only the level might have changed).
1034
1035
WARNING: Level l-1 does not exist anymore afterwards!!!
1036
1037
Args:
1038
l (int): (internal) level number
1039
quiet (bool, optional): No output. Defaults to True.
1040
1041
Returns:
1042
KLevelGraph: new levelgraph with one level less.
1043
1044
EXAMPLES::
1045
1046
sage: from admcycles.diffstrata.klevelgraph import KLevelGraph
1047
sage: G = KLevelGraph.fromPOlist([1,2], [[1],[2,3,4]], [(1,2)], [0,-2,1,3],[0,-1],1)
1048
sage: G.squish_vertical(0)
1049
KLevelGraph([3],[[3, 4]],[],{3: 1, 4: 3},[0],1,True)
1050
"""
1051
# Python2 only allows keyword arguments after *() arguments, so we need
1052
# to repack
1053
dat = self._squish_vertical(l, quiet=quiet) + [self.sig.k]
1054
return KLevelGraph(*dat, quiet=True)
1055
1056
def delta(self, k, quiet=False):
1057
"""
1058
Squish all levels except for the k-th.
1059
1060
Note that delta(1) contracts everything except top-level and that
1061
the argument is interpreted via internal_level_number
1062
1063
WARNING: Currently giving an out of range level (e.g.
1064
0 or >= maxlevel) squishes the entire graph!!!
1065
1066
Return the corresponding divisor.
1067
"""
1068
G = self
1069
if self.is_horizontal():
1070
print("Error: Cannot delta a graph with horizontal edges!")
1071
return deepcopy(G)
1072
# recursive squishing forces us to go backwards, as the lower squished
1073
# level is always removed.
1074
for i in reversed(range(self.numberoflevels() - 1)):
1075
# unfortunately delta and squish use slightly incompatible level
1076
# numbering, as the "illegal" squish here is k-1
1077
if abs(k) - 1 == i:
1078
continue
1079
if not quiet:
1080
print("Squishing level", i)
1081
# note that it is important that internal_level_number of
1082
# self and not G is called in the argument, as the relative
1083
# numbering of levels in G changes, but the internal names don't
1084
G = G.squish_vertical(self.internal_level_number(i), quiet=quiet)
1085
return G
1086
1087
#################################################################
1088
# end Squishing
1089
#################################################################
1090
1091
#################################################################
1092
# Isomorphisms
1093
# DEPRECATED!!!! USE EmbeddedLevelGraphs instead!
1094
####
1095
# This only remains for the BIC generation comparison tests.
1096
#################################################################
1097
def _init_invariant(self):
1098
"""
1099
DEPRECATED!! DO NOT USE!!
1100
1101
Compute a bunch of invariants (as a quick check for not isommorphic).
1102
Up to now we have:
1103
* sorted list of genera (by level)
1104
* number of edges
1105
* number of levels
1106
* names and pole orders of marked points
1107
* sorted list of pole orders
1108
"""
1109
self._invariant = (
1110
len(self.edges),
1111
self.codim(),
1112
[(l, self.orderatleg(l))
1113
for l in sorted(self.list_markings())],
1114
sorted([self.orderatleg(l) for l in self.leglist]))
1115
1116
def invariant(self):
1117
"""
1118
DEPRECATED!!! DO NOT USE!!!
1119
"""
1120
self._init_invariant()
1121
return self._invariant
1122
1123
def _genisom_preserves_levels(self, other, dV):
1124
"""
1125
Check if a "vertex isomorphism" preserves the (relative) level structure
1126
"""
1127
return all(self.sortedlevels.index(self.levelofvertex(sv)) ==
1128
other.sortedlevels.index(other.levelofvertex(ov))
1129
for sv, ov in dV.items())
1130
1131
def _legisom_preserves_poleorders(self, other, dL):
1132
"""
1133
Check if a "leg isomorphism" preserves pole orders.
1134
"""
1135
return all(self.orderatleg(sl) == other.orderatleg(ol)
1136
for sl, ol in dL.items())
1137
1138
def _leveliso(self, other, dV):
1139
"""
1140
Give a dictionary translating the levels of self to other in accordance
1141
with the dictionary of vertices dV.
1142
"""
1143
return {l: other.levelofvertex(dV[self.verticesonlevel(l)[0]])
1144
for l in self.levels}
1145
1146
def is_isomorphic(self, other, adddata=False):
1147
"""
1148
DEPRECATED!!! Instead, use EmbeddedLevelGraph!!!
1149
1150
Check if self is isomorphic to other.
1151
Return boolean + list of isomorphisms if adddata=True.
1152
1153
Note that our isomorphisms are stgraph isomorphisms plus a dictionary
1154
translating the level structure of self into that of other.
1155
1156
At the moment we use admcycles.GraphIsom and check which of these
1157
preserve the KLevelGraph structure (using the above invariants, though!)
1158
Probably it would be much faster to reimplement this for KLevelGraphs
1159
as it seems quite redundant as is...
1160
"""
1161
isoms = GraphIsom(self.stgraph, other.stgraph)
1162
# check if the stable graph isomorphisms preserve level structure
1163
# GraphIsoms consist of a tuple (dV,dL), where dV is a dictionary
1164
# translating the vertices and dL is one for the legs.
1165
levelisoms = [[dV, dL, self._leveliso(other, dV)] for [dV, dL] in isoms
1166
if self._genisom_preserves_levels(other, dV) and
1167
self._legisom_preserves_poleorders(other, dL)]
1168
if adddata:
1169
return (bool(levelisoms), levelisoms)
1170
else:
1171
return (bool(levelisoms))
1172
1173
#################################################################
1174
# Plotting
1175
#################################################################
1176
def plot_obj(self):
1177
"""
1178
Return a sage graphics object generated from a sage graph.
1179
1180
TODO: Some things that are still ugly:
1181
* No legs!!
1182
* currently the vertices have labels (index,genus)
1183
* loops are vertical not horizontal
1184
"""
1185
# Create the underlying graph with edge label prong order.
1186
G = Graph([self.genera,
1187
[(self.vertex(e[0]), self.vertex(e[1]), self.prong(e))
1188
for e in self.edges]],
1189
format='vertices_and_edges', loops=True, multiedges=True)
1190
# unfortunately, sage insists of displaying the vertex name (index)
1191
# and not only the label (genus), so we display both...
1192
G.relabel(list(enumerate(self.genera)))
1193
h = {l: [(v, self.genus(v)) for v in self.verticesonlevel(l)]
1194
for l in self.levels}
1195
# at the moment we just display a list of legs and orders at the bottom
1196
legtext = "Marked points: "
1197
for l in sorted(set(self.levels)):
1198
points_at_level = [leg for leg in self.list_markings()
1199
if self.levelofleg(leg) == l]
1200
if not points_at_level:
1201
continue
1202
legtext += "\nAt level %s: " % l
1203
for leg in points_at_level:
1204
legtext += "\n" + self._pointtype(leg)
1205
legtext += " of order " + repr(self.orderatleg(leg))
1206
print(legtext)
1207
return G.plot(edge_labels=True, vertex_size=1000,
1208
heights=h, layout='ranked') + text(legtext, (0, -0.1),
1209
vertical_alignment='top', axis_coords=True, axes=False)
1210
1211