Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
181 views
unlisted
ubuntu2004
1
import itertools
2
3
# pylint does not know sage
4
from sage.structure.sage_object import SageObject # pylint: disable=import-error
5
from sage.matrix.constructor import matrix # pylint: disable=import-error
6
from sage.misc.flatten import flatten # pylint: disable=import-error
7
from sage.misc.cachefunc import cached_method # pylint: disable=import-error
8
from sage.rings.rational_field import QQ # pylint: disable=import-error
9
from sage.arith.functions import lcm # pylint: disable=import-error
10
11
12
class EmbeddedLevelGraph(SageObject):
13
"""
14
LevelGraph inside a generalised stratum.
15
16
Note that the points of the enveloping GeneralisedStratum are of the form
17
(i,j) where i is the component and j the index of sig of that component,
18
while the points of the level graph are numbers 1,...,n.
19
20
Thus, dmp is a dictionary mapping integers to tuples of integers.
21
22
Attributes:
23
24
* LG (LevelGraph): underlying LevelGraph
25
* X (GeneralisedStratum): enveloping stratum
26
* dmp (dict): (bijective!) dictionary marked points of LG -> points of stratum
27
* dmp_inv (dict): inverse of dmp
28
* dlevels (dict): (bijective!) dictionary levels of LG -> new level numbering
29
* dlevels_inv (dict): inverse of dlevels
30
* top (GeneralisedStratum): (if self is a BIC) top component
31
* bot (GeneralisedStratum): (if self is a BIC) bottom component
32
* clutch_dict (dict): (if self is a BIC) dictionary mapping points of top
33
stratum to points of bottom stratum where there is an edge in self.
34
* emb_top (dict): (if self is a BIC) dictionary mapping points of stratum top
35
to the corresponding points of the enveloping stratum.
36
* emb_bot (dict): (if self is a BIC) dictionary mapping points of stratum bot
37
to the corresponding points of the enveloping stratum.
38
* automorphisms (list of list of dicts): automorphisms
39
* codim (int): codimension of LevelGraph in stratum
40
number_of_levels (int): Number of levels of self.
41
42
Note that attempting to access any of the attributes top, bot, clutch_dict,
43
emb_top or emb_bot will raise a ValueError if self is not a BIC.
44
"""
45
def __init__(self, X, LG, dmp, dlevels):
46
"""
47
Initialises EmbeddedLevelGraph.
48
49
Args:
50
LG (LevelGraph): underlying LevelGraph
51
X (GeneralisedStratum): enveloping stratum
52
dmp (dictionary): (bijective!) dictionary marked points of LG -> points of stratum
53
dlevels (dictionary): (bijective!) dictionary levels of LG -> new level numbering
54
"""
55
self.LG = LG
56
self.X = X
57
self.dmp = dmp
58
self.dmp_inv = {value: key for key, value in dmp.items()}
59
self.add_vertices_at_infinity()
60
self.dlevels = dlevels
61
self.dlevels_inv = {value: key for key, value in dlevels.items()}
62
self._top = None
63
self._bot = None
64
self._clutch_dict = None
65
self._emb_top = None
66
self._emb_bot = None
67
self._automorphisms = None
68
self._level = {}
69
self._ell = None
70
self.codim = self.LG.codim()
71
self.number_of_levels = len(set(self.dlevels.keys()))
72
73
def __repr__(self):
74
return "EmbeddedLevelGraph(LG=%r,dmp=%r,dlevels=%r)" % (
75
self.LG, self.dmp, self.dlevels)
76
77
def __str__(self):
78
return (
79
"Embedded Level Graph consisting of %s with point dictionary %s and level dictionary %s" %
80
(self.LG, self.dmp, self.dlevels))
81
82
def explain(self):
83
"""
84
A more user-friendly display of __str__ :-)
85
"""
86
def _list_print(L):
87
if len(L) > 1:
88
s = ['s ']
89
for x in L[:-2]:
90
s.append('%r, ' % x)
91
s.append('%r ' % L[-2])
92
s.append('and %r.' % L[-1])
93
return ''.join(s)
94
else:
95
return ' %r.' % L[0]
96
97
def _num(i):
98
if i == 1:
99
return 'one edge'
100
else:
101
return '%r edges' % i
102
print("LevelGraph embedded into stratum %s with:" % self.X)
103
LG = self.LG
104
for l in range(LG.numberoflevels()):
105
internal_l = LG.internal_level_number(l)
106
print("On level %r:" % l)
107
for v in LG.verticesonlevel(internal_l):
108
print("* A vertex (number %r) of genus %r" % (v, LG.genus(v)))
109
levels_of_mps = list(
110
set(LG.level_number(LG.levelofleg(leg)) for leg in self.dmp))
111
print("The marked points are on level%s" %
112
_list_print(sorted(levels_of_mps)))
113
print("More precisely, we have:")
114
for leg in self.dmp:
115
print(
116
"* Marked point %r of order %r on vertex %r on level %r" %
117
(self.dmp[leg],
118
LG.orderatleg(leg),
119
LG.vertex(leg),
120
LG.level_number(
121
LG.levelofleg(leg))))
122
print("Finally, we have %s. More precisely:" % _num(len(LG.edges)))
123
edge_dict = {e: (LG.vertex(e[0]), LG.vertex(e[1])) for e in LG.edges}
124
edge_dict_inv = {}
125
for k, v in edge_dict.items():
126
if v in edge_dict_inv:
127
edge_dict_inv[v].append(k)
128
else:
129
edge_dict_inv[v] = [k]
130
for e in edge_dict_inv:
131
print("* %s between vertex %r (on level %r) and vertex %r (on level %r) with prong%s" %
132
(_num(len(edge_dict_inv[e])),
133
e[0], LG.level_number(LG.levelofvertex(e[0])),
134
e[1], LG.level_number(LG.levelofvertex(e[1])),
135
# _write_prongs()
136
_list_print([LG.prong(ee) for ee in edge_dict_inv[e]])))
137
138
def __eq__(self, other):
139
if not isinstance(other, EmbeddedLevelGraph):
140
return False
141
return self.LG == other.LG and self.dmp == other.dmp and self.dlevels == other.dlevels
142
143
@cached_method
144
def is_bic(self):
145
return self.LG.is_bic()
146
147
@property
148
def ell(self):
149
"""
150
If self is a BIC: the lcm of the prongs.
151
152
Raises:
153
RuntimeError: raised if self is not a BIC.
154
155
Returns:
156
int: lcm of the prongs.
157
"""
158
if self._ell is None:
159
if not self.is_bic():
160
raise RuntimeError("ell only defined for BICs!")
161
self._ell = lcm(self.LG.prongs.values())
162
return self._ell
163
164
# Dawei's positivity coefficients
165
@property
166
def b(self):
167
if self.X._h0 > 1:
168
raise ValueError('Cannot compute b on disconnected stratum.')
169
g = self.X._sig_list[0].g
170
val = 0
171
# super inefficient, but probably good enough for now:
172
# take the underlying StableGraph and, for each edge,
173
# contract all other edges and check which graph we end up with:
174
stgraph = self.LG.stgraph
175
for e in self.LG.edges:
176
ee = self.LG.edges[:]
177
ee.remove(e)
178
curr_graph = stgraph.copy()
179
for contr in ee:
180
curr_graph.contract_edge(contr)
181
assert curr_graph.edges() == [e]
182
if len(curr_graph.genera()) == 2:
183
# compact type:
184
i = min(curr_graph.genera())
185
val += QQ(6 * i * (g - i)) / QQ((g + 3) * self.LG.prong(e))
186
else:
187
# irreducible
188
assert len(curr_graph.genera()) == 1
189
val += QQ(g + 1) / QQ((g + 3) * self.LG.prong(e))
190
return self.ell * val
191
192
@property
193
def c(self):
194
return self.ell * (self.bot.kappa_EKZ -
195
self.X.kappa_EKZ * QQ(self.bot.N) / QQ(self.X.N))
196
######
197
198
@property
199
def top(self):
200
if self._top is None:
201
self.split()
202
return self._top
203
204
@property
205
def bot(self):
206
if self._bot is None:
207
self.split()
208
return self._bot
209
210
@property
211
def clutch_dict(self):
212
if self._clutch_dict is None:
213
self.split()
214
return self._clutch_dict
215
216
@property
217
def emb_bot(self):
218
if self._emb_bot is None:
219
self.split()
220
return self._emb_bot
221
222
@property
223
def emb_top(self):
224
if self._emb_top is None:
225
self.split()
226
return self._emb_top
227
228
def add_vertices_at_infinity(self):
229
"""
230
We add the vertices at infinity to the underlying_graph of self.LG.
231
232
These are given by the residue conditions.
233
234
More precisely: Recall that the underlying_graph of self.LG has vertices
235
and edges of self.LG stored in the form UG_vertex = (vertex number, genus, 'LG')
236
and edges of the underlying graph are of the form: (UG_vertex, UG_vertex, edge name)
237
We now add vertices 'at level infinity' by adding, for each res_cond of self.X
238
239
* a UG_vertex called (i, 0, 'res') (where i is the index of the condition in res_cond
240
we are currently considering) and edges so that each residue
241
condition corresponds to an edge from the corresponding pole to some
242
residue at 'level infinity'. We store these in the form:
243
* (res_vertex, UG_vertex, resiedgej)
244
Here UG_vertex is the vertex of self.LG, in the form (vertex number, genus, 'LG'),
245
that res_vertex is attached to and j is the leg of that vertex (as a leg of self.LG!)
246
corresponding to the pole that resi should be attached to.
247
"""
248
# remove any that might already be there:
249
existing_residues = [v for v in self.LG.underlying_graph.vertices(sort=True)
250
if v[2] == 'res']
251
for v in existing_residues:
252
self.LG.underlying_graph.delete_vertex(v)
253
# add a vertex for every residue condition:
254
# TODO: remove duplicates?
255
edges = []
256
for i, rc in enumerate(self.X.res_cond):
257
v_name = (i, 0, 'res')
258
for p in rc:
259
e_name = 'res%redge%r' % (i, self.dmp_inv[p])
260
v_on_graph = self.LG.vertex(self.dmp_inv[p])
261
edges.append((self.LG.UG_vertex(v_on_graph), v_name, e_name))
262
self.LG.underlying_graph.add_edges(edges)
263
264
@property
265
@cached_method
266
def residue_matrix_from_RT(self):
267
"""
268
The matrix associated to the residue conditions imposed by the residue theorem
269
on each vertex of self.
270
271
Returns:
272
SAGE Matrix: matrix of residue conditions given by RT
273
"""
274
poles_by_vertex = {}
275
for p in self.X._polelist:
276
vertex = self.LG.vertex(self.dmp_inv[p])
277
try:
278
poles_by_vertex[vertex].append(p)
279
except KeyError:
280
poles_by_vertex[vertex] = [p]
281
rows = []
282
for v in poles_by_vertex:
283
rows.append([int(p in poles_by_vertex[v])
284
for p in self.X._polelist])
285
return matrix(QQ, rows)
286
287
@property
288
@cached_method
289
def full_residue_matrix(self):
290
"""
291
Residue matrix with GRC conditions and RT conditions (for each vertex).
292
293
OUTPUT:
294
295
A matrix with number of poles columns and a row for each condition.
296
"""
297
M = self.X.residue_matrix()
298
if M:
299
M = M.stack(self.residue_matrix_from_RT)
300
else:
301
M = self.residue_matrix_from_RT
302
return M
303
304
def residue_zero(self, pole):
305
"""
306
Check if the residue at pole is forced zero by residue conditions.
307
308
NOTE: We DO include the RT on the vertices in this check!
309
310
Args:
311
pole (tuple): pole (as a point (i,j) of self.X)
312
313
Returns:
314
bool: True if forced zero, False otherwise.
315
"""
316
# add the equation corresponding to the residue at pole to the residue matrix
317
# and see if the rank changes:
318
i = self.X._polelist.index(pole)
319
res_vec = [[int(i == j) for j in range(len(self.X._polelist))]]
320
RM = self.full_residue_matrix
321
# RM = self.X.residue_matrix()
322
if RM:
323
stacked = RM.stack(matrix(res_vec))
324
return stacked.rank() == self.full_residue_matrix.rank()
325
# return stacked.rank() == self.X.residue_matrix().rank()
326
else:
327
return False
328
329
def level(self, l):
330
"""
331
The generalised stratum on level l.
332
333
Note that this is cached, i.e. on first call, it is stored in the dictionary
334
_level.
335
336
INPUT:
337
338
l: integer
339
The relative level number (0,...,codim)
340
341
OUTPUT:
342
343
The LevelStratum that is
344
345
* a list of Signatures (one for each vertex on the level)
346
* a list of residue conditions, i.e. a list [res_1,...,res_r]
347
where each res_k is a list of tuples [(i_1,j_1),...,(i_n,j_n)]
348
where each tuple (i,j) refers to the point j (i.e. index) on the
349
component i and such that the residues at these points add up
350
to 0.
351
* a dictionary of legs, i.e. n -> (i,j) where n is the original
352
number of the point (on the LevelGraph self) and i is the
353
number of the component, j the index of the point in the signature tuple.
354
355
Note that LevelStratum is a GeneralisedStratum together with
356
a leg dictionary. Here, we provide an additional attribute:
357
358
* leg_orbits, a nested list giving the orbits of the points on the level
359
under the automorphism group of self.
360
"""
361
try:
362
LS = self._level[l]
363
except KeyError:
364
# for the residue conditions: We add the residue conditions from
365
# the enveloping stratum:
366
# We do this by passing those poles with residue forced
367
# zero as those to be ignored in the residue calculations
368
# performed by the LevelGraph:
369
# We have to translate them to points on self:
370
# Note that self.LG knows the "level at infinity"
371
excluded_poles = tuple(self.dmp_inv[p] for p in flatten(
372
self.X.res_cond, max_level=1))
373
LS = self.LG.stratum_from_level(l, excluded_poles=excluded_poles)
374
# add automorphism info
375
LS.leg_orbits = []
376
seen = set()
377
for leg in LS._leg_dict:
378
if leg in seen:
379
continue
380
curr_orbit = [LS._leg_dict[leg]]
381
for _v_map, l_map in self.automorphisms:
382
curr_orbit.append(LS._leg_dict[l_map[leg]])
383
seen.update([l_map[leg]])
384
LS.leg_orbits.append(list(set(curr_orbit))
385
) # remove duplicates
386
self._level[l] = LS
387
return LS
388
389
def legs_are_isomorphic(self, leg, other_leg):
390
"""
391
Check if leg and other_leg are in the same Aut-orbit of self.
392
393
Args:
394
leg (int): leg on self.LG
395
other_leg (int): leg on self.LG
396
397
Raises:
398
RuntimeError: If leg is not in any aut-orbit of the level it should be on.
399
400
Returns:
401
bool: True if they are in the same orbit of self.level(levelofleg),
402
False, otherwise.
403
404
EXAMPLES::
405
406
Note the asymmetric banana graph.
407
408
The symmetric one has isomorphisms.
409
410
Legs are isomorphic to themselves.
411
412
It's symmetric.
413
414
"""
415
level = self.LG.level_number(self.LG.levelofleg(leg))
416
other_level = self.LG.level_number(self.LG.levelofleg(other_leg))
417
if level != other_level:
418
return False
419
assert level == other_level
420
emb_leg = self.level(level)._leg_dict[leg]
421
emb_other_leg = self.level(level)._leg_dict[other_leg]
422
for orbit in self.level(level).leg_orbits:
423
if emb_leg in orbit:
424
return emb_other_leg in orbit
425
else:
426
raise RuntimeError(
427
"leg %r not in any orbit %r of %r" %
428
(leg, self.level(level).leg_orbits, self.level(level)))
429
430
@cached_method
431
def edge_orbit(self, edge):
432
"""
433
The edge orbit of edge in self.
434
435
raises a ValueError if edge is not an edge of self.LG
436
437
INPUT:
438
439
edge: tuple
440
An edge of ``self.LG``, i.e. tuple (start leg, end leg), where start
441
leg should not be on a lower level than end leg.
442
443
OUTPUT:
444
445
The set of edges in automorphism orbit of ``edge``.
446
"""
447
if edge not in self.LG.edges:
448
raise ValueError("%r is not an edge of %r!" % (edge, self))
449
s = set([edge])
450
for v_map, l_map in self.automorphisms:
451
new_edge = (l_map[edge[0]], l_map[edge[1]])
452
s.add(new_edge)
453
return s
454
455
def len_edge_orbit(self, edge):
456
"""
457
Length of the edge orbit of edge in self.
458
459
Args:
460
edge (tuple): edge of self.LG, i.e. tuple (start leg, end leg), where
461
start leg should not be on a lower level than end leg.
462
463
Raises:
464
ValueError: if edge is not an edge of self.LG
465
466
Returns:
467
int: length of the aut-orbit of edge.
468
469
EXAMPLES::
470
471
472
Prongs influence the orbit length.
473
474
"""
475
return len(self.edge_orbit(edge))
476
477
def automorphisms_stabilising_legs(self, leg_tuple):
478
stabs = []
479
for v_map, l_map in self.automorphisms:
480
for l in leg_tuple:
481
if l_map[l] != l:
482
break
483
else: # no break
484
stabs.append(l_map)
485
return stabs
486
487
def delta(self, i):
488
"""
489
Squish all levels except for i.
490
491
Note that delta(1) contracts everything except top-level and that the
492
argument is interpreted via internal_level_number (i.e. a relative level number).
493
494
Moreover, dlevels is set to map to 0 and -1(!).
495
496
Args:
497
i (int): Level not to be squished.
498
499
Returns:
500
EmbeddedLevelGraph: Embedded BIC (result of applying delta to the
501
underlying LevelGraph)
502
"""
503
newLG = self.LG.delta(i, quiet=True)
504
newdmp = self.dmp.copy()
505
# level_number is (positive!) relative level number.
506
newdlevels = {l: -newLG.level_number(l) for l in newLG.levels}
507
return EmbeddedLevelGraph(self.X, newLG, newdmp, newdlevels)
508
509
def squish_vertical(self, level):
510
"""
511
Squish level crossing below level 'level'.
512
513
Note that in contrast to the levelgraph method, we work with relative
514
level numbers here!
515
516
Args:
517
level (int): relative (!) level number.
518
519
Returns:
520
EmbeddedLevelGraph: Result of squishing.
521
522
EXAMPLES::
523
524
sage: from admcycles.diffstrata import *
525
sage: X=GeneralisedStratum([Signature((4,))])
526
sage: p = X.enhanced_profiles_of_length(4)[0][0]
527
sage: g = X.lookup_graph(p)
528
529
lookup_graph uses the sorted profile (note that these do not have to be reduced!):
530
531
sage: assert any(g.squish_vertical(0).is_isomorphic(G) for G in X.lookup(p[1:]))
532
sage: assert any(g.squish_vertical(1).is_isomorphic(G) for G in X.lookup(p[:1]+p[2:]))
533
sage: assert any(g.squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]+p[3:]))
534
sage: assert any(g.squish_vertical(3).is_isomorphic(G) for G in X.lookup(p[:3]))
535
536
Squishing outside the range of levels does nothing:
537
538
sage: assert g.squish_vertical(4) == g
539
540
Recursive squishing removes larger parts of the profile:
541
542
sage: assert any(g.squish_vertical(3).squish_vertical(2).is_isomorphic(G) for G in X.lookup(p[:2]))
543
"""
544
newLG = self.LG.squish_vertical(
545
self.LG.internal_level_number(level), quiet=True)
546
newdmp = self.dmp.copy()
547
# level_number is (positive!) relative level number.
548
newdlevels = {l: -newLG.level_number(l) for l in newLG.levels}
549
return EmbeddedLevelGraph(self.X, newLG, newdmp, newdlevels)
550
551
def split(self):
552
"""
553
Splits embedded BIC self into top and bottom component.
554
555
Raises a ValueError if self is not a BIC.
556
557
OUTPUT:
558
559
A dictionary consising of
560
561
* the GeneralisedStratum self.X
562
* the top component LevelStratum
563
* the bottom component LevelStratum
564
* the clutching dictionary mapping ex-half-edges on
565
top to their partners on bottom (both as points in the
566
respective strata!)
567
* a dictionary embedding top into the stratum of self
568
* a dictionary embedding bot into the stratum of self
569
570
Note that clutch_dict, emb_top and emb_bot are dictionaries between
571
points of strata, i.e. after applying dmp to the points!
572
"""
573
if not self.is_bic():
574
raise ValueError(
575
"Error: %s is not a BIC! Cannot be split into Top and Bottom component!" %
576
self)
577
self._top = self.level(0)
578
self._bot = self.level(1)
579
# To construct emb_top and emb_bot, we have to combine self.dmp with the
580
# the leg_dicts of top and bot.
581
# More precisely: emb_top is the composition of the inverse of the leg_dict
582
# of top, i.e. top.stratum_number, and self.dmp
583
# (giving a map from the points of top to the points of the enveloping
584
# stratum of self) and the same for bot.
585
# We implement this by iterating over the marked points of self on top level,
586
# which are exactly the keys of self.dmp that are on top level.
587
# Note that we make extra sure that we didn't mess up the level numbering by
588
# using the relative level numbering (where the top level is guaranteed to be 0
589
# and the bottom level is 1 (positive!)).
590
self._emb_top = {self._top.stratum_number(l): self.dmp[l]
591
for l in iter(self.dmp)
592
if self.LG.level_number(self.LG.levelofleg(l)) == 0}
593
self._emb_bot = {self._bot.stratum_number(l): self.dmp[l]
594
for l in iter(self.dmp)
595
if self.LG.level_number(self.LG.levelofleg(l)) == 1}
596
# Because this is a BIC, all edges of self are cut in this process
597
# and this is exactly the dictionary we must remember
598
# WARNING: Here we assume that e[0] is on top level and e[1] is on bottom
599
# This is assured by tidy_up, e.g. after initialisation!
600
# Note that all these dictionaries map points of GeneralisedStrata to each
601
# other so we must take the corresponding stratum_number!
602
self._clutch_dict = {
603
self._top.stratum_number(
604
e[0]): self._bot.stratum_number(
605
e[1]) for e in self.LG.edges}
606
return {'X': self.X, 'top': self._top, 'bottom': self._bot,
607
'clutch_dict': self._clutch_dict,
608
'emb_dict_top': self._emb_top, 'emb_dict_bot': self._emb_bot, }
609
610
def is_legal(self):
611
"""
612
Check the R-GRC for self.
613
614
Returns:
615
bool: result of R-GRC.
616
"""
617
# Check if any levels are empty:
618
# Note that this can only happen if self.X has simple poles (as we never
619
# have horizontal edges)
620
if list(self.X.simple_poles()):
621
if any(self.level(l).is_empty()
622
for l in range(self.number_of_levels)):
623
return False
624
# poles are excluded if they are contained in _any_ residue condition of the stratum.
625
# In particular, they are _not_ excluded if they are only restrained by
626
# the RT on some component!
627
poles_in_rc_stratum = flatten(self.X.res_cond, max_level=1)
628
poles_in_rc_graph = tuple(self.dmp_inv[p] for p in poles_in_rc_stratum)
629
return self.LG.is_legal(excluded_poles=poles_in_rc_graph, quiet=True)
630
631
def standard_markings(self):
632
r"""
633
Construct a dictionary for relabelling the markings. A standard labelling will label the legs
634
of markings first and then the half edges. If the generalised stratum has only one component,
635
the standard label of a marking will be exactly the position of that marking in the signature.
636
637
EXAMPLES::
638
639
sage: from admcycles.diffstrata.generalisedstratum import Stratum
640
sage: X=Stratum((1,1))
641
sage: X.bics[0]
642
EmbeddedLevelGraph(LG=LevelGraph([1, 0],[[1, 2], [3, 4, 5, 6]],[(1, 5), (2, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, -1],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -1: -1})
643
sage: X.bics[0].standard_markings()
644
{1: 3, 2: 4, 3: 1, 4: 2, 5: 5, 6: 6}
645
646
"""
647
n_list = [0 for i in range(self.X._h0)] # list of number of markings on each component of the stratum
648
for t in self.dmp_inv:
649
n_list[t[0]] += 1
650
651
# list such that the j-th entry is the sum of numbers of
652
# markings on the components of smaller indices
653
n_sums = [0] + [sum(n_list[i] for i in range(j))
654
for j in range(1, self.X._h0)]
655
new_leg_dict = {} # the mapping dict for relabelling the legs
656
h = 1
657
for i in range(1, len(self.LG.poleorders) + 1):
658
if i in self.dmp:
659
new_leg_dict[i] = (self.dmp[i][1] +
660
n_sums[self.dmp[i][0]] + 1)
661
else:
662
new_leg_dict[i] = len(self.dmp) + h
663
h = h + 1
664
return new_leg_dict
665
666
def relabel(self, legdict, tidyup=True):
667
r"""
668
Relabel the EmbeddedLevelGraph by a given dictionary.
669
670
INPUT:
671
672
- legdict (dict): A dictionary indicating the map from old markings
673
to the new ones
674
675
EXAMPLES::
676
sage: from admcycles.diffstrata.generalisedstratum import Stratum
677
sage: X = Stratum((1,1))
678
sage: dict1={1: 3, 2: 4, 3: 1, 4: 2, 5: 5, 6: 6}
679
sage: X.bics[0].relabel(dict1)
680
EmbeddedLevelGraph(LG=LevelGraph([1, 0],[[3, 4], [1, 2, 5, 6]],[(3, 5), (4, 6)],{1: 1, 2: 1, 3: 0, 4: 0, 5: -2, 6: -2},[0, -1],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0, -1: -1})
681
682
"""
683
newLG = self.LG.relabel(legdict, tidyup)
684
new_dmp = {legdict[i]: j for i, j in self.dmp.items()}
685
if tidyup:
686
new_dmp = {a: b for a, b in sorted(new_dmp.items())}
687
newEmbLG = EmbeddedLevelGraph(self.X, newLG, new_dmp, self.dlevels)
688
689
return newEmbLG
690
691
def is_isomorphic(self, other):
692
"""
693
Check if self and other are isomorphic (as EmbeddedLevelGraphs).
694
695
Args:
696
other (EmbeddedLevelGraph): Graph to check isomorphism.
697
698
Returns:
699
bool: True if there exists at least one isomorphism.
700
"""
701
# TODO: Maybe include a way to check against unembedded LGs
702
# TODO: Check embedding!
703
if not isinstance(other, EmbeddedLevelGraph):
704
return False
705
try:
706
next(self.isomorphisms(other))
707
return True
708
except StopIteration:
709
return False
710
711
@property
712
def automorphisms(self):
713
"""
714
The automorphisms of self (as automorphisms of the underlying LevelGraph,
715
respecting the embedding, see doc of isomorphisms).
716
717
OUTPUT:
718
719
A list of pairs ``(map_of_vertices, map_of_legs)``. Both ``maps_of_vertices``
720
and ``map_of_legs`` are dictionaries.
721
"""
722
if not self._automorphisms:
723
self._automorphisms = list(self.isomorphisms(self))
724
return self._automorphisms
725
726
def isomorphisms(self, other):
727
"""
728
Generator yielding the "next" isomorphism of self and other.
729
730
Note that while this gives an "isomorphism" from self.LG to other.LG, this
731
is not necessarily an isomorphism of the LevelGraphs (the numbered points may
732
be permuted if this is "fixed" by the embedding).
733
734
INPUT:
735
736
other: EmbeddedLevelGraph
737
The (potentially) isomorphic EmbeddedLevelGraph.
738
739
OUTPUT:
740
741
An iterator going through isomorphisms. Each isomorphism is encoded by a
742
pair of dictionaries ``(vertex_map, leg_map)`` The dictionaries
743
``vertex_map`` (respectively ``leg_map``) is to the mapping of the
744
vertices (resp. legs) of ``self.LG`` to the vertices (resp. legs) of
745
``other.LG``.
746
"""
747
# Isomorphisms of EmbeddedLevelGraphs:
748
# An isomorphism of EmbeddedLevelGraph is a set of compatible level isomorphisms.
749
# We iterate through the isomorphisms on each level and yield whenever we find
750
# compatible levelisomorphisms for all levels.
751
# Note that we use dlevels for this, as these should be compatible.
752
# There are (at least) two ways in which this can be optimised:
753
# * don't go through the entire product before checking edge compatibility!
754
# * choose a smart ordering of levels (e.g. number of vertices)
755
isom_vertices = {}
756
isom_legs = {}
757
for level_isos in itertools.product(
758
*[self._level_iso(other, l) for l in self.dlevels.values()]):
759
for level_iso_v, level_iso_l in level_isos:
760
isom_vertices.update(level_iso_v)
761
isom_legs.update(level_iso_l)
762
# check edge compatibility
763
for e in self.LG.edges:
764
if (isom_legs[e[0]], isom_legs[e[1]]) not in other.LG.edges:
765
break
766
else: # no break
767
yield isom_vertices.copy(), isom_legs.copy()
768
769
def _level_iso(self, other, l):
770
"""
771
Generator yielding the "next" isomorphism of level l of self and other.
772
773
Here, l is a value of dlevels (this should be compatible).
774
775
Note that we require the graph to have no horizontal edges, i.e. the level
776
contains no edges!
777
778
TODO: * Maybe add future horizontal support?
779
* Maybe use relative level number instead? (this seems to give weird behaviour
780
right now...)
781
782
Args:
783
other (EmbeddedLevelGraph): The embedded graph we are checking for isomorphism.
784
l (int): Level number (embedded into the stratum, i.e. value of dlevels).
785
786
Yields:
787
tuple: the next isomorphism of levels:
788
dict: vertices of self.LG -> vertices of other.LG
789
dict: legs of self.LG -> legs of other.LG
790
"""
791
# Isomorphisms of levels:
792
# An isomorphism of levels consist of
793
# * a map: vertices to vertices
794
# * a map: legs to legs
795
# respecting:
796
# * the genus,
797
# * the number of legs on every vertex,
798
# * the order at every leg,
799
# * the marked points of the stratum (via dmp).
800
####
801
# First, we extract the data for level l from self and other.
802
# Note that we do not use stratum_from_level to avoid all the overhead.
803
# TODO: All this should be cached!!
804
l_self = self.LG.internal_level_number(l)
805
l_other = other.LG.internal_level_number(l)
806
# we need to be careful to distinguish the indices in the list of genera
807
# of the LevelGraph from the actual genera.
808
vv_self_idx = self.LG.verticesonlevel(l_self) # list of indices
809
vv_other_idx = other.LG.verticesonlevel(l_other) # list of indices
810
if len(vv_self_idx) != len(vv_other_idx):
811
return
812
vv_self = [self.LG.genus(i) for i in vv_self_idx] # list of genera
813
vv_other = [other.LG.genus(i) for i in vv_other_idx] # list of genera
814
# extract the legs: (nested lists)
815
legs_self = [self.LG.legsatvertex(v) for v in vv_self_idx]
816
legs_other = [other.LG.legsatvertex(v) for v in vv_other_idx]
817
# build dictionary: leg -> index in vv
818
leg_dict_self = {l: i for i, legs in enumerate(
819
legs_self) for l in legs}
820
leg_dict_other = {l: i for i, legs in enumerate(
821
legs_other) for l in legs}
822
if len(leg_dict_self) != len(leg_dict_other):
823
return
824
# for quick checks, we create sorted lists of the orders at each vertex
825
order_sorted_self = [sorted([self.LG.orderatleg(l) for l in legs])
826
for legs in legs_self]
827
order_sorted_other = [sorted([other.LG.orderatleg(l) for l in legs])
828
for legs in legs_other]
829
# We create the two maps as dictionaries:
830
# index of vv_self -> index of vv_other
831
isom_vert = {}
832
# leg number (on self.LG) -> leg number (on other.LG)
833
isom_legs = {}
834
# We also want to keep track of whom we've already mapped:
835
# source is a dictionary: genus -> list of indices of vv_self
836
source = {}
837
for i, g in enumerate(vv_self):
838
try:
839
source[g].append(i)
840
except KeyError:
841
source[g] = [i]
842
# target is a dictionary: genus -> list of indices of vv_other
843
target = {}
844
for i, g in enumerate(vv_other):
845
try:
846
target[g].append(i)
847
except KeyError:
848
target[g] = [i]
849
# for the legs we build copies of the nested lists to manipulate
850
legs_source = [legs[:] for legs in legs_self]
851
legs_target = [legs[:] for legs in legs_other]
852
# Next, we exclude some deal-breakers:
853
# * The same genera must appear.
854
if sorted(vv_self) != sorted(vv_other):
855
return
856
# * The same embedded points have to be on this level (they have to be
857
# mapped to each other!)
858
# In particular, this gives a part of the leg map (and thus also of the
859
# vertex map).
860
for p_self, p in self.dmp.items(
861
): # p is the "shared" point of the stratum
862
p_other = other.dmp_inv[p]
863
# If neither point is on this level, we continue:
864
if not (p_self in leg_dict_self or p_other in leg_dict_other):
865
continue
866
# The vertex of p_self must map to that of p_other.
867
# Three things can fail here:
868
# * only one of the two points is on this level.
869
if ((p_self in leg_dict_self and p_other not in leg_dict_other) or (
870
p_self not in leg_dict_self and p_other in leg_dict_other)):
871
return
872
v_self = leg_dict_self[p_self]
873
v_other = leg_dict_other[p_other]
874
# * the points are on incompatible vertices (genus or numbers/orders of legs!)
875
if (vv_self[v_self] != vv_other[v_other] or
876
len(legs_self[v_self]) != len(legs_other[v_other]) or
877
order_sorted_self[v_self] != order_sorted_other[v_other]):
878
return
879
# * two points are on the same vertex in one case, but on different vertices
880
# in the other. I.e. v_self is already being mapped somewhere other than v_other
881
# or v_other is already being mapped to (by someone else)
882
try:
883
if isom_vert[v_self] != v_other:
884
return
885
except KeyError: # v_self not being mapped yet, i.e. still in source
886
g = vv_other[v_other]
887
if v_other in target[g]: # make sure v_other is still a possible target
888
isom_vert[v_self] = v_other
889
source[g].remove(v_self)
890
target[g].remove(v_other)
891
else:
892
return
893
# now we can safely map the legs:
894
isom_legs[p_self] = p_other
895
# and remove them from source and target (so they won't be
896
# reassigned later)
897
legs_source[v_self].remove(p_self)
898
legs_target[v_other].remove(p_other)
899
# Next, we construct maps of the remaining vertices.
900
# For this, we use a small recursive function:
901
curr_v_map = {}
902
legal_v_maps = []
903
904
def vertex_maps(sl, tl):
905
if not sl:
906
# all entries of tl should be None at this point:
907
assert all(tv is None for tv in tl)
908
legal_v_maps.append(curr_v_map.copy())
909
return
910
curr_v = sl.pop()
911
curr_legs = len(legs_self[curr_v])
912
# try to map curr_v to tl:
913
for i, tv in enumerate(tl):
914
# we temporarily set "hit" targets to None so we don't have to worry
915
# about indexing...
916
if tv is None:
917
continue
918
# check if legs _can_ be compatible:
919
if (curr_legs != len(legs_other[tv]) or
920
order_sorted_self[curr_v] != order_sorted_other[tv]):
921
continue
922
tl[i] = None
923
curr_v_map[curr_v] = tv
924
vertex_maps(sl, tl)
925
# undo
926
del curr_v_map[curr_v]
927
tl[i] = tv
928
# undo
929
sl.append(curr_v)
930
# the function for the legs is almost the same, just the condition is
931
# different:
932
curr_l_map = {}
933
legal_l_maps = []
934
935
def leg_maps(sl, tl):
936
if not sl:
937
# all entries of tl should be None at this point:
938
assert all(tleg is None for tleg in tl)
939
legal_l_maps.append(curr_l_map.copy())
940
return
941
curr_l = sl.pop()
942
# try to map curr_l to tl:
943
for i, tleg in enumerate(tl):
944
# we temporarily set "hit" targets to None so we don't have to worry
945
# about indexing...
946
if tleg is None:
947
continue
948
# check if orders are compatible:
949
if self.LG.orderatleg(curr_l) == other.LG.orderatleg(tleg):
950
tl[i] = None
951
curr_l_map[curr_l] = tleg
952
leg_maps(sl, tl)
953
# undo
954
del curr_l_map[curr_l]
955
tl[i] = tleg
956
# undo
957
sl.append(curr_l)
958
# Now we build the list of all vertex isomorphisms going through the
959
# vertices by genus
960
v_isom_list = []
961
for g in source:
962
legal_v_maps = [] # will get filled by vertex_maps
963
vertex_maps(source[g], target[g])
964
v_isom_list.append(legal_v_maps[:]) # copy!
965
# v_isom_list is now a nested list of maps for each genus.
966
# the product consists of tuples, one map for every genus.
967
for v_maps in itertools.product(*v_isom_list):
968
for v_map in v_maps:
969
# this overwrites exactly the vertices in source.
970
isom_vert.update(v_map)
971
# Finally, the returned vertex map should use the indexing of the
972
# LevelGraph, not of the level:
973
return_isom_vert = {vv_self_idx[k]: vv_other_idx[v]
974
for k, v in isom_vert.items()}
975
# Now we build all leg maps, again as a product of all maps at vertices.
976
# Note: This also included the previously assigned vertices (with
977
# marked points...)
978
l_isom_list = []
979
for v in isom_vert:
980
# Construct leg maps:
981
# We work with legs_source and legs_target, i.e. the list
982
# of legs with the marked points removed.
983
legal_l_maps = []
984
leg_maps(legs_source[v], legs_target[isom_vert[v]])
985
l_isom_list.append(legal_l_maps[:]) # copy!
986
for l_maps in itertools.product(*l_isom_list):
987
for l_map in l_maps:
988
isom_legs.update(l_map)
989
yield return_isom_vert.copy(), isom_legs.copy()
990
991