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