Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
180 views
unlisted
ubuntu2004
1
# -*- coding: utf-8 -*-
2
r"""
3
Stable graphs
4
"""
5
import itertools
6
from collections import defaultdict
7
8
from sage.misc.misc_c import prod
9
10
from sage.structure.sage_object import SageObject
11
12
from sage.combinat.permutation import Permutations
13
from sage.combinat.subset import Subsets
14
from sage.rings.integer_ring import ZZ
15
from sage.modules.free_module import FreeModule
16
from sage.functions.other import floor
17
from sage.graphs.graph import Graph
18
from sage.rings.all import Integer
19
from sage.arith.all import factorial
20
from sage.groups.perm_gps.permgroup_named import SymmetricGroup
21
22
from sage.plot.polygon import polygon2d
23
from sage.plot.text import text
24
from sage.plot.bezier_path import bezier_path
25
from sage.plot.line import line
26
from sage.plot.circle import circle
27
28
from .superseded import deprecated_function_alias
29
from .moduli import MODULI_ST, MODULI_TL, MODULI_CT, MODULI_RT, MODULI_SM
30
31
32
class StableGraph(SageObject):
33
r"""
34
Stable graph.
35
36
A stable graph is a graph (with allowed loops and multiple edges) that
37
has "genus" and "marking" decorations at its vertices. It is represented
38
as a list of genera of its vertices, a list of legs at each vertex and a
39
list of pairs of legs forming edges.
40
41
The markings (the legs that are not part of edges) are allowed to have
42
repetitions.
43
44
EXAMPLES:
45
46
We create a stable graph with two vertices of genera 3,5 joined by an edge
47
with a self-loop at the genus 3 vertex::
48
49
sage: from admcycles import StableGraph
50
sage: StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
51
[3, 5] [[1, 3, 5], [2]] [(1, 2), (3, 5)]
52
53
The markings can have repetitions::
54
55
sage: StableGraph([1,0], [[1,2], [3,2]], [(1,3)])
56
[1, 0] [[1, 2], [3, 2]] [(1, 3)]
57
58
It is also possible to create graphs which are not necessarily stable::
59
60
sage: StableGraph([1,0], [[1], [2,3]], [(1,2)])
61
[1, 0] [[1], [2, 3]] [(1, 2)]
62
63
sage: StableGraph([0], [[1]], [])
64
[0] [[1]] []
65
66
If the input is invalid a ``ValueError`` is raised::
67
68
sage: StableGraph([0, 0], [[1], [2], [3]], [])
69
Traceback (most recent call last):
70
...
71
ValueError: genera and legs must have the same length
72
73
sage: StableGraph([0, 'hello'], [[1], [2]], [(1,2)])
74
Traceback (most recent call last):
75
...
76
ValueError: genera must be a list of non-negative integers
77
78
sage: StableGraph([0, 0], [[1,2], [3]], [(3,4)])
79
Traceback (most recent call last):
80
...
81
ValueError: the edge (3, 4) uses invalid legs
82
83
sage: StableGraph([0, 0], [[2,3], [2]], [(2,3)])
84
Traceback (most recent call last):
85
...
86
ValueError: the edge (2, 3) uses invalid legs
87
"""
88
__slots__ = ['_genera', '_legs', '_edges', '_maxleg', '_mutable',
89
'_graph_cache', '_canonical_label_cache', '_hash']
90
91
def __init__(self, genera, legs, edges, mutable=False, check=True):
92
"""
93
INPUT:
94
95
- ``genera`` -- (list) List of genera of the vertices of length m.
96
97
- ``legs`` -- (list) List of length m, where ith entry is list of legs
98
attached to vertex i.
99
100
- ``edges`` -- (list) List of edges of the graph. Each edge is a 2-tuple of legs.
101
102
- ``mutable`` - (boolean, default to ``False``) whether this stable graph
103
should be mutable
104
105
- ``check`` - (boolean, default to ``True``) whether some additional sanity
106
checks are performed on input
107
"""
108
if check:
109
if not isinstance(genera, list) or \
110
not isinstance(legs, list) or \
111
not isinstance(edges, list):
112
raise TypeError('genera, legs, edges must be lists')
113
114
if len(genera) != len(legs):
115
raise ValueError('genera and legs must have the same length')
116
117
if not all(isinstance(g, (int, Integer)) and g >= 0 for g in genera):
118
raise ValueError("genera must be a list of non-negative integers")
119
120
mlegs = defaultdict(int)
121
for v, l in enumerate(legs):
122
if not isinstance(l, list):
123
raise ValueError("legs must be a list of lists")
124
for i in l:
125
if not isinstance(i, (int, Integer)) or i <= 0:
126
raise ValueError("legs must be positive integers")
127
mlegs[i] += 1
128
129
for e in edges:
130
if not isinstance(e, tuple) or len(e) != 2:
131
raise ValueError("invalid edge {}".format(e))
132
if mlegs.get(e[0], 0) != 1 or mlegs.get(e[1], 0) != 1:
133
raise ValueError("the edge {} uses invalid legs".format(e))
134
135
self._genera = genera
136
self._legs = legs
137
self._edges = edges
138
self._maxleg = max([max(j + [0]) for j in self._legs]) # highest leg-label that occurs
139
self._mutable = bool(mutable)
140
self._graph_cache = None
141
self._canonical_label_cache = None
142
self._hash = None
143
144
def set_immutable(self):
145
r"""
146
Set this graph immutable.
147
"""
148
self._mutable = False
149
150
def is_mutable(self):
151
r"""
152
Return whether this graph is mutable.
153
"""
154
return self._mutable
155
156
def __hash__(self):
157
r"""
158
EXAMPLES::
159
160
sage: from admcycles import StableGraph
161
sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
162
sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
163
sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])
164
sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])
165
sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])
166
sage: hash(G1) == hash(G2)
167
True
168
sage: (hash(G1) == hash(G3) or hash(G1) == hash(G4) or
169
....: hash(G1) == hash(G5) or hash(G3) == hash(G4) or
170
....: hash(G3) == hash(G5) or hash(G4) == hash(G5))
171
False
172
"""
173
if self._mutable:
174
raise TypeError("mutable stable graph are not hashable")
175
if self._hash is None:
176
self._hash = hash((tuple(self._genera),
177
tuple(tuple(x) for x in self._legs),
178
tuple(self._edges)))
179
return self._hash
180
181
def copy(self, mutable=True):
182
r"""
183
Return a copy of this graph.
184
185
When it is asked for an immutable copy of an immutable graph the
186
current graph is returned without copy.
187
188
INPUT:
189
190
- ``mutable`` - (boolean, default ``True``) whether the returned graph must
191
be mutable
192
193
EXAMPLES::
194
195
sage: from admcycles import StableGraph
196
sage: G = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
197
sage: H = G.copy()
198
sage: H == G
199
True
200
sage: H.is_mutable()
201
True
202
sage: H is G
203
False
204
205
sage: G.copy(mutable=False) is G
206
True
207
"""
208
if not self.is_mutable() and not mutable:
209
# avoid copy when immutability holds for both
210
return self
211
G = StableGraph.__new__(StableGraph)
212
G._genera = self._genera[:]
213
G._legs = [x[:] for x in self._legs]
214
G._edges = [x[:] for x in self._edges]
215
G._maxleg = self._maxleg
216
G._mutable = mutable
217
G._graph_cache = None
218
G._canonical_label_cache = None
219
G._hash = None
220
return G
221
222
def __repr__(self):
223
return repr(self._genera) + ' ' + repr(self._legs) + ' ' + repr(self._edges)
224
225
def __lt__(self, other):
226
r"""
227
TESTS::
228
229
sage: from admcycles import StableGraph
230
sage: g0 = StableGraph([1], [[1, 2, 3]], [])
231
sage: g1 = StableGraph([0], [[5, 6, 1, 2, 3]], [(5, 6)])
232
sage: g2 = StableGraph([0, 1], [[1, 2, 5], [3, 6]], [(5, 6)])
233
sage: g3 = StableGraph([0, 1], [[1, 3, 5], [2, 6]], [(5, 6)])
234
sage: g4 = StableGraph([0, 1], [[2, 3, 5], [1, 6]], [(5, 6)])
235
sage: sorted([g3, g1, g2, g4, g0]) == sorted([g2, g0, g4, g1, g3]) == [g0, g1, g2, g3, g4]
236
True
237
238
sage: g0 < g0
239
False
240
sage: g0 <= g0
241
True
242
243
sage: g1 > g0 and g1 >= g0 and g2 > g1 and g2 >= g1
244
True
245
sage: g1 < g0 or g1 <= g0 or g2 < g1 or g2 <= g1
246
False
247
"""
248
if type(self) is not type(other):
249
raise TypeError('incomparable elements')
250
251
# first sort by number of edges
252
sne = self.num_edges()
253
one = other.num_edges()
254
if sne < one:
255
return True
256
if one < sne:
257
return False
258
259
# then by number of vertices
260
snv = self.num_verts()
261
onv = other.num_verts()
262
if snv < onv:
263
return True
264
elif snv > onv:
265
return False
266
267
# then use plain comparison of the data
268
if self._genera < other._genera:
269
return True
270
elif self._genera > other._genera:
271
return False
272
elif self._legs < other._legs:
273
return True
274
elif self._legs > other._legs:
275
return False
276
elif self._edges < other._edges:
277
return True
278
elif self._edges > other._edges:
279
return False
280
else:
281
# equality case
282
return False
283
284
def __eq__(self, other):
285
r"""
286
TESTS::
287
288
sage: from admcycles import StableGraph
289
sage: G1 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
290
sage: G2 = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
291
sage: G3 = StableGraph([3,5], [[3,5,1],[2]], [(2,1),(3,5)])
292
sage: G4 = StableGraph([2,2], [[1,3,5],[2]], [(1,2),(3,5)])
293
sage: G5 = StableGraph([3,5], [[1,4,5],[2]], [(1,2),(4,5)])
294
sage: G1 == G2
295
True
296
sage: G1 == G3 or G1 == G4 or G1 == G5
297
False
298
"""
299
if type(self) is not type(other):
300
return False
301
return self._genera == other._genera and \
302
self._legs == other._legs and \
303
self._edges == other._edges
304
305
def __ne__(self, other):
306
return not (self == other)
307
308
def __le__(self, other):
309
return self == other or self < other
310
311
def __gt__(self, other):
312
return other < self
313
314
def __ge__(self, other):
315
return self == other or other < self
316
317
def _graph(self):
318
if not self._mutable and self._graph_cache is not None:
319
return self._graph_cache
320
321
# TODO: it should be much faster to build this graph...
322
from collections import defaultdict
323
324
legs = defaultdict(list)
325
326
# the multiplicity of edges is stored as integral labels
327
# on the graph
328
G = Graph(len(self._genera), loops=True, multiedges=False, weighted=True)
329
for li, lj in self._edges:
330
i = self.vertex(li)
331
j = self.vertex(lj)
332
if G.has_edge(i, j):
333
G.set_edge_label(i, j, G.edge_label(i, j) + 1)
334
else:
335
G.add_edge(i, j, 1)
336
337
if i < j:
338
legs[i, j].append((li, lj))
339
else:
340
legs[j, i].append((lj, li))
341
342
vertex_partition = defaultdict(list)
343
for i, (g, l) in enumerate(zip(self._genera, self._legs)):
344
l = sorted(self.list_markings(i))
345
inv = (g,) + tuple(l)
346
vertex_partition[inv].append(i)
347
348
vertex_data = sorted(vertex_partition)
349
partition = [vertex_partition[k] for k in vertex_data]
350
351
output = (G, vertex_data, dict(legs), partition)
352
if not self._mutable:
353
self._graph_cache = output
354
return output
355
356
def _canonical_label(self):
357
if not self._mutable and self._canonical_label_cache is not None:
358
return self._canonical_label_cache
359
G, _, _, partition = self._graph()
360
361
# NOTE: we explicitly set algorithm='sage' since bliss got confused
362
# with edge labeled graphs. See
363
# https://trac.sagemath.org/ticket/28531
364
H, phi = G.canonical_label(partition=partition,
365
edge_labels=True,
366
certificate=True,
367
algorithm='sage')
368
369
output = (H, phi)
370
if not self._mutable:
371
self._canonical_label_cache = output
372
return output
373
374
def set_canonical_label(self, certificate=False):
375
r"""
376
Set canonical label to this stable graph.
377
378
The canonical labeling is such that two isomorphic graphs get relabeled
379
the same way. If certificate is true return a pair `dicv`, `dicl` of
380
mappings from the vertices and legs to the canonical ones.
381
382
EXAMPLES::
383
384
sage: from admcycles import StableGraph
385
386
sage: S = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)])
387
388
sage: T = S.copy()
389
sage: T.set_canonical_label()
390
sage: T # random
391
[1, 2, 3] [[1, 4, 6], [2, 5, 8], [3, 7, 9]] [(4, 5), (6, 7), (8, 9)]
392
sage: assert S.is_isomorphic(T)
393
394
Check that isomorphic graphs get relabelled the same way::
395
396
sage: S0 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)], mutable=False)
397
sage: S1 = S0.copy()
398
sage: S2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)], mutable=True)
399
sage: S3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)], mutable=True)
400
sage: S1.set_canonical_label()
401
sage: S2.set_canonical_label()
402
sage: S3.set_canonical_label()
403
sage: assert S1 == S2 == S3
404
sage: assert S0.is_isomorphic(S1)
405
406
sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=False)
407
sage: S1 = S0.copy()
408
sage: S2 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)
409
sage: S3 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)
410
sage: S1.set_canonical_label()
411
sage: S2.set_canonical_label()
412
sage: S3.set_canonical_label()
413
sage: assert S1 == S2 == S3
414
sage: assert S0.is_isomorphic(S1)
415
416
sage: S0 = StableGraph([1,2,3], [[1,4,5],[2,6,7],[3,8,9]], [(4,6),(5,8),(7,9)], mutable=True)
417
sage: S1 = StableGraph([3,2,1], [[3,8,5],[2,6,7],[1,4,9]], [(8,6),(5,4),(7,9)], mutable=True)
418
sage: S2 = StableGraph([2,1,3], [[7,2,5],[6,4,1],[3,9,8]], [(7,6),(5,8),(4,9)], mutable=True)
419
sage: for S in [S0, S1, S2]:
420
....: T = S.copy()
421
....: assert S == T
422
....: vm, lm = T.set_canonical_label(certificate=True)
423
....: S.relabel(vm, lm, inplace=True)
424
....: S.tidy_up()
425
....: assert S == T, (S, T)
426
427
428
TESTS::
429
430
sage: from admcycles import StableGraph
431
sage: S0 = StableGraph([2], [[]], [], mutable=True)
432
sage: S0.set_canonical_label()
433
434
sage: S0 = StableGraph([0,1,0], [[1,2,4], [3], [5,6,7]], [(2,3), (4,5), (6,7)])
435
sage: S1 = S0.copy()
436
sage: S1.set_canonical_label()
437
sage: assert S0.is_isomorphic(S1)
438
sage: S2 = S1.copy()
439
sage: S1.set_canonical_label()
440
sage: assert S1 == S2
441
442
sage: S0 = StableGraph([1,0], [[2,3], [5,7,9]], [(2,5), (7,9)], mutable=True)
443
sage: S1 = StableGraph([1,0], [[1,3], [2,7,9]], [(1,2), (7,9)], mutable=True)
444
sage: S0.set_canonical_label()
445
sage: S1.set_canonical_label()
446
sage: assert S0 == S1
447
"""
448
if not self._mutable:
449
raise ValueError("the graph must be mutable; use inplace=False")
450
451
G, vdat, legs, part = self._graph()
452
H, dicv = self._canonical_label()
453
invdicv = {j: i for i, j in dicv.items()}
454
if certificate:
455
dicl = {}
456
457
# vdat: list of the possible (g, l0, l1, ...)
458
# where l0, l1, ... are the markings
459
# legs: dico: (i,j) -> list of edges given by pair of legs
460
461
new_genera = [self._genera[invdicv[i]] for i in range(self.num_verts())]
462
assert new_genera == sorted(self._genera)
463
new_legs = [sorted(self.list_markings(invdicv[i])) for i in range(self.num_verts())]
464
465
can_edges = []
466
for e0, e1, mult in G.edges(sort=False):
467
f0 = dicv[e0]
468
f1 = dicv[e1]
469
inv = False
470
if f0 > f1:
471
f0, f1 = f1, f0
472
inv = True
473
can_edges.append((f0, f1, e0, e1, mult, inv))
474
can_edges.sort()
475
476
nl = self.num_legs()
477
marks = set(self.list_markings())
478
m0 = (max(marks) if marks else 0) + 1
479
non_markings = range(m0, m0 + nl - len(marks))
480
assert len(non_markings) == 2 * sum(mult for _, _, _, _, mult, _ in can_edges)
481
482
k = 0
483
new_edges = []
484
for f0, f1, e0, e1, mult, inv in can_edges:
485
assert f0 <= f1
486
new_legs[f0].extend(non_markings[i] for i in range(k, k + 2 * mult, 2))
487
new_legs[f1].extend(non_markings[i] for i in range(k + 1, k + 2 * mult, 2))
488
new_edges.extend((non_markings[i], non_markings[i + 1]) for i in range(k, k + 2 * mult, 2))
489
490
if certificate:
491
assert len(legs[e0, e1]) == mult, (len(legs[e0, e1]), mult)
492
if inv:
493
assert dicv[e0] == f1
494
assert dicv[e1] == f0
495
for i, (l1, l0) in enumerate(legs[e0, e1]):
496
dicl[l0] = non_markings[k + 2 * i]
497
dicl[l1] = non_markings[k + 2 * i + 1]
498
else:
499
assert dicv[e0] == f0
500
assert dicv[e1] == f1
501
for i, (l0, l1) in enumerate(legs[e0, e1]):
502
dicl[l0] = non_markings[k + 2 * i]
503
dicl[l1] = non_markings[k + 2 * i + 1]
504
505
k += 2 * mult
506
507
self._genera = new_genera
508
self._legs = new_legs
509
self._edges = new_edges
510
511
if certificate:
512
return (dicv, dicl)
513
514
def relabel(self, vm, lm, inplace=False, mutable=False):
515
r"""
516
INPUT:
517
518
- ``vm`` -- (dictionary) a vertex map (from the label of this graph to
519
the new labels). If a vertex number is missing it will remain untouched.
520
521
- ``lm`` -- (dictionary) a leg map (idem). If a leg label is missing it
522
will remain untouched.
523
524
- ``inplace`` -- (boolean, default ``False``) whether to relabel the
525
graph inplace
526
527
- ``mutable`` -- (boolean, default ``False``) whether to make the
528
resulting graph immutable. Ignored when ``inplace=True``
529
530
EXAMPLES::
531
532
sage: from admcycles import StableGraph
533
sage: G = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])
534
sage: vm = {0:1, 1:0}
535
sage: lm = {1:3, 2:5, 3:7, 4:9, 5:15}
536
sage: G.relabel(vm, lm)
537
[5, 3] [[5], [3, 7, 15, 9]] [(3, 5), (7, 15)]
538
539
sage: G.relabel(vm, lm, mutable=False).is_mutable()
540
False
541
sage: G.relabel(vm, lm, mutable=True).is_mutable()
542
True
543
544
sage: H = G.copy()
545
sage: H.relabel(vm, lm, inplace=True)
546
sage: H == G.relabel(vm, lm)
547
True
548
"""
549
# TODO: more inplace operations when possible
550
m = len(self._genera)
551
genera = [None] * m
552
legs = [None] * m
553
for i, (g, l) in enumerate(zip(self._genera, self._legs)):
554
j = vm.get(i, i) # new label
555
genera[j] = g
556
legs[j] = [lm.get(k, k) for k in l]
557
558
edges = [(lm.get(i, i), lm.get(j, j)) for i, j in self._edges]
559
560
if inplace:
561
if not self._mutable:
562
raise ValueError('the graph is not mutable; use a copy instead')
563
self._genera = genera
564
self._legs = legs
565
self._edges = edges
566
else:
567
return StableGraph(genera, legs, edges, mutable)
568
569
def is_isomorphic(self, other, certificate=False):
570
r"""
571
Test whether this stable graph is isomorphic to ``other``.
572
573
INPUT:
574
575
- ``certificate`` - if set to ``True`` return also a vertex mapping and
576
a legs mapping
577
578
EXAMPLES::
579
580
sage: from admcycles import StableGraph
581
582
sage: G1 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,5)])
583
sage: G2 = StableGraph([3,5], [[2,3,1,4],[5]], [(2,5),(3,1)])
584
sage: G3 = StableGraph([5,3], [[5],[2,3,4,1]], [(2,5),(3,1)])
585
sage: G1.is_isomorphic(G2) and G1.is_isomorphic(G3)
586
True
587
588
Graphs with distinct markings are not isomorphic::
589
590
sage: G4 = StableGraph([3,5], [[1,3,5,4],[2]], [(1,2),(3,4)])
591
sage: G1.is_isomorphic(G4) or G2.is_isomorphic(G4) or G3.is_isomorphic(G4)
592
False
593
594
Graph with marking multiplicities::
595
596
sage: H1 = StableGraph([0], [[1,1]], [])
597
sage: H2 = StableGraph([0], [[1,1]], [])
598
sage: H1.is_isomorphic(H2)
599
True
600
601
sage: H3 = StableGraph([0,0], [[1,2,4],[1,3]], [(2,3)])
602
sage: H4 = StableGraph([0,0], [[1,2],[1,3,4]], [(2,3)])
603
sage: H3.is_isomorphic(H4)
604
True
605
606
TESTS::
607
608
sage: from admcycles import StableGraph
609
610
sage: G = StableGraph([0, 0], [[5, 8, 4, 3], [9, 2, 1]], [(8, 9)])
611
sage: H = StableGraph([0, 0], [[1, 2, 6], [3, 4, 5, 7]], [(6, 7)])
612
sage: G.is_isomorphic(H)
613
True
614
sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)
615
sage: assert ans
616
sage: G.relabel(vm, lm)
617
[0, 0] [[6, 2, 1], [5, 7, 4, 3]] [(7, 6)]
618
619
sage: G = StableGraph([0, 0], [[4, 8, 2], [3, 9, 1]], [(8, 9)])
620
sage: H = StableGraph([0, 0], [[1, 3, 5], [2, 4, 6]], [(5, 6)])
621
sage: G.is_isomorphic(H)
622
True
623
sage: ans, (vm, lm) = G.is_isomorphic(H, certificate=True)
624
sage: assert ans
625
sage: G.relabel(vm, lm)
626
[0, 0] [[3, 5, 1], [4, 6, 2]] [(6, 5)]
627
628
sage: G = StableGraph([0, 1, 1], [[1, 3, 5], [2, 4], [6]], [(1, 2), (3, 4), (5, 6)])
629
sage: H = StableGraph([1, 0, 1], [[1], [2, 3, 5], [4, 6]], [(1, 2), (3, 4), (5, 6)])
630
sage: _ = G.is_isomorphic(H, certificate=True)
631
632
Check for https://gitlab.com/modulispaces/admcycles/issues/22::
633
634
sage: g1 = StableGraph([0, 0], [[1, 3], [2,4]], [(1, 2), (3, 4)])
635
sage: g2 = StableGraph([0, 0], [[1], [2]], [(1, 2)])
636
sage: g1.is_isomorphic(g2)
637
False
638
639
Check for https://gitlab.com/modulispaces/admcycles/issues/24::
640
641
sage: 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)])
642
sage: 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)])
643
sage: gr1.is_isomorphic(gr2)
644
False
645
"""
646
if type(self) != type(other):
647
raise TypeError
648
649
sG, svdat, slegs, spart = self._graph()
650
oG, ovdat, olegs, opart = other._graph()
651
652
# first compare vertex data partitions
653
if svdat != ovdat or any(len(p) != len(q) for p, q in zip(spart, opart)):
654
return (False, None) if certificate else False
655
656
# next, graph isomorphism
657
# NOTE: since canonical label maps the first atom of the partition to
658
# {0, ..., m_1 - 1} the second to {m_1, ..., m_1 + m_2 - 1}, etc we are
659
# guaranteed that the partitions match when the graphs are isomorphic.
660
sH, sphi = self._canonical_label()
661
oH, ophi = other._canonical_label()
662
if sH != oH:
663
return (False, None) if certificate else False
664
665
if certificate:
666
ophi_inv = {j: i for i, j in ophi.items()}
667
vertex_map = {i: ophi_inv[sphi[i]] for i in range(len(self._genera))}
668
669
legs_map = {}
670
# legs that are not part of edges are untouched
671
# (slot zero in the list is the genus)
672
for l in svdat:
673
for i in l[1:]:
674
legs_map[i] = i
675
676
# pair of legs that form edges have moved
677
for (i, j), sl in slegs.items():
678
m = sG.edge_label(i, j)
679
assert len(sl) == m, (m, sl, self, other)
680
681
ii = ophi_inv[sphi[i]]
682
jj = ophi_inv[sphi[j]]
683
if ii <= jj:
684
ol = olegs[ii, jj]
685
else:
686
ol = [(b, a) for (a, b) in olegs[jj, ii]]
687
assert len(ol) == m, (m, sl, ol, self, other)
688
689
for (a, b), (c, d) in zip(sl, ol):
690
legs_map[a] = c
691
legs_map[b] = d
692
693
return True, (vertex_map, legs_map)
694
695
else:
696
return True
697
698
def vertex_automorphism_group(self, *args, **kwds):
699
r"""
700
Return the action of the automorphism group on the vertices.
701
702
All arguments provided are forwarded to the method `automorphism_group`
703
of Sage graphs.
704
705
EXAMPLES::
706
707
sage: from admcycles import StableGraph
708
709
sage: G = StableGraph([0], [[1, 2, 3, 4, 5, 6]], [(3, 4), (5, 6)])
710
sage: G.vertex_automorphism_group()
711
Permutation Group with generators [()]
712
713
sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])
714
sage: G.vertex_automorphism_group()
715
Permutation Group with generators [(0,1)]
716
sage: G = StableGraph([0, 0], [[4, 1, 2], [1, 3, 4]], [(3, 2)])
717
sage: G.vertex_automorphism_group()
718
Permutation Group with generators [(0,1)]
719
720
sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],
721
....: [(2,3),(4,5),(6,1)])
722
sage: A = G.vertex_automorphism_group()
723
sage: A
724
Permutation Group with generators [(1,2), (0,1)]
725
sage: A.cardinality()
726
6
727
728
Using extra arguments::
729
730
sage: G.vertex_automorphism_group(algorithm='sage')
731
Permutation Group with generators [(1,2), (0,1)]
732
sage: G.vertex_automorphism_group(algorithm='bliss') # optional - bliss
733
Permutation Group with generators [(1,2), (0,1)]
734
"""
735
G, _, _, partition = self._graph()
736
return G.automorphism_group(partition=partition,
737
edge_labels=True,
738
*args, **kwds)
739
740
def leg_automorphism_induce(self, g, A=None, check=True):
741
r"""
742
Given a leg automorphism ``g`` return its action on the vertices.
743
744
Note that there is no check that the element ``g`` is a valid automorphism.
745
746
INPUT:
747
748
- ``g`` - a permutation acting on the legs
749
750
- ``A`` - ambient permutation group for the result
751
752
- ``check`` - (default ``True``) parameter forwarded to the constructor
753
of the permutation group acting on vertices.
754
755
EXAMPLES::
756
757
sage: from admcycles import StableGraph
758
sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],
759
....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])
760
sage: Averts = G.vertex_automorphism_group()
761
sage: Alegs = G.leg_automorphism_group()
762
sage: g = Alegs('(1,14)(2,13)(3,4)(5,10)(6,9)(7,8)(11,12)(15,16)')
763
sage: assert G.leg_automorphism_induce(g) == Averts('(0,3)(1,2)')
764
sage: g = Alegs('(3,11)(4,12)(5,13)(6,14)')
765
sage: assert G.leg_automorphism_induce(g) == Averts('')
766
sage: g = Alegs('(1,11,13,15,9,3,5,7)(2,12,14,16,10,4,6,8)')
767
sage: assert G.leg_automorphism_induce(g) == Averts('(0,1,2,3)')
768
769
TESTS::
770
771
sage: G = StableGraph([3], [[]], [])
772
sage: S = SymmetricGroup([0])
773
sage: G.leg_automorphism_induce(S(''))
774
()
775
"""
776
if A is None:
777
A = SymmetricGroup(list(range(len(self._genera))))
778
if len(self._genera) == 1:
779
return A('')
780
p = [self.vertex(g(self._legs[u][0])) for u in range(len(self._genera))]
781
return A(p, check=check)
782
783
def vertex_automorphism_lift(self, g, A=None, check=True):
784
r"""
785
Provide a canonical lift of vertex automorphism to leg automorphism.
786
787
Note that there is no check that ``g`` defines a valid automorphism of
788
the vertices.
789
790
INPUT:
791
792
- ``g`` - permutation automorphism of the vertices
793
794
- ``A`` - an optional permutation group in which the result belongs to
795
796
- ``check`` -- (default ``True``) parameter forwarded to the constructor
797
of the permutation group
798
799
EXAMPLES::
800
801
sage: from admcycles import StableGraph
802
sage: G = StableGraph([0, 0, 0, 0], [[1,8,9,16], [2,3,10,11], [4,5,12,13], [6,7,14,15]],
803
....: [(1,2),(3,4),(5,6),(7,8),(9,10),(11,12),(13,14),(15,16)])
804
sage: Averts = G.vertex_automorphism_group()
805
sage: G.vertex_automorphism_lift(Averts(''))
806
()
807
sage: G.vertex_automorphism_lift(Averts('(0,3)(1,2)'))
808
(1,6)(2,5)(3,4)(7,8)(9,14)(10,13)(11,12)(15,16)
809
sage: G.vertex_automorphism_lift(Averts('(0,1,2,3)'))
810
(1,3,5,7)(2,4,6,8)(9,11,13,15)(10,12,14,16)
811
"""
812
p = sorted(sum(self._legs, []))
813
if A is None:
814
A = SymmetricGroup(p)
815
leg_pos = {j: i for i, j in enumerate(p)}
816
817
G, _, edge_to_legs, partition = self._graph()
818
819
# promote automorphisms of G as automorphisms of the stable graph
820
for u, v, lab in G.edges(sort=True):
821
gu = g(u)
822
gv = g(v)
823
824
if u < v:
825
start = edge_to_legs[u, v]
826
else:
827
start = [(t, s) for s, t in edge_to_legs[v, u]]
828
829
if gu < gv:
830
end = edge_to_legs[gu, gv]
831
else:
832
end = [(t, s) for s, t in edge_to_legs[gv, gu]]
833
834
for (lu, lv), (glu, glv) in zip(start, end):
835
p[leg_pos[lu]] = glu
836
p[leg_pos[lv]] = glv
837
838
return A(p, check=check)
839
840
def leg_automorphism_group_vertex_stabilizer(self, *args, **kwds):
841
r"""
842
Return the group of automorphisms of this stable graph stabilizing the vertices.
843
844
All arguments are forwarded to the subgroup method of the Sage symmetric group.
845
846
EXAMPLES::
847
848
sage: from admcycles import StableGraph
849
sage: G = StableGraph([0],[[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])
850
sage: G.leg_automorphism_group_vertex_stabilizer()
851
Subgroup 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)
852
853
sage: G = StableGraph([1,1],[[1,2],[3,4]], [(1,3),(2,4)])
854
sage: G.leg_automorphism_group_vertex_stabilizer()
855
Subgroup generated by [(1,2)(3,4)] of (Symmetric group of order 4! as a permutation group)
856
"""
857
legs = sorted(sum(self._legs, []))
858
859
G, _, edge_to_legs, partition = self._graph()
860
861
S = SymmetricGroup(legs)
862
gens = []
863
for u, v, lab in G.edges(sort=True):
864
if lab == 1 and u != v:
865
continue
866
867
if u < v:
868
multiedge = edge_to_legs[u, v]
869
else:
870
multiedge = edge_to_legs[v, u]
871
872
if lab >= 2:
873
(lu0, lv0) = multiedge[0]
874
(lu1, lv1) = multiedge[1]
875
gens.append(S.element_class([(lu0, lu1), (lv0, lv1)], S, check=False))
876
if lab >= 3:
877
gens.append(S.element_class([tuple(x for x, y in multiedge),
878
tuple(y for x, y in multiedge)], S, check=False))
879
if u == v:
880
(lu, lv) = multiedge[0]
881
gens.append(S.element_class([(lu, lv)], S, check=False))
882
883
return S.subgroup(gens, *args, **kwds)
884
885
def leg_automorphism_group(self, *args, **kwds):
886
r"""
887
Return the action of the automorphism group on the legs.
888
889
The arguments provided to this function are forwarded to the
890
constructor of the subgroup of the symmetric group.
891
892
EXAMPLES::
893
894
sage: from admcycles import StableGraph
895
896
A triangle::
897
898
sage: G = StableGraph([0, 0, 0], [[1,2], [3,4], [5,6]],
899
....: [(2,3),(4,5),(6,1)])
900
sage: Alegs = G.leg_automorphism_group()
901
sage: assert Alegs.cardinality() == G.automorphism_number()
902
903
A vertex with four loops::
904
905
sage: G = StableGraph([0], [[1,2,3,4,5,6,7,8]], [(1,2),(3,4),(5,6),(7,8)])
906
sage: Alegs = G.leg_automorphism_group()
907
sage: assert Alegs.cardinality() == G.automorphism_number()
908
sage: a = Alegs.random_element()
909
910
Using extra arguments::
911
912
sage: G = StableGraph([0,0,0], [[6,1,7,8],[2,3,9,10],[4,5,11,12]],
913
....: [(1,2), (3,4), (5,6), (7,8), (9,10), (11,12)])
914
sage: G.leg_automorphism_group()
915
Subgroup 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)
916
sage: G.leg_automorphism_group(canonicalize=False)
917
Subgroup 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)
918
"""
919
legs = sorted(sum(self._legs, []))
920
G, _, edge_to_legs, partition = self._graph()
921
# NOTE: this is too much generators. It is enough to move around the multiple
922
# edges in each orbit of the vertex automorphism group.
923
S = SymmetricGroup(legs)
924
gens = []
925
for g in self.vertex_automorphism_group().gens():
926
gens.append(self.vertex_automorphism_lift(g, S))
927
for g in self.leg_automorphism_group_vertex_stabilizer().gens():
928
gens.append(g)
929
930
return S.subgroup(gens, *args, **kwds)
931
932
def automorphism_number(self):
933
r"""
934
Return the size of the automorphism group (acting on legs).
935
936
EXAMPLES::
937
938
sage: from admcycles import StableGraph
939
sage: G = StableGraph([0, 2], [[1, 2, 4], [3, 5]], [(4, 5)])
940
sage: G.automorphism_number()
941
1
942
943
sage: G = StableGraph([0, 0, 0], [[1,2,7,8], [3,4,9,10], [5,6,11,12]],
944
....: [(2,3),(4,5),(6,1),(8,9),(10,11),(12,7)])
945
sage: G.automorphism_number()
946
48
947
sage: G.leg_automorphism_group().cardinality()
948
48
949
950
sage: G = StableGraph([0, 0], [[1, 1, 2], [1, 1, 3]], [(2, 3)])
951
sage: G.automorphism_number()
952
Traceback (most recent call last):
953
...
954
NotImplementedError: automorphism_number not valid for repeated marking
955
956
TESTS::
957
958
sage: from sage.combinat.permutation import Permutations
959
sage: glist=[1,1,2,2]
960
sage: for p in Permutations([0,1,2,3]):
961
....: 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)])
962
....: assert gr.automorphism_number() == 4, (gr, gr.automorphism_number())
963
"""
964
G, _, _, _ = self._graph()
965
aut = self.vertex_automorphism_group().cardinality()
966
967
# edge automorphism
968
for i, j, lab in G.edges(sort=False):
969
aut *= factorial(lab)
970
if i == j:
971
aut *= 2**lab
972
973
# explicitly forbid marking multiplicity
974
markings = self.list_markings()
975
if len(markings) != len(set(markings)):
976
raise NotImplementedError("automorphism_number not valid for repeated marking")
977
978
return aut
979
980
def g(self):
981
r"""
982
Return the genus of this stable graph
983
984
EXAMPLES::
985
986
sage: from admcycles import StableGraph
987
sage: G = StableGraph([1,2],[[1,2], [3,4]], [(1,3),(2,4)])
988
sage: G.g()
989
4
990
"""
991
return sum(self._genera) + len(self._edges) - len(self._genera) + ZZ.one()
992
993
def n(self):
994
r"""
995
Return the number of legs of the stable graph.
996
997
EXAMPLES::
998
999
sage: from admcycles import StableGraph
1000
sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)]);G.n()
1001
2
1002
"""
1003
return len(self.list_markings())
1004
1005
def genera(self, i=None, copy=True):
1006
"""
1007
Return the list of genera of the stable graph.
1008
1009
If an integer argument is given, return the genus at this vertex.
1010
1011
EXAMPLES::
1012
1013
sage: from admcycles import StableGraph
1014
sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])
1015
sage: G.genera()
1016
[1, 2]
1017
sage: G.genera(0), G.genera(1)
1018
(1, 2)
1019
"""
1020
if i is None:
1021
return self._genera[:] if copy else self._genera
1022
else:
1023
return self._genera[i]
1024
1025
def edges(self, copy=True):
1026
r"""
1027
Return the list of edges.
1028
1029
By default, this returns a copy of the corresponding list,
1030
set copy=False to obtain the internal variable of the graph.
1031
1032
EXAMPLES::
1033
1034
sage: from admcycles import StableGraph
1035
sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7)])
1036
sage: L = Gamma.edges(); L
1037
[(2, 3), (4, 5), (6, 7)]
1038
sage: L.append((8,9)); L
1039
[(2, 3), (4, 5), (6, 7), (8, 9)]
1040
sage: Gamma # not changed by above operation
1041
[1, 1] [[1, 2, 3, 4, 5, 6], [7, 8, 9]] [(2, 3), (4, 5), (6, 7)]
1042
"""
1043
return self._edges[:] if copy else self._edges
1044
1045
def num_verts(self):
1046
r"""
1047
Return the number of vertices.
1048
1049
EXAMPLES::
1050
1051
sage: from admcycles import StableGraph
1052
sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])
1053
sage: Gamma.num_verts()
1054
2
1055
"""
1056
return len(self._genera)
1057
1058
def legs(self, v=None, copy=True):
1059
r"""
1060
Return the list of legs at vertex v, or the whole list of
1061
legs if v is not specified.
1062
1063
By default, this returns a copy of the corresponding list(s),
1064
set copy=False to obtain the internal variable of the graph.
1065
1066
EXAMPLES::
1067
1068
sage: from admcycles import StableGraph
1069
sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7)])
1070
sage: L = Gamma.legs(0); L
1071
[1, 2, 3, 4, 5, 6]
1072
sage: L.append(10); L
1073
[1, 2, 3, 4, 5, 6, 10]
1074
sage: Gamma # not changed by above operation
1075
[1, 1] [[1, 2, 3, 4, 5, 6], [7, 8, 9]] [(2, 3), (4, 5), (6, 7)]
1076
sage: Gamma.legs()
1077
[[1, 2, 3, 4, 5, 6], [7, 8, 9]]
1078
"""
1079
if v is None:
1080
return [l[:] for l in self._legs] if copy else self._legs
1081
else:
1082
return self._legs[v][:] if copy else self._legs[v]
1083
1084
# TODO: deprecate
1085
numvert = num_verts
1086
1087
def num_edges(self):
1088
r"""
1089
Return the number of edges.
1090
1091
EXAMPLES::
1092
1093
sage: from admcycles import StableGraph
1094
sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])
1095
sage: Gamma.num_edges()
1096
4
1097
"""
1098
return len(self._edges)
1099
1100
def num_loops(self):
1101
r"""
1102
Return the number of loops (ie edges attached twice to a vertex).
1103
1104
EXAMPLES::
1105
1106
sage: from admcycles import StableGraph
1107
sage: Gamma = StableGraph([1,1], [[1,2,3,4,5,6],[7,8,9]], [(2,3), (4,5), (6,7), (8,9)])
1108
sage: Gamma.num_loops()
1109
3
1110
"""
1111
n = 0
1112
for h0, h1 in self._edges:
1113
n += self.vertex(h0) == self.vertex(h1)
1114
return n
1115
1116
def num_legs(self, i=None):
1117
r"""
1118
Return the number of legs at vertex i, or the total number of legs
1119
if i is not specified.
1120
1121
EXAMPLES::
1122
1123
sage: from admcycles import StableGraph
1124
sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)])
1125
sage: Gamma.num_legs(0)
1126
4
1127
sage: Gamma.num_legs()
1128
6
1129
"""
1130
if i is None:
1131
return sum(len(l) for l in self._legs)
1132
else:
1133
return len(self._legs[i])
1134
1135
def _graph_(self):
1136
r"""
1137
Return the Sage graph object encoding the stable graph
1138
1139
This inserts a vertex with label (i,j) in the middle of the edge (i,j).
1140
Also inserts a vertex with label ('L',i) at the end of each leg l.
1141
1142
EXAMPLES::
1143
1144
sage: from admcycles import StableGraph
1145
sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])
1146
sage: SageGr = Gr._graph_()
1147
sage: SageGr
1148
Multi-graph on 5 vertices
1149
sage: SageGr.vertices(sort=False) # random
1150
[(1, 2), 0, 1, (3, 5), ('L', 7)]
1151
"""
1152
G = Graph(multiedges=True)
1153
for i, j in self._edges:
1154
G.add_vertex((i, j))
1155
G.add_edge((i, j), self.vertex(i))
1156
G.add_edge((i, j), self.vertex(j))
1157
for i in self.list_markings():
1158
G.add_edge(('L', i), self.vertex(i))
1159
return G
1160
1161
def dim(self, v=None):
1162
"""
1163
Return dimension of moduli space at vertex v.
1164
1165
If v=None, return dimension of entire stratum parametrized by graph.
1166
1167
EXAMPLES::
1168
1169
sage: from admcycles import StableGraph
1170
sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])
1171
sage: Gr.dim(0), Gr.dim(1), Gr.dim()
1172
(10, 13, 23)
1173
"""
1174
if v is None:
1175
return sum([self.dim(v) for v in range(len(self._genera))])
1176
return 3 * self._genera[v] - 3 + len(self._legs[v])
1177
1178
# TODO: deprecate and remove
1179
def invariant(self):
1180
r"""
1181
Return a graph-invariant in form of a tuple of integers or tuples
1182
1183
At the moment this assumes that we only compare stable graph with same total
1184
g and set of markings
1185
currently returns sorted list of (genus,degree) for vertices
1186
1187
IDEAS:
1188
1189
* number of self-loops for each vertex
1190
1191
EXAMPLES::
1192
1193
sage: from admcycles import StableGraph
1194
sage: Gr = StableGraph([3,5],[[1,3,5,7],[2]],[(1,2),(3,5)])
1195
sage: Gr.invariant()
1196
((3, 4, (7,)), (5, 1, ()))
1197
"""
1198
return tuple(sorted([(self._genera[v], len(self._legs[v]), tuple(sorted(self.list_markings(v)))) for v in range(len(self._genera))]))
1199
1200
def tidy_up(self):
1201
r"""
1202
Sort legs and edges.
1203
1204
EXAMPLES::
1205
1206
sage: from admcycles import StableGraph
1207
sage: S = StableGraph([1, 3, 2], [[1, 5, 4], [2, 3, 6], [9, 8, 7]], [(9,3), (1,2)], mutable=True)
1208
sage: S.tidy_up()
1209
sage: S
1210
[1, 3, 2] [[1, 4, 5], [2, 3, 6], [7, 8, 9]] [(1, 2), (3, 9)]
1211
"""
1212
if not self._mutable:
1213
raise ValueError("the graph is immutable; use a copy instead")
1214
for e in range(len(self._edges)):
1215
if self._edges[e][0] > self._edges[e][1]:
1216
self._edges[e] = (self._edges[e][1], self._edges[e][0])
1217
for legs in self._legs:
1218
legs.sort()
1219
self._edges.sort()
1220
self._maxleg = max([max(j + [0]) for j in self._legs])
1221
1222
def vertex(self, l):
1223
r"""
1224
Return vertex number of leg l.
1225
1226
EXAMPLES::
1227
1228
sage: from admcycles import StableGraph
1229
sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)])
1230
sage: Gamma.vertex(1)
1231
0
1232
sage: Gamma.vertex(6)
1233
1
1234
"""
1235
for v in range(len(self._legs)):
1236
if l in self._legs[v]:
1237
return v
1238
return -1 # self does not have leg l
1239
1240
def list_markings(self, v=None):
1241
r"""
1242
Return the list of markings (non-edge legs) of self at vertex v.
1243
1244
EXAMPLES::
1245
1246
sage: from admcycles import StableGraph
1247
1248
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1249
sage: gam.list_markings(0)
1250
(3, 7)
1251
sage: gam.list_markings()
1252
(3, 7, 4)
1253
"""
1254
if v is None:
1255
return tuple([j for v in range(len(self._genera))
1256
for j in self.list_markings(v)])
1257
s = set(self._legs[v])
1258
for e in self._edges:
1259
s -= set(e)
1260
return tuple(s)
1261
1262
def leglist(self):
1263
r"""
1264
Return the list of legs
1265
1266
EXAMPLES::
1267
1268
sage: from admcycles import StableGraph
1269
1270
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1271
sage: gam.leglist()
1272
[1, 3, 7, 2, 4]
1273
"""
1274
return [j for leg in self._legs for j in leg]
1275
1276
def halfedges(self):
1277
r"""
1278
Return the tuple containing all half-edges, i.e. legs belonging to an edge
1279
1280
EXAMPLES::
1281
1282
sage: from admcycles import StableGraph
1283
1284
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1285
sage: gam.halfedges()
1286
(1, 2)
1287
"""
1288
return tuple(j for ed in self._edges for j in ed)
1289
1290
def edges_between(self, i, j):
1291
r"""
1292
Return 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 once
1293
1294
EXAMPLES::
1295
1296
sage: from admcycles import StableGraph
1297
1298
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1299
sage: gam.edges_between(0, 1)
1300
[(1, 2)]
1301
sage: gam.edges_between(0, 0)
1302
[]
1303
"""
1304
if i == j:
1305
return [e for e in self._edges if (e[0] in self._legs[i] and e[1] in self._legs[j])]
1306
else:
1307
return [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])]
1308
1309
def forget_markings(self, markings):
1310
r"""
1311
Erase all legs in the list markings, do not check if this gives well-defined graph
1312
1313
EXAMPLES::
1314
1315
sage: from admcycles import StableGraph
1316
sage: Gamma = StableGraph([0,2], [[1,2,3,4],[6,8]], [(4,6)], mutable=True)
1317
sage: Gamma.forget_markings([1,3,8])
1318
[0, 2] [[2, 4], [6]] [(4, 6)]
1319
"""
1320
if not self._mutable:
1321
raise ValueError("the graph is not mutable; use a copy instead")
1322
for m in markings:
1323
self._legs[self.vertex(m)].remove(m)
1324
return self
1325
1326
def leginversion(self, l):
1327
r"""
1328
Returns l' if l and l' form an edge, otherwise returns l
1329
1330
EXAMPLES::
1331
1332
sage: from admcycles import StableGraph
1333
1334
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1335
sage: gam.leginversion(1)
1336
2
1337
"""
1338
for a, b in self._edges:
1339
if a == l:
1340
return b
1341
if b == l:
1342
return a
1343
return l
1344
1345
def stabilize(self):
1346
r"""
1347
Stabilize this stable graph.
1348
1349
(all vertices with genus 0 have at least three markings) and returns
1350
``(dicv,dicl,dich)`` where
1351
1352
- dicv a dictionary sending new vertex numbers to the corresponding old
1353
vertex numbers
1354
1355
- dicl a dictionary sending new marking names to the label of the last
1356
half-edge that they replaced during the stabilization this happens
1357
for instance if a g=0 vertex with marking m and half-edge l
1358
(belonging to edge (l,l')) is stabilized: l' is replaced by m, so
1359
dicl[m]=l'
1360
1361
- dich a dictionary sending half-edges that vanished in the
1362
stabilization process to the last half-edge that replaced them this
1363
happens if a g=0 vertex with two half-edges a,b (whose corresponding
1364
half-edges are c,d) is stabilized: we obtain an edge (c,d) and a ->
1365
d, b -> d we assume here that a stabilization exists (all connected
1366
components have 2g-2+n>0)
1367
1368
EXAMPLES::
1369
1370
sage: from admcycles import StableGraph
1371
1372
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)
1373
sage: gam.stabilize()
1374
({0: 0, 1: 1}, {}, {})
1375
1376
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1377
sage: gam.stabilize()
1378
Traceback (most recent call last):
1379
...
1380
ValueError: the graph is not mutable; use a copy instead
1381
1382
sage: g = StableGraph([0,0,0], [[1,5,6],[2,3],[4,7,8]], [(1,2),(3,4),(5,6),(7,8)], mutable=True)
1383
sage: g.stabilize()
1384
({0: 0, 1: 2}, {}, {2: 4, 3: 1})
1385
sage: h = StableGraph([0, 0, 0], [[1], [2,3], [4]], [(1,2), (3,4)], mutable=True)
1386
sage: h.stabilize()
1387
({0: 2}, {}, {})
1388
"""
1389
if not self._mutable:
1390
raise ValueError("the graph is not mutable; use a copy instead")
1391
markings = self.list_markings()
1392
stable = False
1393
verteximages = list(range(len(self._genera)))
1394
dicl = {}
1395
dich = {}
1396
while not stable:
1397
numvert = len(self._genera)
1398
count = 0
1399
stable = True
1400
while count < numvert:
1401
if self._genera[count] == 0 and len(self._legs[count]) == 1:
1402
stable = False
1403
e0 = self._legs[count][0]
1404
e1 = self.leginversion(e0)
1405
v1 = self.vertex(e1)
1406
self._genera.pop(count)
1407
verteximages.pop(count)
1408
numvert -= 1
1409
self._legs[v1].remove(e1)
1410
self._legs.pop(count)
1411
try:
1412
self._edges.remove((e0, e1))
1413
except ValueError:
1414
self._edges.remove((e1, e0))
1415
elif self._genera[count] == 0 and len(self._legs[count]) == 2:
1416
stable = False
1417
e0 = self._legs[count][0]
1418
e1 = self._legs[count][1]
1419
1420
if e1 in markings:
1421
swap = e0
1422
e0 = e1
1423
e1 = swap
1424
1425
e1prime = self.leginversion(e1)
1426
v1 = self.vertex(e1prime)
1427
self._genera.pop(count)
1428
verteximages.pop(count)
1429
numvert -= 1
1430
1431
if e0 in markings:
1432
dicl[e0] = e1prime
1433
self._legs[v1].remove(e1prime)
1434
self._legs[v1].append(e0)
1435
self._legs.pop(count)
1436
try:
1437
self._edges.remove((e1, e1prime))
1438
except ValueError:
1439
self._edges.remove((e1prime, e1))
1440
else:
1441
e0prime = self.leginversion(e0)
1442
self._legs.pop(count)
1443
try:
1444
self._edges.remove((e0, e0prime))
1445
except ValueError:
1446
self._edges.remove((e0prime, e0))
1447
try:
1448
self._edges.remove((e1, e1prime))
1449
except ValueError:
1450
self._edges.remove((e1prime, e1))
1451
self._edges.append((e0prime, e1prime))
1452
1453
# update dich
1454
dich[e0] = e1prime
1455
dich[e1] = e0prime
1456
dich.update({h: e1prime for h in dich if dich[h] == e0})
1457
dich.update({h: e0prime for h in dich if dich[h] == e1})
1458
else:
1459
count += 1
1460
return ({i: verteximages[i] for i in range(len(verteximages))}, dicl, dich)
1461
1462
def degenerations(self, v=None, mutable=False):
1463
r"""
1464
Run through the list of all possible degenerations of this graph.
1465
1466
A degeneration happens by adding an edge at v or splitting it
1467
into two vertices connected by an edge the new edge always
1468
comes last in the list of edges.
1469
1470
If v is None, return all degenerations at all vertices.
1471
1472
EXAMPLES::
1473
1474
sage: from admcycles import StableGraph
1475
1476
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1477
sage: list(gam.degenerations(1))
1478
[[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)],
1479
[3, 0, 5] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],
1480
[3, 1, 4] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],
1481
[3, 1, 4] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],
1482
[3, 1, 4] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],
1483
[3, 1, 4] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)],
1484
[3, 2, 3] [[1, 3, 7], [8], [2, 4, 9]] [(1, 2), (8, 9)],
1485
[3, 2, 3] [[1, 3, 7], [2, 8], [4, 9]] [(1, 2), (8, 9)],
1486
[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)],
1487
[3, 2, 3] [[1, 3, 7], [2, 4, 8], [9]] [(1, 2), (8, 9)]]
1488
1489
All these graphs are immutable (or mutable when ``mutable=True``)::
1490
1491
sage: any(g.is_mutable() for g in gam.degenerations())
1492
False
1493
sage: all(g.is_mutable() for g in gam.degenerations(mutable=True))
1494
True
1495
1496
TESTS::
1497
1498
sage: from admcycles import StableGraph
1499
sage: G = StableGraph([2,2,2], [[1],[2,3],[4]], [(1,2),(3,4)])
1500
sage: sum(1 for v in range(3) for _ in G.degenerations(v))
1501
8
1502
sage: sum(1 for _ in G.degenerations())
1503
8
1504
"""
1505
if v is None:
1506
for v in range(self.num_verts()):
1507
for h in self.degenerations(v, mutable):
1508
yield h
1509
return
1510
1511
g = self._genera[v]
1512
l = len(self._legs[v])
1513
1514
# for positive genus: add loop to v and decrease genus
1515
if g > 0:
1516
G = self.copy()
1517
G.degenerate_nonsep(v)
1518
if not mutable:
1519
G.set_immutable()
1520
yield G
1521
1522
# now all graphs with separating edge : separate in (g1,M) and (g-g1,legs[v]-M), take note of symmetry and stability
1523
for g1 in range(floor(g / 2) + 1):
1524
for M in Subsets(set(self._legs[v])):
1525
if (g1 == 0 and len(M) < 2) or \
1526
(g == g1 and l - len(M) < 2) or \
1527
(2 * g1 == g and l > 0 and (not self._legs[v][0] in M)):
1528
continue
1529
G = self.copy()
1530
G.degenerate_sep(v, g1, M)
1531
if not mutable:
1532
G.set_immutable()
1533
yield G
1534
1535
def newleg(self):
1536
r"""
1537
Create two new leg-indices that can be used to create an edge
1538
1539
This modifies ``self._maxleg``.
1540
1541
EXAMPLES::
1542
1543
sage: from admcycles import StableGraph
1544
1545
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1546
sage: gam.newleg()
1547
(8, 9)
1548
"""
1549
self._maxleg += 2
1550
return (self._maxleg - 1, self._maxleg)
1551
1552
def rename_legs(self, dic, shift=None, inplace=True, return_dicts=False, mutable=False, tidyup=True):
1553
r"""
1554
Rename the markings according to the dictionary ``dic``.
1555
1556
Return a new stable graph which is isomorphic to self up to the
1557
change of markings given in ``dic``.
1558
1559
If ``return_dicts`` is set to ``True``, return a triple
1560
``(new_stable_graph, markings_relabelling, all_legs_relabelling)``
1561
where
1562
1563
- ``markings_relabelling`` is the dictionary whose keys are the marking of
1564
the current graph and the values are the new markings
1565
1566
- ``all_legs_relabelling`` is a dictionary of all the marking relabelling
1567
(this argument could be forwarded to :meth:`StableGraph.relabel`)
1568
1569
INPUT:
1570
1571
dic : dictionary
1572
the relabeling old label -> new label
1573
1574
shift : integer
1575
if provided, perform the corresponding shift on the non-marked legs
1576
1577
inplace : boolean (default ``True``)
1578
whether to do an inplace modification or return a new stable graph
1579
1580
return_dicts : boolean
1581
whether to return the extra relabelling information
1582
1583
mutable : boolean (default ``False``)
1584
whether the return graph should be mutable (ignored when ``inplace=True``)
1585
1586
EXAMPLES::
1587
1588
sage: from admcycles import StableGraph
1589
1590
sage: g = StableGraph([0], [[1,2]], [], mutable=True)
1591
sage: g.rename_legs({1: 3})
1592
[0] [[2, 3]] []
1593
1594
sage: g = StableGraph([0, 0], [[1,3,4],[2,5]], [(4,5)], mutable=True)
1595
sage: gg, rel, extra = g.rename_legs({1:2, 2:1}, return_dicts=True)
1596
sage: gg
1597
[0, 0] [[2, 3, 4], [1, 5]] [(4, 5)]
1598
sage: rel == {1: 2, 2: 1, 3: 3}
1599
True
1600
1601
A graph involving some changes in non-marking legs::
1602
1603
sage: g = StableGraph([0, 0], [[1, 4], [2, 3, 5]], [(4, 5)])
1604
sage: g.rename_legs({1: 2, 2: 4, 3: 6}, inplace=False)
1605
[0, 0] [[1, 2], [4, 5, 6]] [(1, 5)]
1606
sage: gg, markings_dic, legs_dic = g.rename_legs({1: 2, 2: 4, 3: 6}, inplace=False, return_dicts=True)
1607
sage: gg
1608
[0, 0] [[1, 2], [4, 5, 6]] [(1, 5)]
1609
sage: markings_dic == {1: 2, 2: 4, 3: 6}
1610
True
1611
sage: legs_dic == {1: 2, 2: 4, 3: 6, 4: 1, 5: 5}
1612
True
1613
1614
Using the ``shift`` argument::
1615
1616
sage: g = StableGraph([0], [[1, 2, 3]], [(2, 3)], mutable=True)
1617
sage: g.rename_legs({1: 5}, shift=1, inplace=False)
1618
[0] [[3, 4, 5]] [(3, 4)]
1619
sage: gg, markings_dic, legs_dic = g.rename_legs({1: 5}, shift=1, inplace=False, return_dicts=True)
1620
sage: gg
1621
[0] [[3, 4, 5]] [(3, 4)]
1622
sage: markings_dic == {1: 5}
1623
True
1624
sage: legs_dic == {1: 5, 2: 3, 3: 4}
1625
True
1626
1627
This method forbids renaming internal legs (for that purpose use :meth:`relabel`)::
1628
1629
sage: g = StableGraph([0], [[1,2,3]], [(2,3)], mutable=True)
1630
sage: g.rename_legs({2:5})
1631
Traceback (most recent call last):
1632
...
1633
ValueError: non-marking legs [2] in dic (use the relabel method instead)
1634
"""
1635
# 1. compute an admissible relabelling of the legs
1636
markings = self.list_markings()
1637
markings_relabelling = {i: dic.get(i, i) for i in markings}
1638
bad_legs = [i for i in dic if i not in markings]
1639
if bad_legs:
1640
raise ValueError('non-marking legs {} in dic (use the relabel method instead)'.format(bad_legs))
1641
1642
if shift is not None:
1643
# replace v[j] by markings_relabelling[v[j]] if v[j] in markings_relabelling and leave at v[j] otherwise
1644
all_legs_relabelling = {i: i + shift for e in self._edges for i in e}
1645
all_legs_relabelling.update(markings_relabelling)
1646
else:
1647
image_markings = tuple(sorted(markings_relabelling.values()))
1648
for i in range(len(image_markings) - 1):
1649
if image_markings[i + 1] == image_markings[i]:
1650
raise ValueError('repeated marking in the image')
1651
if markings == image_markings:
1652
# no need to modify any non-marking leg
1653
all_legs_relabelling = {i: i for e in self._edges for i in e}
1654
all_legs_relabelling.update(markings_relabelling)
1655
else:
1656
# find a minimal relabelling avoiding collisions by recycling the forgotten markings
1657
k = 0
1658
forget = [i for i in markings if i not in image_markings]
1659
all_legs = []
1660
for l in self._legs:
1661
all_legs.extend(l)
1662
all_legs_relabelling = {i: i for e in self._edges for i in e}
1663
for i in image_markings:
1664
if i in markings or i not in all_legs_relabelling:
1665
continue
1666
# exchange i with an unused marking
1667
all_legs_relabelling[i] = forget[k]
1668
k += 1
1669
all_legs_relabelling.update(markings_relabelling)
1670
1671
# 2. actually perform the relabelling
1672
if inplace:
1673
result = self
1674
result.relabel({}, all_legs_relabelling, inplace=True)
1675
if tidyup: # by default it will sort the list of legs and edges
1676
result.tidy_up()
1677
else:
1678
result = self.relabel({}, all_legs_relabelling, inplace=False, mutable=True)
1679
if tidyup: # by default it will sort the list of legs and edges
1680
result.tidy_up()
1681
if not mutable:
1682
result.set_immutable()
1683
1684
# 3. return the result
1685
return (result, markings_relabelling, all_legs_relabelling) if return_dicts else result
1686
1687
def degenerate_sep(self, v, g1, M):
1688
r"""
1689
degenerate vertex v into two vertices with genera g1 and g(v)-g1 and legs M and complement
1690
1691
add new edge (e[0],e[1]) such that e[0] is in new vertex v, e[1] in last vertex, which is added
1692
1693
EXAMPLES::
1694
1695
sage: from admcycles import StableGraph
1696
1697
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)
1698
sage: G.degenerate_sep(1, 2, [4])
1699
sage: G
1700
[3, 2, 3] [[1, 3, 7], [4, 8], [2, 9]] [(1, 2), (8, 9)]
1701
1702
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1703
sage: G.degenerate_sep(1, 2, [4])
1704
Traceback (most recent call last):
1705
...
1706
ValueError: the graph is not mutable; use a copy instead
1707
"""
1708
if not self._mutable:
1709
raise ValueError("the graph is not mutable; use a copy instead")
1710
g = self._genera[v]
1711
oldleg = self._legs[v]
1712
e = self.newleg()
1713
1714
self._genera[v] = g1
1715
self._genera += [g - g1]
1716
self._legs[v] = list(M) + [e[0]]
1717
self._legs += [list(set(oldleg) - set(M)) + [e[1]]]
1718
self._edges += [e]
1719
1720
def degenerate_nonsep(self, v):
1721
"""
1722
EXAMPLES::
1723
1724
sage: from admcycles import StableGraph
1725
1726
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)], mutable=True)
1727
sage: G.degenerate_nonsep(1)
1728
sage: G
1729
[3, 4] [[1, 3, 7], [2, 4, 8, 9]] [(1, 2), (8, 9)]
1730
1731
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1732
sage: G.degenerate_nonsep(1)
1733
Traceback (most recent call last):
1734
...
1735
ValueError: the graph is not mutable; use a copy instead
1736
"""
1737
if not self._mutable:
1738
raise ValueError("the graph is not mutable; use a copy instead")
1739
e = self.newleg()
1740
self._genera[v] -= 1
1741
self._legs[v] += [e[0], e[1]]
1742
self._edges += [e]
1743
1744
def contract_edge(self, e, adddata=False):
1745
r"""
1746
Contracts the edge e=(e0,e1).
1747
1748
This method returns nothing by default. But if ``adddata`` is set to
1749
``True``, then it returns a tuple ``(av, edgegraph, vnum)`` where
1750
1751
- ``av`` is the the vertex in the modified graph on which previously
1752
the edge ``e`` had been attached
1753
1754
- ``edgegraph`` is a stable graph induced in self by the edge e
1755
(1-edge graph, all legs at the ends of this edge are considered as
1756
markings)
1757
1758
- ``vnum`` is a list of one or two vertices e was attached before the
1759
contraction (in the order in which they appear)
1760
1761
- ``diccv`` is a dictionary mapping old vertex numbers to new vertex
1762
numbers
1763
1764
EXAMPLES::
1765
1766
sage: from admcycles import StableGraph
1767
1768
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)
1769
sage: G.contract_edge((1,2))
1770
sage: G
1771
[8] [[3, 7, 4]] [(3, 7)]
1772
sage: G.contract_edge((3,7))
1773
sage: G
1774
[9] [[4]] []
1775
1776
sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)
1777
sage: G2.contract_edge((3,7))
1778
sage: G2.contract_edge((1,2))
1779
sage: G == G2
1780
True
1781
1782
sage: G2 = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2),(3,7)], mutable=True)
1783
sage: G2.contract_edge((3,7), adddata=True)
1784
(0, [3] [[1, 3, 7]] [(3, 7)], [0], {0: 0, 1: 1})
1785
1786
sage: G = StableGraph([1,1,1,1],[[1],[2,4],[3,5],[6]],[(1,2),(3,4),(5,6)],mutable=True)
1787
sage: G.contract_edge((3,4),True)
1788
(1, [1, 1] [[2, 4], [3, 5]] [(3, 4)], [1, 2], {0: 0, 1: 1, 2: 1, 3: 2})
1789
1790
sage: G = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1791
sage: G.contract_edge((1,2))
1792
Traceback (most recent call last):
1793
...
1794
ValueError: the graph is not mutable; use a copy instead
1795
"""
1796
if not self._mutable:
1797
raise ValueError("the graph is not mutable; use a copy instead")
1798
1799
v0 = self.vertex(e[0])
1800
v1 = self.vertex(e[1])
1801
1802
if v0 == v1: # contracting a loop
1803
if adddata:
1804
H = StableGraph([self._genera[v0]], [self._legs[v0][:]], [e])
1805
self._genera[v0] += 1
1806
self._legs[v0].remove(e[0])
1807
self._legs[v0].remove(e[1])
1808
self._edges.remove(e)
1809
1810
if adddata:
1811
return (v0, H, [v0], {v: v for v in range(len(self._genera))})
1812
1813
else: # contracting an edge between different vertices
1814
if v0 > v1:
1815
v0, v1 = v1, v0
1816
1817
if adddata:
1818
diccv = {v: v for v in range(v1)}
1819
diccv[v1] = v0
1820
diccv.update({v: v - 1 for v in range(v1 + 1, len(self._genera))})
1821
H = StableGraph([self._genera[v0], self._genera[v1]], [self._legs[v0][:], self._legs[v1][:]], [e])
1822
1823
g1 = self._genera.pop(v1)
1824
self._genera[v0] += g1
1825
l1 = self._legs.pop(v1)
1826
self._legs[v0] += l1
1827
self._legs[v0].remove(e[0])
1828
self._legs[v0].remove(e[1])
1829
self._edges.remove(e)
1830
1831
if adddata:
1832
return (v0, H, [v0, v1], diccv)
1833
1834
def glue_vertex(self, i, Gr, divGr={}, divs={}, dil={}):
1835
r"""
1836
Glues the stable graph ``Gr`` at the vertex ``i`` of this stable graph
1837
1838
optional arguments: if divGr/dil are given they are supposed to be a
1839
dictionary, which will be cleared and updated with the
1840
renaming-convention to pass from leg/vertex-names in Gr to
1841
leg/vertex-names in the glued graph similarly, divs will be a
1842
dictionary assigning vertex numbers in the old self the corresponding
1843
number in the new self necessary condition:
1844
1845
- every leg of i is also a leg in Gr
1846
- every leg of Gr that is not a leg of i in self gets a new name
1847
1848
EXAMPLES::
1849
1850
sage: from admcycles import StableGraph
1851
sage: Gamma = StableGraph([2,1], [[1,2,3,4],[5]], [(2,3),(4,5)], mutable=True)
1852
sage: Gr = StableGraph([0,1,1], [[1,2,5],[6,3,7],[8,4]], [(5,6),(7,8)])
1853
sage: divGr=dict()
1854
sage: divs=dict()
1855
sage: dil=dict()
1856
sage: Gamma.glue_vertex(0, Gr, divGr, divs, dil)
1857
sage: Gamma
1858
[1, 0, 1, 1] [[5], [1, 2, 10], [3, 11, 12], [4, 9]] [(4, 5), (9, 12), (10, 11)]
1859
sage: divGr
1860
{0: 1, 1: 2, 2: 3}
1861
sage: divs
1862
{1: 0}
1863
sage: dil
1864
{5: 10, 6: 11, 7: 12, 8: 9}
1865
"""
1866
if not self._mutable:
1867
raise ValueError("the graph is not mutable; use a copy instead")
1868
1869
divGr.clear()
1870
divs.clear()
1871
dil.clear()
1872
1873
# remove vertex i, legs corresponding to it and all self-edges
1874
selfedges_old = self.edges_between(i, i)
1875
self._genera.pop(i)
1876
legs_old = self._legs.pop(i)
1877
for e in selfedges_old:
1878
self._edges.remove(e)
1879
1880
# when gluing in Gr, make sure that new legs corresponding to edges inside Gr
1881
# or legs in Gr which are already attached to a vertex different from i in self get a new unique label in the glued graph
1882
m = max(self._maxleg, Gr._maxleg) # largest index used in either graph, new labels m+1, m+2, ...
1883
1884
Gr_new = Gr.copy()
1885
1886
a = []
1887
for l in Gr._legs:
1888
a += l
1889
a = set(a) # legs of Gr
1890
b = set(legs_old) # legs of self at vertex i
1891
1892
# set of legs of Gr that need to be relabeled: all legs of Gr that are not attached to vertex i in self
1893
e = a - b
1894
for l in e:
1895
m += 1
1896
dil[l] = m
1897
Gr_new.relabel({}, dil, inplace=True)
1898
1899
# vertex dictionaries
1900
for j in range(i):
1901
divs[j] = j
1902
for j in range(i + 1, len(self._genera) + 1):
1903
divs[j] = j - 1
1904
for j in range(len(Gr._genera)):
1905
divGr[j] = len(self._genera) + j
1906
1907
self._genera += Gr_new._genera
1908
self._legs += Gr_new._legs
1909
self._edges += Gr_new._edges
1910
1911
self.tidy_up()
1912
1913
# TODO: overlaps with relabel
1914
def reorder_vertices(self, vord):
1915
r"""
1916
Reorders vertices according to tuple given (permutation of range(len(self._genera)))
1917
"""
1918
if not self._mutable:
1919
raise ValueError("the graph is not mutable; use a copy instead")
1920
new_genera = [self._genera[j] for j in vord]
1921
new_legs = [self._legs[j] for j in vord]
1922
self._genera = new_genera
1923
self._legs = new_legs
1924
1925
def extract_subgraph(self, vertices, outgoing_legs=None, rename=True, mutable=False):
1926
r"""
1927
Extracts from self a subgraph induced by the list vertices
1928
1929
if the list outgoing_legs is given, the markings of the subgraph are
1930
called 1,2,..,m corresponding to the elements of outgoing_legs in this
1931
case, all edges involving outgoing edges should be cut returns a triple
1932
(Gamma,dicv,dicl), where
1933
1934
- Gamma is the induced subgraph
1935
1936
- dicv, dicl are (surjective) dictionaries associating vertex/leg
1937
labels in self to the vertex/leg labels in Gamma
1938
1939
if ``rename=False``, do not rename any legs when extracting
1940
1941
EXAMPLES::
1942
1943
sage: from admcycles import StableGraph
1944
1945
sage: gam = StableGraph([3,5],[[1,3,7],[2,4]],[(1,2)])
1946
sage: gam.extract_subgraph([0])
1947
([3] [[1, 2, 3]] [], {0: 0}, {1: 1, 3: 2, 7: 3})
1948
"""
1949
attachedlegs = set(l for v in vertices for l in self._legs[v])
1950
if outgoing_legs is None:
1951
alllegs = attachedlegs.copy()
1952
for (e0, e1) in self._edges:
1953
if e0 in alllegs and e1 in alllegs:
1954
alllegs.remove(e0)
1955
alllegs.remove(e1)
1956
outgoing_legs = list(alllegs)
1957
1958
shift = len(outgoing_legs) + 1
1959
if rename:
1960
dicl = {outgoing_legs[i]: i + 1 for i in range(shift - 1)}
1961
else:
1962
dicl = {l: l for l in self.leglist()}
1963
1964
genera = [self._genera[v] for v in vertices]
1965
legs = [[dicl.setdefault(l, l + shift) for l in self._legs[v]] for v in vertices]
1966
edges = [(dicl[e0], dicl[e1]) for (e0, e1) in self._edges if (
1967
e0 in attachedlegs and e1 in attachedlegs and (e0 not in outgoing_legs) and (e1 not in outgoing_legs))]
1968
dicv = {vertices[i]: i for i in range(len(vertices))}
1969
1970
return (StableGraph(genera, legs, edges, mutable=mutable), dicv, dicl)
1971
1972
def vanishes(self, moduli):
1973
if moduli == MODULI_ST:
1974
return False
1975
elif moduli == MODULI_TL:
1976
return self.num_edges() - self.num_loops() != self.num_verts() - 1
1977
elif moduli == MODULI_CT:
1978
return self.num_edges() != self.num_verts() - 1
1979
elif moduli == MODULI_RT:
1980
return (self.num_edges() != self.num_verts() - 1) or \
1981
sum(bool(g) for g in self._genera) != 1
1982
elif moduli == MODULI_SM:
1983
return bool(self._edges)
1984
else:
1985
raise ValueError('unknown moduli')
1986
1987
def boundary_pushforward(self, classes=None):
1988
r"""
1989
Computes the pushforward of a product of tautological classes (one for each vertex) under the
1990
boundary gluing map for this stable graph.
1991
1992
INPUT:
1993
1994
- ``classes`` -- list (default: `None`); list of tautclasses, one for each vertex of the stable
1995
graph. The genus of the ith class is assumed to be the genus of the ith vertex, the markings
1996
of the ith class are supposed to be 1, ..., ni where ni is the number of legs at the ith vertex.
1997
Note: the jth leg at vertex i corresponds to the marking j of the ith class.
1998
For classes=None, place the fundamental class at each vertex.
1999
2000
EXAMPLES::
2001
2002
sage: from admcycles import StableGraph, TautologicalRing
2003
sage: B=StableGraph([2,1],[[4,1,2],[3,5]],[(4,5)])
2004
sage: Bclass=B.boundary_pushforward() # class of undecorated boundary divisor
2005
sage: Bclass*Bclass
2006
Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]
2007
Polynomial : -psi_5 - psi_4
2008
sage: R1 = TautologicalRing(2,3)
2009
sage: R2 = TautologicalRing(1,2)
2010
sage: si1=B.boundary_pushforward([R1.fundamental_class(),-R2.psi(2)]); si1
2011
Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]
2012
Polynomial : -psi_5
2013
sage: si2=B.boundary_pushforward([-R1.psi(1), R2.fundamental_class()]); si2
2014
Graph : [2, 1] [[4, 1, 2], [3, 5]] [(4, 5)]
2015
Polynomial : -psi_4
2016
sage: a = Bclass*Bclass-si1-si2
2017
sage: a.simplify()
2018
0
2019
"""
2020
from .admcycles import prodtautclass
2021
return prodtautclass(self.copy(mutable=False), protaut=classes).pushforward()
2022
2023
# TODO: adapt to TautologicalRing
2024
def boundary_pullback(self, other):
2025
r"""
2026
Pulls back the TautologicalClass or decstratum other to self and
2027
returns a prodtautclass with gamma=self.
2028
2029
EXAMPLES::
2030
2031
sage: from admcycles import StableGraph, psiclass
2032
sage: G = StableGraph([0, 2], [[1, 2, 4, 3], [5]], [(3, 5)])
2033
sage: H = StableGraph([2],[[1,2,4]],[])
2034
sage: a = H.boundary_pushforward([psiclass(3,2,3)]); a
2035
Graph : [2] [[1, 2, 4]] []
2036
Polynomial : psi_4
2037
sage: G.boundary_pullback(a)
2038
Outer graph : [0, 2] [[1, 2, 4, 3], [5]] [(3, 5)]
2039
Vertex 0 :
2040
Graph : [0] [[1, 2, 3, 4]] []
2041
Polynomial : psi_3
2042
Vertex 1 :
2043
Graph : [2] [[1]] []
2044
Polynomial : 1
2045
"""
2046
from .tautological_ring import TautologicalClass
2047
from .admcycles import decstratum
2048
2049
if isinstance(other, TautologicalClass):
2050
from .admcycles import prodtautclass
2051
result = prodtautclass(self.copy(mutable=False), [])
2052
for t in other._terms.values():
2053
result += self.boundary_pullback(t)
2054
return result
2055
elif isinstance(other, decstratum):
2056
# NOTE: this is using much more than just stable graphs
2057
# I would suggest to move it out of this class
2058
from .admcycles import (common_degenerations, prodtautclass,
2059
onekppoly, kppoly, kappacl, psicl)
2060
rename = not (set(self.list_markings()) == set(range(1, self.n() + 1)))
2061
commdeg = common_degenerations(self, other.gamma, modiso=True, rename=rename)
2062
result = prodtautclass(self.copy(mutable=False), [])
2063
2064
for (Gamma, dicv1, dicl1, dicv2, dicl2) in commdeg:
2065
numvert = len(Gamma._genera)
2066
# first determine edges that are covered by self and other - for excess int. terms
2067
legcount = {l: 0 for l in Gamma.leglist()}
2068
for l in dicl1.values():
2069
legcount[l] += 1
2070
for l in dicl2.values():
2071
legcount[l] += 1
2072
2073
excesspoly = onekppoly(numvert)
2074
for e in Gamma._edges:
2075
if legcount[e[0]] == 2:
2076
excesspoly *= ((-1) * (psicl(e[0], numvert) + psicl(e[1], numvert)))
2077
2078
# partition vertices of Gamma according to where they go in self
2079
v1preim = [[] for v in range(len(self._genera))]
2080
for w in dicv1:
2081
v1preim[dicv1[w]].append(w)
2082
2083
graphpartition = [Gamma.extract_subgraph(
2084
v1preim[v], outgoing_legs=[dicl1[l] for l in self._legs[v]]) for v in range(len(self._genera))]
2085
2086
v2preim = [[] for v in range(len(other.gamma._genera))]
2087
for w in dicv2:
2088
v2preim[dicv2[w]].append(w)
2089
2090
resultpoly = kppoly([], [])
2091
# 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 terms
2092
for (kappa, psi, coeff) in other.poly:
2093
# psi-classes are transferred by dicl2, but kappa-classes might be split up if dicv2 has multiple preimages of same vertex
2094
# multiply everything together in a kppoly on Gamma, then split up this polynomial according to graphpartition
2095
psipolydict = {dicl2[l]: psi[l] for l in psi}
2096
psipoly = kppoly([([[] for i in range(numvert)], psipolydict)], [1])
2097
2098
kappapoly = prod([prod([sum([kappacl(w, k + 1, numvert) for w in v2preim[v]])**kappa[v][k]
2099
for k in range(len(kappa[v]))]) for v in range(len(other.gamma._genera))])
2100
2101
resultpoly += coeff * psipoly * kappapoly
2102
2103
resultpoly *= excesspoly
2104
# TODO: filter for terms that vanish by dimension reasons?
2105
2106
# now fiddle the terms of resultpoly apart and distribute them to graphpartition
2107
for (kappa, psi, coeff) in resultpoly:
2108
decstratlist = []
2109
for v in range(len(self._genera)):
2110
kappav = [kappa[w] for w in v1preim[v]]
2111
psiv = {graphpartition[v][2][l]: psi[l] for l in graphpartition[v][2] if l in psi}
2112
decstratlist.append(decstratum(graphpartition[v][0], kappa=kappav, psi=psiv))
2113
tempresu = prodtautclass(self, [decstratlist])
2114
tempresu *= coeff
2115
result += tempresu
2116
return result
2117
else:
2118
raise TypeError('invalid input other={}'.format(other))
2119
2120
def to_tautological_class(self, R=None):
2121
r"""
2122
Return the pure boundary stratum associated to this stable graph.
2123
2124
Note: does not divide by automorphisms!
2125
2126
EXAMPLES::
2127
2128
sage: from admcycles import StableGraph
2129
2130
sage: g = StableGraph([0,0], [[1,3,4], [2,5,6]], [(3,5),(4,6)])
2131
sage: g.to_tautological_class()
2132
Graph : [0, 0] [[1, 3, 4], [2, 5, 6]] [(3, 5), (4, 6)]
2133
Polynomial : 1
2134
2135
TESTS::
2136
2137
sage: from admcycles import StableGraph
2138
2139
sage: g = StableGraph([0,0], [[1,3,4], [2,5,6]], [(3,5),(4,6)])
2140
sage: g.to_tautclass()
2141
doctest:...: DeprecationWarning: to_tautclass is deprecated. Please use to_tautological_class instead.
2142
See https://gitlab.com/modulispaces/admcycles/-/merge_requests/109 for details.
2143
Graph : [0, 0] [[1, 3, 4], [2, 5, 6]] [(3, 5), (4, 6)]
2144
Polynomial : 1
2145
"""
2146
if R is None:
2147
from .tautological_ring import TautologicalRing
2148
R = TautologicalRing(self.g(), self.n())
2149
return R(self)
2150
2151
to_tautclass = deprecated_function_alias(109, to_tautological_class)
2152
2153
def _vertex_module(self):
2154
nv = len(self._genera)
2155
return FreeModule(ZZ, nv)
2156
2157
def _edge_module(self):
2158
ne = len(self._edges)
2159
return FreeModule(ZZ, ne)
2160
2161
# TODO: implement cache as this is used intensively in the flow code below
2162
# See https://gitlab.com/modulispaces/admcycles/issues/14
2163
def spanning_tree(self, root=0):
2164
r"""
2165
Return a spanning tree.
2166
2167
INPUT:
2168
2169
- ``root`` - optional vertex for the root of the spanning tree
2170
2171
OUTPUT: a triple
2172
2173
- list of triples ``(vertex ancestor, sign, edge index)`` where the
2174
triple at position ``i`` correspond to vertex ``i``.
2175
2176
- the set of indices of extra edges not in the tree
2177
2178
- vertices sorted according to their heights in the tree (first element is the root
2179
and the end of the list are the leaves). In other words, a topological sort of
2180
the vertices with respect to the spanning tree.
2181
2182
EXAMPLES::
2183
2184
sage: from admcycles import StableGraph
2185
sage: G = StableGraph([0,0,0], [[1,2],[3,4,5],[6,7]], [(1,3),(4,6),(5,7)])
2186
sage: G.spanning_tree()
2187
([None, (0, -1, 0), (1, -1, 1)], {2}, [0, 1, 2])
2188
sage: G.spanning_tree(1)
2189
([(1, 1, 0), None, (1, -1, 1)], {2}, [1, 0, 2])
2190
sage: G.spanning_tree(2)
2191
([(1, 1, 0), (2, 1, 1), None], {2}, [2, 1, 0])
2192
"""
2193
from collections import deque
2194
2195
nv = len(self._genera)
2196
ne = len(self._edges)
2197
2198
out_edges = [[] for _ in range(nv)]
2199
for i, (lu, lv) in enumerate(self._edges):
2200
u = self.vertex(lu)
2201
v = self.vertex(lv)
2202
out_edges[u].append((v, -1, i))
2203
out_edges[v].append((u, 1, i))
2204
2205
ancestors = [None] * nv
2206
ancestors[root] = None
2207
unseen = [True] * nv
2208
unseen[root] = False
2209
todo = deque()
2210
todo.append(root)
2211
extra_edges = set(range(ne))
2212
vertices = [root]
2213
2214
while todo:
2215
u = todo.popleft()
2216
for v, s, i in out_edges[u]:
2217
if unseen[v]:
2218
ancestors[v] = (u, s, i)
2219
todo.append(v)
2220
unseen[v] = False
2221
extra_edges.remove(i)
2222
vertices.append(v)
2223
2224
return ancestors, extra_edges, vertices
2225
2226
def cycle_basis(self):
2227
r"""
2228
Return a basis of the cycles as vectors of length the number of edges in the graph.
2229
2230
The coefficient at a given index in a vector corresponds to the edge
2231
of this index in the list of edges of the stable graph. The coefficient is
2232
+1 or -1 depending on the orientation of the edge in the cycle.
2233
2234
EXAMPLES::
2235
2236
sage: from admcycles import StableGraph
2237
sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])
2238
sage: G.cycle_basis()
2239
[(1, 1, 0, 0), (1, 0, -1, 1)]
2240
"""
2241
ne = len(self._edges)
2242
V = self._edge_module()
2243
2244
ancestors, extra_edges, _ = self.spanning_tree()
2245
basis = []
2246
2247
for i in extra_edges:
2248
vec = [0] * ne
2249
2250
# contribution of the edge
2251
vec[i] = 1
2252
2253
lu, lv = self._edges[i]
2254
u = self.vertex(lu)
2255
v = self.vertex(lv)
2256
2257
# contribution of the path from root to u
2258
while ancestors[u] is not None:
2259
u, s, i = ancestors[u]
2260
vec[i] -= s
2261
2262
# contribution of the path from v to root
2263
while ancestors[v] is not None:
2264
v, s, i = ancestors[v]
2265
vec[i] += s
2266
2267
basis.append(V(vec))
2268
2269
return basis
2270
2271
def cycle_space(self):
2272
r"""
2273
Return the subspace of the edge module generated by the cycles.
2274
2275
EXAMPLES::
2276
2277
sage: from admcycles import StableGraph
2278
sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])
2279
sage: C = G.cycle_space()
2280
sage: C
2281
Free module of degree 4 and rank 2 over Integer Ring
2282
Echelon basis matrix:
2283
[ 1 0 -1 1]
2284
[ 0 1 1 -1]
2285
sage: G.flow_divergence(C.random_element()).is_zero()
2286
True
2287
"""
2288
return self._edge_module().submodule(self.cycle_basis())
2289
2290
def flow_divergence(self, flow):
2291
r"""
2292
Return the divergence of the given ``flow``.
2293
2294
The divergence at a given vertex is the sum of the flow on the outgoing
2295
edges at this vertex.
2296
2297
EXAMPLES::
2298
2299
sage: from admcycles import StableGraph
2300
sage: G = StableGraph([1,0,0], [[1,2],[3,4,5,6],[7,8]], [(1,3),(4,2),(5,8),(7,6)])
2301
sage: G.flow_divergence([1,1,1,1])
2302
(0, 0, 0)
2303
sage: G.flow_divergence([1,0,0,0])
2304
(1, -1, 0)
2305
"""
2306
flow = self._edge_module()(flow)
2307
deriv = self._vertex_module()()
2308
for i, (lu, lv) in enumerate(self._edges):
2309
u = self.vertex(lu)
2310
v = self.vertex(lv)
2311
deriv[u] += flow[i]
2312
deriv[v] -= flow[i]
2313
2314
return deriv
2315
2316
def flow_solve(self, vertex_weights):
2317
r"""
2318
Return a solution for the flow equation with given vertex weights.
2319
2320
EXAMPLES::
2321
2322
sage: from admcycles import StableGraph
2323
2324
sage: G = StableGraph([0,0,0], [[1,2,3], [4,5,6], [7,8]], [(1,4),(5,2),(3,7),(6,8)])
2325
sage: flow = G.flow_solve((-3, 2, 1))
2326
sage: flow
2327
(-2, 0, -1, 0)
2328
sage: G.flow_divergence(flow)
2329
(-3, 2, 1)
2330
sage: div = vector((-34, 27, 7))
2331
sage: flow = G.flow_solve(div)
2332
sage: G.flow_divergence(flow) == div
2333
True
2334
sage: C = G.cycle_space()
2335
sage: G.flow_divergence(flow + C.random_element()) == div
2336
True
2337
2338
sage: G = StableGraph([0,0,0], [[1],[2,3],[4]], [(1,2),(3,4)])
2339
sage: G.flow_solve((-1, 0, 1))
2340
(-1, -1)
2341
sage: G.flow_divergence((-1, -1))
2342
(-1, 0, 1)
2343
sage: G.flow_solve((-1, 3, -2))
2344
(-1, 2)
2345
sage: G.flow_divergence((-1, 2))
2346
(-1, 3, -2)
2347
2348
TESTS::
2349
2350
sage: V = ZZ**4
2351
sage: vectors = [V((-1, 0, 0, 1)), V((-3, 1, -2, 4)), V((5, 2, -13, 6))]
2352
sage: G1 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (5,6)])
2353
sage: G2 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (3,4), (6,5)])
2354
sage: G3 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(2,1), (3,4), (6,5)])
2355
sage: G4 = StableGraph([0,0,0,0], [[1], [2,3], [4,5], [6]], [(1,2), (4,3), (5,6)])
2356
sage: for G in [G1,G2,G3,G4]:
2357
....: for v in vectors:
2358
....: flow = G.flow_solve(v)
2359
....: assert G.flow_divergence(flow) == v, (v,flow,G)
2360
2361
sage: V = ZZ**6
2362
sage: 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)])
2363
sage: for _ in range(10):
2364
....: v0 = V.random_element()
2365
....: for u in V.basis():
2366
....: v = v0 - sum(v0) * u
2367
....: flow = G.flow_solve(v)
2368
....: assert G.flow_divergence(flow) == v, (v, flow)
2369
"""
2370
# NOTE: if we compute the divergence matrix, one can also use solve_right
2371
# directly. It might be faster on some large instances.
2372
nv = len(self._genera)
2373
if len(vertex_weights) != nv or sum(vertex_weights) != 0:
2374
raise ValueError("vertex_weights must have length nv and sum up to zero")
2375
2376
ancestors, _, vertices = self.spanning_tree()
2377
vertices.reverse() # move leaves first and root last
2378
vertices.pop() # remove the root
2379
2380
new_vertex_weights = self._vertex_module()(vertex_weights)
2381
if new_vertex_weights is vertex_weights and vertex_weights.is_mutable():
2382
new_vertex_weights = vertex_weights.__copy__()
2383
vertex_weights = new_vertex_weights
2384
2385
V = self._edge_module()
2386
vec = V()
2387
for u in vertices:
2388
v, s, i = ancestors[u]
2389
vec[i] += s * vertex_weights[u]
2390
vertex_weights[v] += vertex_weights[u]
2391
2392
return vec
2393
2394
def plot(self, vord=None, vpos=None, eheight=None):
2395
r"""
2396
Return a graphics object in which ``self`` is plotted.
2397
2398
If ``vord`` is ``None``, the method uses the default vertex order.
2399
2400
If ``vord`` is ``[]``, the parameter ``vord`` is used to give back the vertex order.
2401
2402
EXAMPLES::
2403
2404
sage: from admcycles import *
2405
sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])
2406
sage: G.plot()
2407
Graphics object consisting of 12 graphics primitives
2408
2409
TESTS::
2410
2411
sage: from admcycles import *
2412
sage: G = StableGraph([1,2],[[1,2],[3,4,5,6]],[(1,3),(2,4)])
2413
sage: vertex_order = []
2414
sage: P = G.plot(vord=vertex_order)
2415
sage: vertex_order
2416
[0, 1]
2417
"""
2418
# some parameters
2419
mark_dx = 1 # x-distance of different markings
2420
mark_dy = 1 # y-distance of markings from vertices
2421
mark_rad = 0.2 # radius of marking-circles
2422
v_dxmin = 1 # minimal x-distance of two vertices
2423
ed_dy = 0.7 # max y-distance of different edges between same vertex-pair
2424
2425
vsize = 0.4 # (half the) size of boxes representing vertices
2426
2427
if not vord:
2428
default_vord = list(range(len(self._genera)))
2429
if vord is None:
2430
vord = default_vord
2431
else:
2432
vord += default_vord
2433
reord_self = self.copy()
2434
reord_self.reorder_vertices(vord)
2435
2436
if not vpos:
2437
default_vpos = [(0, 0)]
2438
for i in range(1, len(self._genera)):
2439
default_vpos += [(default_vpos[i - 1][0] + mark_dx * (len(reord_self.list_markings(i - 1)) + 2 * len(reord_self.edges_between(
2440
i - 1, i - 1)) + len(reord_self.list_markings(i)) + 2 * len(reord_self.edges_between(i, i))) / ZZ(2) + v_dxmin, 0)]
2441
if vpos is None:
2442
vpos = default_vpos
2443
else:
2444
vpos += default_vpos
2445
2446
if not eheight:
2447
ned = {(i, j): 0 for i in range(len(reord_self._genera)) for j in range(i + 1, len(reord_self._genera))}
2448
default_eheight = {}
2449
for e in reord_self._edges:
2450
ver1 = reord_self.vertex(e[0])
2451
ver2 = reord_self.vertex(e[1])
2452
if ver1 != ver2:
2453
default_eheight[e] = abs(ver1 - ver2) - 1 + ned[(min(ver1, ver2), max(ver1, ver2))] * ed_dy
2454
ned[(min(ver1, ver2), max(ver1, ver2))] += 1
2455
2456
if eheight is None:
2457
eheight = default_eheight
2458
else:
2459
eheight.update(default_eheight)
2460
2461
# now the drawing starts
2462
# vertices
2463
vertex_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]],
2464
color='white', fill=True, edgecolor='black', thickness=1, zorder=2) + text('g=' + repr(reord_self._genera[i]), vpos[i], fontsize=20, color='black', zorder=3) for i in range(len(reord_self._genera))]
2465
2466
# non-self edges
2467
edge_graph = []
2468
for e in reord_self._edges:
2469
ver1 = reord_self.vertex(e[0])
2470
ver2 = reord_self.vertex(e[1])
2471
if ver1 != ver2:
2472
x = (vpos[ver1][0] + vpos[ver2][0]) / ZZ(2)
2473
y = (vpos[ver1][1] + vpos[ver2][1]) / ZZ(2) + eheight[e]
2474
edge_graph += [bezier_path([[vpos[ver1], (x, y), vpos[ver2]]], color='black', zorder=1)]
2475
2476
marking_graph = []
2477
2478
for v in range(len(reord_self._genera)):
2479
se_list = reord_self.edges_between(v, v)
2480
m_list = reord_self.list_markings(v)
2481
v_x0 = vpos[v][0] - (2 * len(se_list) + len(m_list) - 1) * mark_dx / ZZ(2)
2482
2483
for e in se_list:
2484
edge_graph += [bezier_path([[vpos[v], (v_x0, -mark_dy),
2485
(v_x0 + mark_dx, -mark_dy), vpos[v]]], zorder=1)]
2486
v_x0 += 2 * mark_dx
2487
for l in m_list:
2488
marking_graph += [line([vpos[v], (v_x0, -mark_dy)], color='black', zorder=1) + circle((v_x0, -mark_dy), mark_rad, fill=True,
2489
facecolor='white', edgecolor='black', zorder=2) + text(repr(l), (v_x0, -mark_dy), fontsize=10, color='black', zorder=3)]
2490
v_x0 += mark_dx
2491
G = sum(marking_graph) + sum(edge_graph) + sum(vertex_graph)
2492
G.axes(False)
2493
return G
2494
2495
def _unicode_art_(self):
2496
"""
2497
Return unicode art for the stable graph ``self``.
2498
2499
EXAMPLES::
2500
2501
sage: from admcycles import *
2502
sage: A = StableGraph([1, 2],[[1, 2, 3], [4]],[(3, 4)])
2503
sage: unicode_art(A)
2504
╭────╮
2505
3 4
2506
╭┴─╮ ╭┴╮
2507
│1 │ │2│
2508
╰┬┬╯ ╰─╯
2509
12
2510
2511
sage: A = StableGraph([333, 4444],[[1, 2, 3], [4]],[(3, 4)])
2512
sage: unicode_art(A)
2513
╭─────╮
2514
3 4
2515
╭┴──╮ ╭┴───╮
2516
│333│ │4444│
2517
╰┬┬─╯ ╰────╯
2518
12
2519
2520
sage: B = StableGraph([3,5], [[1,3,5],[2]], [(1,2),(3,5)])
2521
sage: unicode_art(B)
2522
╭─────╮
2523
│╭╮ │
2524
135 2
2525
╭┴┴┴╮ ╭┴╮
2526
│3 │ │5│
2527
╰───╯ ╰─╯
2528
2529
sage: C = StableGraph([3,5], [[3,1,5],[2,4]], [(1,2),(3,5)])
2530
sage: unicode_art(C)
2531
╭────╮
2532
╭─╮ │
2533
315 2
2534
╭┴┴┴╮ ╭┴╮
2535
│3 │ │5│
2536
╰───╯ ╰┬╯
2537
4
2538
"""
2539
from sage.typeset.unicode_art import UnicodeArt
2540
N = self.num_verts()
2541
all_half_edges = self.halfedges()
2542
half_edges = {i: [j for j in legs_i if j in all_half_edges]
2543
for i, legs_i in zip(range(N), self._legs)}
2544
open_edges = {i: [j for j in legs_i if j not in all_half_edges]
2545
for i, legs_i in zip(range(N), self._legs)}
2546
left_box = [u' ', u'╭', u'│', u'╰', u' ']
2547
right_box = [u' ', u'╮ ', u'│ ', u'╯ ', u' ']
2548
positions = {}
2549
boxes = UnicodeArt()
2550
total_width = 0
2551
for vertex in range(N):
2552
t = list(left_box)
2553
for v in half_edges[vertex]:
2554
w = str(v)
2555
positions[v] = total_width + len(t[0])
2556
t[0] += w
2557
t[1] += u'┴' + u'─' * (len(w) - 1)
2558
for v in open_edges[vertex]:
2559
w = str(v)
2560
t[4] += w
2561
t[3] += u'┬' + u'─' * (len(w) - 1)
2562
t[2] += str(self._genera[vertex])
2563
length = max(len(line) for line in t)
2564
for i in [0, 2, 4]:
2565
t[i] += u' ' * (length - len(t[i]))
2566
for i in [1, 3]:
2567
t[i] += u'─' * (length - len(t[i]))
2568
for i in range(5):
2569
t[i] += right_box[i]
2570
total_width += length + 2
2571
boxes += UnicodeArt(t)
2572
num_edges = self.num_edges()
2573
matchings = [[u' '] * (1 + max(positions.values()))
2574
for _ in range(num_edges)]
2575
for i, (a, b) in enumerate(self.edges()):
2576
xa = positions[a]
2577
xb = positions[b]
2578
if xa > xb:
2579
xa, xb = xb, xa
2580
for j in range(xa + 1, xb):
2581
matchings[i][j] = u'─'
2582
matchings[i][xa] = u'╭'
2583
matchings[i][xb] = u'╮'
2584
for j in range(i + 1, num_edges):
2585
matchings[j][xa] = u'│'
2586
matchings[j][xb] = u'│'
2587
matchings = [u''.join(line) for line in matchings]
2588
return UnicodeArt(matchings + list(boxes))
2589
2590
2591
# This function is about to be deprecated. Instead use
2592
# - StableGraph.is_isomorphic
2593
# - StableGraph.automorphism_number
2594
2595
2596
# computes union of dictionaries
2597
2598
def dicunion(*dicts):
2599
return dict(itertools.chain.from_iterable(dct.items() for dct in dicts))
2600
2601
2602
def GraphIsom(G, H, check=False):
2603
# TODO: Insert quick hash-check if G,H can be isomorphic at all
2604
if (G.invariant() != H.invariant()):
2605
return []
2606
2607
Isomlist = [] # list of isomorphisms that will be returned
2608
IsoV = {} # working vertex-dictionary
2609
IsoL = {j: j for j in G.list_markings()} # working leg-dictionary
2610
2611
for j in G.list_markings():
2612
vG = G.vertex(j)
2613
vH = H.vertex(j)
2614
2615
# if vertices containing j have different genera or degrees in G, there is no automorphism
2616
# also if the markings give contradictory information where the vertices go, there is no automorphism
2617
if (G._genera[vG] != H._genera[vH]) or (len(G._legs[vG]) != len(H._legs[vH])) or (vG in IsoV and IsoV[vG] != vH):
2618
return []
2619
# otherwise add known association of vertices to IsoV
2620
IsoV[vG] = vH
2621
# Now vG and vH contain all information prescribed by markings, proceed to assign markingless vertices
2622
# Create dictionaries gdG, gdH associating to tuples (genus, degree) the indices of markingless vertices in G,H with those data
2623
gdG = {}
2624
gdH = {}
2625
2626
for v in range(len(G._genera)):
2627
if v not in IsoV:
2628
gd = (G._genera[v], len(G._legs[v]))
2629
if gd in gdG:
2630
gdG[gd] += [v]
2631
else:
2632
gdG[gd] = [v]
2633
for v in range(len(H._genera)):
2634
if v not in IsoV.values():
2635
gd = (H._genera[v], len(H._legs[v]))
2636
if gd in gdH:
2637
gdH[gd] += [v]
2638
else:
2639
gdH[gd] = [v]
2640
2641
if set(gdG) != set(gdH):
2642
return []
2643
2644
# list (for all keys (g,d) of gdG) of lists of all possible dictionary-bijections from gdG(gd) to gdH(gd)
2645
VertIm = []
2646
2647
for gd in gdG:
2648
if len(gdG[gd]) != len(gdH[gd]):
2649
return []
2650
P = Permutations(len(gdG[gd]))
2651
ind = list(range(len(gdG[gd])))
2652
VertIm += [[{gdG[gd][i]:gdH[gd][p[i] - 1] for i in ind} for p in P]]
2653
2654
# Now VertIm is a list of the form [[{0:0, 1:1},{0:1, 1:0}], [{2:2}]]
2655
# Iterate over all possible combinations
2656
for VI in itertools.product(*VertIm):
2657
continueloop = False
2658
IsoV2 = dicunion(IsoV, *VI) # IsoV2 is now complete dictionary of vertices
2659
2660
# 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 pair
2661
LegIm = []
2662
2663
# TODO: possibly want quick check that now graphs are actually isomorphic
2664
2665
# first look at loops of G
2666
for v in range(len(G._genera)):
2667
EdG = G.edges_between(v, v)
2668
2669
EdH = H.edges_between(IsoV2[v], IsoV2[v])
2670
if len(EdG) != len(EdH):
2671
continueloop = True
2672
break
2673
2674
LegImvv = []
2675
2676
# 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 goes
2677
P = Permutations(len(EdG))
2678
ran = list(range(len(EdG)))
2679
choic = len(EdG) * [[0, 1]]
2680
for p in P:
2681
for o in itertools.product(*choic):
2682
# 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}
2683
di = dicunion({EdG[i][0]: EdH[p[i] - 1][o[i]]
2684
for i in ran}, {EdG[i][1]: EdH[p[i] - 1][1 - o[i]] for i in ran})
2685
LegImvv += [di]
2686
LegIm += [LegImvv]
2687
if continueloop:
2688
continue
2689
2690
# now look at edges from v to w
2691
for v in range(len(G._genera)):
2692
if continueloop:
2693
break
2694
for w in range(v + 1, len(G._genera)):
2695
EdG = G.edges_between(v, w)
2696
EdH = H.edges_between(IsoV2[v], IsoV2[w])
2697
if len(EdG) != len(EdH):
2698
continueloop = True
2699
break
2700
2701
LegImvw = []
2702
2703
# P gives the ways to biject from EdG to EdH
2704
P = Permutations(len(EdG))
2705
ran = list(range(len(EdG)))
2706
for p in P:
2707
# Example: EdG=[(1,2),(3,4)], EdH=[(6,7),(9,11)], p=[2,1] -> {1:9, 2:11, 3:6, 4:7}
2708
di = dicunion({EdG[i][0]: EdH[p[i] - 1][0]
2709
for i in ran}, {EdG[i][1]: EdH[p[i] - 1][1] for i in ran})
2710
LegImvw += [di]
2711
LegIm += [LegImvw]
2712
2713
if continueloop:
2714
continue
2715
2716
# if program runs to here, then IsoV2 is a bijection of vertices respecting the markings,
2717
# LegIm is a list of lists of dictionaries giving bijections of non-marking legs that respect
2718
# the map of vertices and edges; it remains to add the various elements to IsoL and return them
2719
Isomlist += [[IsoV2, dicunion(IsoL, *LegDics)] for LegDics in itertools.product(*LegIm)]
2720
2721
if check and Isomlist:
2722
return Isomlist
2723
2724
return Isomlist
2725
2726