Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
181 views
unlisted
ubuntu2004
1
# -*- coding: utf-8 -*-
2
3
import itertools
4
import sys
5
from sympy.utilities.iterables import partitions
6
7
# pylint does not know sage
8
from sage.structure.sage_object import SageObject # pylint: disable=import-error
9
from sage.matrix.constructor import matrix # pylint: disable=import-error
10
from sage.misc.flatten import flatten # pylint: disable=import-error
11
from sage.misc.cachefunc import cached_method # pylint: disable=import-error
12
from sage.rings.rational_field import QQ # pylint: disable=import-error
13
from sage.functions.other import factorial # pylint: disable=import-error
14
from sage.combinat.integer_vector_weighted import WeightedIntegerVectors # pylint: disable=import-error
15
from sage.functions.other import binomial # pylint: disable=import-error
16
from sage.symbolic.constants import pi, I # pylint: disable=import-error
17
from sage.misc.misc_c import prod # pylint: disable=import-error
18
19
from copy import deepcopy
20
21
import admcycles.admcycles
22
import admcycles.stratarecursion
23
24
import admcycles.diffstrata.levelgraph
25
import admcycles.diffstrata.bic
26
import admcycles.diffstrata.sig
27
import admcycles.diffstrata.stratatautring
28
import admcycles.diffstrata.embeddedlevelgraph
29
import admcycles.diffstrata.additivegenerator
30
import admcycles.diffstrata.elgtautclass
31
32
from admcycles.diffstrata.auxiliary import (hash_AG, unite_embedded_graphs,
33
adm_key, get_squished_level)
34
35
#######################################################################
36
#######################################################################
37
# Recursive Calculations and Degeneration Graphs
38
#######################################################################
39
# The idea is to do all calculations recursively.
40
# In particular, the Degeneration Graph is itself a recursive object.
41
##
42
# The key observation is that:
43
# * Each 3-level graph arises by either clutching a top component of
44
# a BIC to a BIC of its bottom component of a BIC of the top
45
# component to the bottom component.
46
# * On the other hand, each 3-level graph is the intersection of
47
# two (different) BICs of the Stratum.
48
# * Therefore, for each BIC B of the Stratum, every BIC Bt in the top
49
# component corresponds to a unique BIC B' of the stratum, so
50
# that the 3-level graph (Bt clutched to the bottom component of B)
51
# is B*B' (i.e. delta_1 of this graph is B', delta_2 is B).
52
# The same is true for the BICs of the bottom component.
53
# * We thus obtain two maps, for each BIC B of the stratum:
54
# * top_to_bic mapping BICs of the top component to BICs of
55
# the stratum
56
# * bot_to_bic mapping BICs of the bottom component to BICs of
57
# the stratum
58
# * These maps have disjoint images.
59
# * These maps fail to be embeddings precisely when the intersection
60
# of two BICs is not irreducible (i.e. clutching different BICs
61
# to a component results in the intersection with the same divisor)
62
# or when there are automorphisms involved (i.e. several ways of
63
# undegenerating to the same BIC).
64
# We can thereby express the clutching of a product of BICs in the top
65
# and bottom components of a BIC in our stratum as a product of BICs of
66
# our stratum. Hence the procedure is recursive.
67
##
68
# Therefore, the GenDegenerationGraph needs to remember only the BICs
69
# together with, for each BIC, top and bottom components and the two maps.
70
##
71
# More precisely, the Degeneration Graph of a GeneralisedStratum
72
# consists of the following information:
73
# * The BICs inside the Stratum.
74
# * For each BIC, its top and bottom component (GeneralisedStratum
75
# together with a dictionary Stratum points -> LevelGraph points)
76
# * For each BIC, the BICs of its top and bottom component, together
77
# with the maps top_to_bic and bot_to_bic.
78
##
79
# We can now calculate the GenDegenerationGraph:
80
# * Step 1: Calculate all BICs in a GeneralisedStratum.
81
# * Step 2: Separate these into top an bottom component.
82
# * Step 3: Calculate all BICs in every top and bottom component.
83
# * Step 4: Calculate top_to_bic and bot_to_bic for each BIC in the
84
# Stratum (as dictionaries: index of BIC in top/bottom ->
85
# index of BIC in stratum)
86
##
87
# In particular, we this implies the following recursive algorithm for
88
# the EmbeddedLevelGraph of an arbitrary product of BICs in the stratum:
89
# INPUT: Product of BICs.
90
# OUTPUT: EmbeddedLevelGraph.
91
# * Step 1: Choose a BIC B from the product (e.g. the first).
92
# * Step 2: Find the preimages of the other BICs in the product under
93
# top_to_bic and bot_to_bic of B.
94
# * This gives (possibly multiple) products of BICs in the top and bottom
95
# stratum of B.
96
# * Step 3: Apply to product in top an bottom to get two EmbeddedLevelGraphs
97
# * Step 4: Return the clutching of the top and bottom graph.
98
##
99
# Moreover, we can generate the "lookup list", consisting of the non-empty
100
# products of BICs in each stratum.
101
# For this, we record all intersections that give 3-level graphs in each
102
# GenDegenerationGraph (i.e. (2,1) means that there exists a 3-level graph
103
# C such that delta(1) of C is bics[2] and delta(2) of C is bics[1]).
104
# Note that this is equivalent to 2 being in the image of top_to_bic(1).
105
##
106
# The key observation here is that any profile (i_1,...,i_n) can be
107
# written as a "domino" of 3-level graphs, i.e. (i_1,i_2)(i_2,_3)...(i_n-1,i_n).
108
##
109
# However, for the recursive generation of the lookup list, it is enough
110
# to take a profile and add the top generations of the first bic and the
111
# bottom degenerations of the last bic to obtain a profile with length
112
# increased by one (see the implementation below for more details.)
113
##
114
#######################################################################
115
#######################################################################
116
117
118
class GeneralisedStratum(SageObject):
119
"""
120
A union of (meromorphic) strata with residue conditions.
121
122
A GeneralisedStratum is uniquely identified by the following information:
123
124
* sig_list : list of signatures [sig_1,...,sig_n], where sig_i is the Signature
125
of the component i,
126
* res_cond : list of residue conditions, i.e. [R_1,...,R_n] where each R_l is
127
a list of tuples (i,j), corresponding to the j-th component of sig_i, that
128
share a residue condition (i.e. the residues at these poles add up to 0).
129
130
Note that the residue theorem for each component will be added automatically.
131
"""
132
def __init__(self, sig_list, res_cond=None):
133
assert all(sig.k == 1 for sig in sig_list)
134
self._h0 = len(sig_list)
135
self._sig_list = sig_list
136
self._n = sum([sig.n for sig in sig_list]) # total number of points
137
self._g = [sig.g for sig in sig_list]
138
# remember poles as (i,j) where i is the component and j is the index
139
# in sig_i
140
self._polelist = [(i, j) for i, sig in enumerate(sig_list)
141
for j in sig.pole_ind]
142
self._p = len(self._polelist)
143
if res_cond is None:
144
res_cond = []
145
self._res_cond = res_cond
146
self.init_more()
147
148
def init_more(self):
149
self._bics = None
150
self._smooth_LG = None
151
self._all_graphs = None
152
self._lookup_list = None
153
self._lookup = {}
154
self._DG = None
155
# cache AdditiveGenerators:
156
self._AGs = {}
157
# tautological class of self:
158
self._ONE = None
159
# tautological class of zero:
160
self._ZERO = None
161
162
def __repr__(self):
163
return "GeneralisedStratum(sig_list=%r,res_cond=%r)" % (
164
self._sig_list, self._res_cond)
165
166
def __str__(self):
167
rep = ''
168
if self._h0 > 1:
169
rep += 'Product of Strata:\n'
170
else:
171
rep += 'Stratum: '
172
for sig in self._sig_list:
173
rep += repr(sig.sig) + '\n'
174
rep += 'with residue conditions: '
175
if not self._res_cond:
176
rep += repr([]) + '\n'
177
for res in self._res_cond:
178
rep += repr(res) + '\n'
179
return rep
180
181
def info(self):
182
"""
183
Print facts about self.
184
185
This calculates everything, so could take long(!)
186
187
EXAMPLES::
188
189
sage: from admcycles.diffstrata import *
190
sage: X=GeneralisedStratum([Signature((1,1))])
191
sage: X.info()
192
Stratum: (1, 1)
193
with residue conditions: []
194
Genus: [2]
195
Dimension: 4
196
Boundary Graphs (without horizontal edges):
197
Codimension 0: 1 graph
198
Codimension 1: 4 graphs
199
Codimension 2: 4 graphs
200
Codimension 3: 1 graph
201
Total graphs: 10
202
203
sage: X=GeneralisedStratum([Signature((4,))])
204
sage: X.info()
205
Stratum: (4,)
206
with residue conditions: []
207
Genus: [3]
208
Dimension: 5
209
Boundary Graphs (without horizontal edges):
210
Codimension 0: 1 graph
211
Codimension 1: 8 graphs
212
Codimension 2: 19 graphs
213
Codimension 3: 16 graphs
214
Codimension 4: 4 graphs
215
Total graphs: 48
216
"""
217
def _graph_word(n):
218
if n == 1:
219
return "graph"
220
else:
221
return "graphs"
222
print(self)
223
print("Genus: %s" % self._g)
224
print("Dimension: %s" % self.dim())
225
print("Boundary Graphs (without horizontal edges):")
226
tot = 0
227
for c, graphs in enumerate(self.all_graphs):
228
n = len(graphs)
229
print("Codimension %s: %s %s" % (c, n, _graph_word(n)))
230
tot += n
231
print("Total graphs: %s" % tot)
232
233
def additive_generator(self, enh_profile, leg_dict=None):
234
"""
235
The AdditiveGenerator for the psi-polynomial given by leg_dict on enh_profile.
236
237
For example, if psi_2 is the psi-class at leg 2 of enh_profile,
238
the polynomial psi_2^3 would be encoded by the leg_dict {2 : 3}.
239
240
This method should always be used instead of generating AdditiveGenerators
241
directly, as the objects are cached here, i.e. the _same_ object is returned
242
on every call.
243
244
INPUT:
245
246
enh_profile: tuple
247
The enhanced profile
248
249
leg_dict: dict (optional)
250
A dictionary mapping legs of the underlying
251
graph of enh_profile to positive integers, corresponding to
252
the power of the psi class associated to this leg. Defaults to None.
253
254
OUTPUT:
255
256
The AdditiveGenerator
257
258
EXAMPLES::
259
260
sage: from admcycles.diffstrata import *
261
sage: X=Stratum((2,))
262
sage: a = X.additive_generator(((0,),0))
263
sage: a is X.additive_generator(((0,),0))
264
True
265
sage: a is AdditiveGenerator(X,((0,),0))
266
False
267
"""
268
ag_hash = hash_AG(leg_dict, enh_profile)
269
return self.additive_generator_from_hash(ag_hash)
270
271
def additive_generator_from_hash(self, ag_hash):
272
if ag_hash not in self._AGs:
273
self._AGs[ag_hash] = admcycles.diffstrata.additivegenerator.AdditiveGenerator.from_hash(
274
self, ag_hash)
275
return self._AGs[ag_hash]
276
277
def simple_poles(self):
278
"""
279
Return an iterator over simple poles.
280
281
EXAMPLES::
282
283
sage: from admcycles.diffstrata import *
284
sage: X = GeneralisedStratum([Signature((1,1))])
285
sage: list(X.simple_poles())
286
[]
287
"""
288
for p in self._polelist:
289
if self.stratum_point_order(p) == -1:
290
yield p
291
292
@cached_method
293
def is_empty(self):
294
"""
295
Checks if self fails to exist for residue reasons (simple pole with residue forced zero).
296
297
Returns:
298
bool: existence of simple pole with residue zero.
299
300
EXAMPLES::
301
302
sage: from admcycles.diffstrata import *
303
sage: X = Stratum((1,-1))
304
sage: X.is_empty()
305
True
306
sage: X = Stratum((1,1))
307
sage: X.is_empty()
308
False
309
"""
310
return any(self.smooth_LG.residue_zero(p)
311
for p in self.simple_poles())
312
313
def is_disconnected(self):
314
"""
315
Return whether self is not connected.
316
317
EXAMPLES::
318
319
sage: from admcycles.diffstrata import *
320
sage: X = Stratum((1,1))
321
sage: X.is_disconnected()
322
False
323
sage: X = GeneralisedStratum([Signature((5,1)),Signature((1,3))])
324
sage: X.is_disconnected()
325
True
326
"""
327
return self._h0 > 1
328
329
def stratum_point_order(self, p):
330
"""
331
The pole order at the stratum point p.
332
333
Args:
334
p (tuple): Point (i,j) of self.
335
336
Returns:
337
int: Pole order of p.
338
"""
339
i, j = p
340
return self._sig_list[i].sig[j]
341
342
@property
343
def bics(self):
344
"""
345
Initialise BIC list on first call.
346
347
Note that _bics is a list of tuples of EmbeddedLevelGraphs
348
(each tuple consists of one EmbeddedLevelGraph for every
349
connected component).
350
"""
351
if self.is_empty():
352
return []
353
if self._bics is None:
354
return self.gen_bic()
355
return self._bics
356
357
@property
358
def res_cond(self):
359
return self._res_cond
360
361
@property
362
def lookup_list(self):
363
"""
364
The list of all (ordered) profiles.
365
366
Note that starting with SAGE 9.0 profile numbering is no longer deterministic.
367
368
Note that this generates all graphs and can take a long time for large strata!
369
370
Returns:
371
list: Nested list of tuples.
372
373
EXAMPLES::
374
375
sage: from admcycles.diffstrata import *
376
sage: X=Stratum((2,))
377
sage: assert len(X.lookup_list) == 3
378
sage: X.lookup_list[0]
379
[()]
380
sage: X.lookup_list[1]
381
[(0,), (1,)]
382
sage: assert len(X.lookup_list[2]) == 1
383
"""
384
if self.is_empty():
385
return []
386
if self._lookup_list is None:
387
# First, we build the "lookup-list", i.e. the list of all profiles:
388
# the non-empty profiles can be found recursively:
389
# given a profile, we create new profiles by adding top and bottom
390
# degenerations of the corresponding bic to the beginning and end.
391
self._lookup_list = [[tuple()]] # only one with 0 levels
392
n = len(self.bics)
393
self._lookup_list += [[(i,) for i in range(n)]] # bics
394
new_profiles = n
395
while new_profiles:
396
# we temporarily work with a set to avoid duplicates
397
self._lookup_list.append(set())
398
for profile in self._lookup_list[-2]:
399
first = profile[0]
400
for i in self.DG.top_to_bic(first).values():
401
self._lookup_list[-1].add((i,) + profile)
402
if len(profile) > 1:
403
last = profile[-1]
404
for i in self.DG.bot_to_bic(last).values():
405
self._lookup_list[-1].add(profile + (i,))
406
self._lookup_list[-1] = list(self._lookup_list[-1])
407
new_profiles = len(self._lookup_list[-1])
408
self._lookup_list.pop()
409
return self._lookup_list
410
411
@property
412
def DG(self):
413
assert not self.is_empty()
414
if self._DG is None:
415
self._DG = admcycles.diffstrata.stratatautring.GenDegenerationGraph(
416
self)
417
return self._DG
418
419
@property
420
def ONE(self):
421
if self._ONE is None:
422
one = self.additive_generator((tuple(), 0))
423
self._ONE = one.as_taut()
424
return self._ONE
425
426
@property
427
def ZERO(self):
428
if self._ZERO is None:
429
self._ZERO = admcycles.diffstrata.elgtautclass.ELGTautClass(self, [
430
])
431
return self._ZERO
432
433
@property
434
def all_graphs(self):
435
"""
436
Nested list of all EmbeddedLevelGraphs in self.
437
438
This list is built on first call.
439
440
EXAMPLES::
441
442
sage: from admcycles.diffstrata import *
443
sage: X=GeneralisedStratum([Signature((1,1))])
444
sage: assert comp_list(X.all_graphs[0], [EmbeddedLevelGraph(X, LG=LevelGraph([2],[[1, 2]],[],{1: 1, 2: 1},[0],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0})])
445
sage: assert comp_list(X.all_graphs[1], \
446
[EmbeddedLevelGraph(X, 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}),\
447
EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2], [3, 4, 5, 6]],[(2, 5), (1, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, 0, -1],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -1: -1}),\
448
EmbeddedLevelGraph(X, LG=LevelGraph([1, 1],[[1], [2, 3, 4]],[(1, 4)],{1: 0, 2: 1, 3: 1, 4: -2},[0, -1],True),dmp={2: (0, 0), 3: (0, 1)},dlevels={0: 0, -1: -1}),\
449
EmbeddedLevelGraph(X, LG=LevelGraph([2, 0],[[1], [2, 3, 4]],[(1, 4)],{1: 2, 2: 1, 3: 1, 4: -4},[0, -1],True),dmp={2: (0, 0), 3: (0, 1)},dlevels={0: 0, -1: -1})])
450
sage: assert comp_list(X.all_graphs[2],\
451
[EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0],[[1], [2, 3, 4], [5, 6, 7, 8]],[(1, 4), (3, 8), (2, 7)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 1, 6: 1, 7: -2, 8: -2},[0, -1, -2],True),dmp={5: (0, 0), 6: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\
452
EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0],[[1, 2], [3, 4, 5], [6, 7, 8]],[(1, 4), (2, 5), (3, 8)],{1: 0, 2: 0, 3: 2, 4: -2, 5: -2, 6: 1, 7: 1, 8: -4},[0, -1, -2],True),dmp={6: (0, 0), 7: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\
453
EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2, 3], [4, 5, 6]],[(1, 3), (2, 6)],{1: 0, 2: 2, 3: -2, 4: 1, 5: 1, 6: -4},[0, -1, -2],True),dmp={4: (0, 0), 5: (0, 1)},dlevels={0: 0, -2: -2, -1: -1}),\
454
EmbeddedLevelGraph(X, LG=LevelGraph([1, 1, 0],[[1], [2], [3, 4, 5, 6]],[(2, 5), (1, 6)],{1: 0, 2: 0, 3: 1, 4: 1, 5: -2, 6: -2},[0, -1, -2],True),dmp={3: (0, 0), 4: (0, 1)},dlevels={0: 0, -2: -2, -1: -1})])
455
sage: assert comp_list(X.all_graphs[2], [EmbeddedLevelGraph(X, LG=LevelGraph([1, 0, 0, 0],[[1], [2, 3, 4], [5, 6, 7], [8, 9, 10]],[(1, 4), (3, 7), (2, 6), (5, 10)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2, 8: 1, 9: 1, 10: -4},[0, -1, -2, -3],True),dmp={8: (0, 0), 9: (0, 1)},dlevels={0: 0, -2: -2, -1: -1, -3: -3})])
456
"""
457
if self.is_empty():
458
return []
459
if self._all_graphs is None:
460
# We build the graph list from the lookup list:
461
# Note that lookup returns a list of graphs.
462
self._all_graphs = []
463
for l in self.lookup_list:
464
self._all_graphs.append(
465
list(itertools.chain.from_iterable(self.lookup(g)
466
for g in l))
467
)
468
# Ensure that degenerations of top and bottom match up:
469
assert all(k in self.DG.bot_to_bic(j).values()
470
for k in range(self.DG.n)
471
for j in self.DG.top_to_bic(k).values())
472
return self._all_graphs
473
474
@property
475
def smooth_LG(self):
476
"""
477
The smooth EmbeddedLevelGraph inside a LevelStratum.
478
479
Note that the graph might be disconnected!
480
481
EXAMPLES::
482
483
sage: from admcycles.diffstrata import *
484
sage: X=GeneralisedStratum([Signature((1,1))])
485
sage: assert X.smooth_LG.is_isomorphic(EmbeddedLevelGraph(X,LG=LevelGraph([2],[[1, 2]],[],{1: 1, 2: 1},[0],True),dmp={1: (0, 0), 2: (0, 1)},dlevels={0: 0}))
486
487
Note that we get a single disconnected graph if the stratum is
488
disconnected.
489
490
sage: X=GeneralisedStratum([Signature((0,)), Signature((0,))])
491
sage: X.smooth_LG
492
EmbeddedLevelGraph(LG=LevelGraph([1, 1],[[1], [2]],[],{1: 0, 2: 0},[0, 0],True),dmp={1: (0, 0), 2: (1, 0)},dlevels={0: 0})
493
494
Returns:
495
EmbeddedLevelGraph: The output of unite_embedded_graphs applied to
496
the (embedded) smooth_LGs of each component of self.
497
"""
498
if not self._smooth_LG:
499
graph_list = []
500
for i, sig in enumerate(self._sig_list):
501
g = admcycles.diffstrata.levelgraph.smooth_LG(sig)
502
dmp = {j: (i, j - 1) for j in range(1, sig.n + 1)}
503
graph_list.append((self, g, dmp, {0: 0}))
504
self._smooth_LG = unite_embedded_graphs(tuple(graph_list))
505
return self._smooth_LG
506
507
@cached_method
508
def residue_matrix(self):
509
"""
510
Calculate the matrix associated to the residue space,
511
i.e. a matrix with a line for every residue condition and a column for every pole of self.
512
513
The residue conditions consist ONLY of the ones coming from the GRC (in _res_cond)
514
For inclusion of the residue theorem on each component, use smooth_LG.full_residue_matrix!
515
"""
516
return self.matrix_from_res_conditions(self._res_cond)
517
518
def matrix_from_res_conditions(self, res_conds):
519
"""
520
Calculate the matrix for a list of residue conditions, i.e.
521
a matrix with one line for every residue condition and a column for each pole of self.
522
523
Args:
524
res_conds (list): list of residue conditions, i.e. a nested list R
525
R = [R_1,R_2,...] where each R_i is a list of poles (stratum points)
526
whose residues add up to zero.
527
528
Returns:
529
SAGE matrix: residue matrix (with QQ coefficients)
530
531
EXAMPLES::
532
533
sage: from admcycles.diffstrata import *
534
sage: X=GeneralisedStratum([Signature((2,-2,-2)),Signature((2,-2,-2))])
535
sage: X.matrix_from_res_conditions([[(0,1),(0,2),(1,2)],[(0,1),(1,1)],[(1,1),(1,2)]])
536
[1 1 0 1]
537
[1 0 1 0]
538
[0 0 1 1]
539
"""
540
res_vec = []
541
for res_c in res_conds:
542
# note: int(True)=1, int(False)=0
543
res_vec += [[int(p in res_c) for p in self._polelist]]
544
return matrix(QQ, res_vec)
545
546
@cached_method
547
def residue_matrix_rank(self):
548
return self.residue_matrix().rank()
549
550
@cached_method
551
def dim(self):
552
"""
553
Return the dimension of the stratum, that is the sum of 2g_i + n_i - 1 - residue conditions -1 for projective.
554
555
The residue conditions are given by the rank of the (full!) residue matrix.
556
557
Empty strata return -1.
558
559
EXAMPLES::
560
561
sage: from admcycles.diffstrata import *
562
sage: X=GeneralisedStratum([Signature((4,))])
563
sage: all(B.top.dim() + B.bot.dim() == X.dim()-1 for B in X.bics)
564
True
565
"""
566
if self.is_empty():
567
return -1
568
# add residue conditions from RT for every connected component:
569
M = self.smooth_LG.full_residue_matrix
570
return (sum([2 * sig.g + sig.n - 1 for sig in self._sig_list])
571
- M.rank() - 1)
572
573
@property
574
def N(self):
575
"""
576
Unprojectivised dimension of self.
577
578
Returns:
579
int: dim() + 1
580
"""
581
return self.dim() + 1
582
583
def gen_bic(self):
584
"""
585
Generates all BICs (using bic) as EmbeddedLevelGraphs.
586
587
Returns:
588
list: self._bics i.e. a list of (possibly disconnected)
589
EmbeddedLevelGraphs.
590
(More precisely, each one is a tuple consisting of one
591
EmbeddedLevelGraph for every connected component that has
592
been fed to unite_embedded_graphs).
593
"""
594
self._bics = []
595
if self.is_empty():
596
return
597
# The BICs are the products of BICs for each connected component
598
# (satisfying the residue condition).
599
# Moreover, if there are several connected components, we also need
600
# to include the smooth stratum on each level.
601
emb_bic_list = []
602
603
# First, we establish the dictionaries for the EmbeddedLevelGraphs:
604
# * the marked points of the stratum are numbered (i,j) where (i,j)
605
# is the j-th point on the i-th connected component.
606
# Note that j is the index in sig, i.e. starts at 0.
607
# * on each BIC, the i-th point of the signature is the point i+1
608
# mp_dict maps the points on the BIC to the points of the stratum
609
for i, sig in enumerate(self._sig_list):
610
mp_dict = {j: (i, j - 1) for j in range(1, sig.n + 1)}
611
# We can't build the EmbeddedLevelGraph until we have the data for all
612
# components (otherwise we mess up the legality check, etc.)
613
# So for now, we just store the generating info for each connected
614
# component separately.
615
emb_bic_list_cur = []
616
for g in admcycles.diffstrata.bic.bic_alt_noiso(sig.sig):
617
level_dict = {g.internal_level_number(0): 0,
618
g.internal_level_number(-1): -1}
619
EG = (self, g, mp_dict, level_dict)
620
emb_bic_list_cur.append(EG)
621
if self._h0 > 1:
622
# we need the smooth component on each level
623
for l in [0, -1]:
624
emb_bic_list_cur.append((self, admcycles.diffstrata.levelgraph.smooth_LG(sig), mp_dict,
625
{0: l}, # one for each level
626
)
627
)
628
emb_bic_list.append(emb_bic_list_cur)
629
# The elements of _bics are now products of the (embedded) bics of the components
630
# Careful: The only ones that are not allowed are those, where all
631
# components are on one level!!
632
prod_bics = itertools.product(*emb_bic_list)
633
for prod_graph in prod_bics:
634
# levels are stored in values of the dict in component 3 of each
635
# tuple:
636
if any(0 in g[3].values() for g in prod_graph) and \
637
any(-1 in g[3].values() for g in prod_graph):
638
# NOTE: This actually builds the EmbeddedLevelGraphs!
639
pg = unite_embedded_graphs(prod_graph)
640
if pg.is_legal(): # check r-GRC
641
self._bics.append(pg)
642
# isomorphism classes: (possibly figure out how to check earlier?)
643
self._bics = admcycles.diffstrata.bic.isom_rep(self._bics)
644
return self._bics
645
646
# Ideally, we could always work with enhanced profiles, never with graphs.
647
# Then edge maps could work like this:
648
# Def: A leg is a tuple consisting of:
649
# * an enhanced profile (of a levelgraph)
650
# * the levelstratum inside the profile (e.g. for the profile (1,2,3) this would
651
# be either 1^top, 3^bot, (12) or (23)). These were computed for the stratum anyway.
652
# * an orbit of a marked point of this gen levelstratum, which corresponds to an edge
653
# of the corresponding graph
654
# i.e. an ordered tuple of marked points equivalent by automorphisms of the corresponding
655
# BIC or 3-level graph (which should be also an automorphism of the full graph??!!)
656
##
657
# Then:
658
# INPUT: (leg, enhanced profile)
659
# The enhanced profile should be that of a degeneration of the graph of leg (!)
660
##
661
# OUTPUT: leg (on the second profile)
662
##
663
# Case 1:
664
# The levelstratum of the leg is unchanged by the degeneration.
665
# i.e.: (1,2) and (1,2,3) for an edge on (1,2).
666
# In this case the output is trivially the same edge embedded into (1,2,3)
667
# (because (1,2) is still a level of (1,2,3)).
668
##
669
# Case 2:
670
# The levelstratum is degenerated,
671
# i.e.: (1,2) and (1,3,2) for an leg e on (1,2).
672
# In this case we know that e (by checking the sign of the order) is either
673
# a leg on 1^bot or 2^top and the degeneration is given by top_to_bic_inv (or
674
# bot_to_bic_inv) of 3, where we can then track the marked point associated to e.
675
####
676
677
# TODO: This should work "smarter", see above.
678
@cached_method
679
def explicit_leg_maps(self, enh_profile, enh_deg_profile, only_one=False):
680
"""
681
Provide explicit leg maps (as list of dictionaries: legs of LG to legs of LG), from
682
the graph associated to enh_profile to the one associated to enh_deg_profile.
683
684
If enh_deg_profile is not a degeneration (on the level of profiles), None is
685
returned.
686
687
Raises a RuntimeError if enh_profile is empty and a UserWarning if
688
there are no degenerations in the appropriate profile.
689
690
INPUT:
691
692
enh_profile (enhanced profile): tuple (profile, index).
693
694
enh_deg_profile (enhanced profile): tuple (profile, index).
695
696
only_one (bool, optional): Give only one (the 'first') map (or None if none exist).
697
Defaults to False.
698
699
OUTPUT:
700
701
list of dicts: List of the leg isomorphisms, None if not a degeneration,
702
only one dict if only_one=True.
703
"""
704
profile = enh_profile[0]
705
deg_profile = enh_deg_profile[0]
706
# check if deg_profile is actually a (profile) degeneration:
707
if not set(profile).issubset(set(deg_profile)):
708
return None
709
g = self.lookup_graph(*enh_profile)
710
degen = self.lookup_graph(*enh_deg_profile)
711
if not degen:
712
raise RuntimeError("%r is not a graph in %r!" %
713
(enh_deg_profile, self))
714
# To obtain g, we have to squish degen at the places in the profile
715
# of degen that are not in the profile of g.
716
# We work from right to left to avoid confusion with the level
717
# numbering.
718
degen_squish = degen
719
for level, bic_index in list(enumerate(deg_profile))[::-1]:
720
if bic_index in profile:
721
continue
722
degen_squish = degen_squish.squish_vertical(level)
723
isoms = (l_iso for v_iso, l_iso in g.isomorphisms(degen_squish))
724
try:
725
first_isom = next(isoms)
726
except StopIteration:
727
# No isomorphisms found
728
raise UserWarning("Squish of %r not isomorphic to %r!" %
729
(enh_deg_profile, enh_profile))
730
if only_one:
731
return first_isom
732
else:
733
return [first_isom] + list(isoms)
734
735
#####
736
# Common degenerations:
737
# This should eat two graphs, given by their "enhanced profile" i.e. things we can
738
# feed to graph_lookup (a list of BICs and an index) and also return a list of
739
# enhanced profiles.
740
741
# Naive approach:
742
# do a (set-wise) degeneration of the profile and just go through the list
743
# checking which ones are actually degenerations:
744
# INPUT: Profile + index
745
# OUTPUT: List of profiles + indices
746
747
# TODO: There should be a smart way! For that one has to understand
748
# how to correctly encode the irreducible components of the profiles.
749
750
@cached_method
751
def common_degenerations(self, s_enh_profile, o_enh_profile):
752
"""
753
Find common degenerations of two graphs.
754
755
INPUT:
756
757
s_enh_profile (tuple): Enhanced profile, i.e. a tuple (p,k) consisting of
758
759
* a sorted (!) profile p
760
* an index in self.lookup(p)
761
thus giving the information of an EmbeddedLevelGraph in self.
762
763
o_enh_profile (tuple): Enhanced profile.
764
765
OUTPUT:
766
767
A list of enhanced profiles, i.e. entries of type [tuple profile, index]
768
(that undegenerate to the two given graphs).
769
770
EXAMPLES::
771
772
sage: from admcycles.diffstrata import *
773
sage: X=GeneralisedStratum([Signature((4,))])
774
775
To retrieve the actual EmbeddedLevelGraphs, we must use lookup_graph.
776
(Because of BIC renumbering between different SAGE versions we can't provide any concrete examples :/)
777
778
Note that the number of components can also go down.
779
780
Providing common graphs works::
781
782
sage: X.common_degenerations(((2,),0),((2,),0))
783
[((2,), 0)]
784
785
Empty intersection gives empty list.
786
787
"""
788
s_profile = s_enh_profile[0]
789
o_profile = o_enh_profile[0]
790
try:
791
# merge_profiles returns None if there aren't any...
792
deg_profile = tuple(self.merge_profiles(s_profile, o_profile))
793
except TypeError:
794
return []
795
return_list = []
796
# careful with reducible profiles:
797
for i in range(len(self.lookup(deg_profile))):
798
if self.is_degeneration(
799
(deg_profile, i), s_enh_profile) and self.is_degeneration(
800
(deg_profile, i), o_enh_profile):
801
return_list.append((deg_profile, i))
802
return return_list
803
804
# Excess intersection of two additive generators in an ambient graph
805
def intersection(self, s_taut_class, o_taut_class, amb_enh_profile=None):
806
"""
807
Excess intersection of two tautological classes in Chow of ambient_enh_profile.
808
809
Raises a RuntimeError if any summand of any tautological class is not on
810
a degeneration of ambient_enh_profile.
811
812
INPUT:
813
814
s_taut_class (ELGTautClass): tautological class
815
816
o_taut_class (ELGTautClass): tautological class
817
818
amb_enh_profile (tuple, optional): enhanced profile of ambient graph.
819
Defaults to None.
820
821
OUTPUT:
822
823
ELGTautClass: Tautological class on common degenerations
824
"""
825
# check input:
826
if amb_enh_profile is None:
827
amb_enh_profile = ((), 0)
828
if s_taut_class == 0 or s_taut_class == self.ZERO:
829
return self.ZERO
830
if s_taut_class == 1 or s_taut_class == self.ONE:
831
return o_taut_class
832
if o_taut_class == 0 or o_taut_class == self.ZERO:
833
return self.ZERO
834
if o_taut_class == 1 or o_taut_class == self.ONE:
835
return s_taut_class
836
return_list = []
837
# unpack tautological classes:
838
for s_coeff, s_add_gen in s_taut_class.psi_list:
839
for o_coeff, o_add_gen in o_taut_class.psi_list:
840
prod = self.intersection_AG(
841
s_add_gen, o_add_gen, amb_enh_profile)
842
if prod == 0 or prod == self.ZERO:
843
continue
844
return_list.append(s_coeff * o_coeff * prod)
845
return_value = self.ELGsum(return_list)
846
if return_value == 0:
847
return self.ZERO
848
if s_taut_class.is_equidimensional() and o_taut_class.is_equidimensional():
849
assert return_value.is_equidimensional(),\
850
"Product of equidimensional classes is not equidimensional! %s * %s = %s"\
851
% (s_taut_class, o_taut_class, return_value)
852
return return_value
853
854
@cached_method
855
def intersection_AG(self, s_add_gen, o_add_gen, amb_enh_profile=None):
856
"""
857
Excess intersection formula for two AdditiveGenerators in Chow of amb_enh_profile.
858
859
Note that as AdditiveGenerators and enhanced profiles are hashable,
860
this method can (and will) be cached (in contrast with intersection).
861
862
Raises a RuntimeError if any of the AdditiveGenerators is not on
863
a degeneration of ambient_enh_profile.
864
865
INPUT:
866
867
s_add_gen (AdditiveGenerator): first AG
868
o_add_gen (AdditiveGenerator): second AG
869
amb_enh_profile (tuple, optional): enhanced profile of ambient graph.
870
Defaults to None.
871
872
OUTPUT:
873
874
A Tautological class on common degenerations
875
"""
876
if amb_enh_profile is None:
877
amb_enh_profile = ((), 0)
878
s_enh_profile = s_add_gen.enh_profile
879
o_enh_profile = o_add_gen.enh_profile
880
if not self.is_degeneration(s_enh_profile, amb_enh_profile):
881
raise RuntimeError("%r is not a degeneration of %r" %
882
(s_enh_profile, amb_enh_profile))
883
if not self.is_degeneration(o_enh_profile, amb_enh_profile):
884
raise RuntimeError("%r is not a degeneration of %r" %
885
(o_enh_profile, amb_enh_profile))
886
# Degree check:
887
# * the degree of the product is the sum of the degrees
888
# * the product is supported on a set of codim >= max(codim(s),codim(o))
889
# => if the sum of the degrees is > (dim(self) - max(codim(s),codim(o)))
890
# the product will be 0 in any case
891
# NOTE: degree = codim + psi-degree
892
deg_sum = s_add_gen.psi_degree + o_add_gen.psi_degree
893
if deg_sum > self.dim() - \
894
max(len(s_enh_profile[0]), len(o_enh_profile[0])):
895
return self.ZERO
896
degenerations = self.common_degenerations(s_enh_profile, o_enh_profile)
897
if not degenerations:
898
return self.ZERO
899
NB = self.cnb(s_enh_profile, o_enh_profile, amb_enh_profile)
900
if NB == 1:
901
# Intersection is transversal, in this case we are done:
902
# the pullback of an additive generator is a taut class
903
# where all classes live on the same graph:
904
prod = [(_cs * _co, s_pb * o_pb)
905
for L in degenerations
906
for _cs, s_pb in s_add_gen.pull_back(L).psi_list
907
for _co, o_pb in o_add_gen.pull_back(L).psi_list]
908
return admcycles.diffstrata.elgtautclass.ELGTautClass(
909
self, list(prod))
910
elif NB == 0 or NB == self.ZERO:
911
# product with 0 is 0 ...
912
return NB # maybe better: always self.ZERO?
913
else:
914
# intersect the pullback to L with the normal bundle pulled back to
915
# L (in L):
916
summands = [self.intersection(
917
self.intersection(
918
s_add_gen.pull_back(L),
919
o_add_gen.pull_back(L),
920
L),
921
self.gen_pullback_taut(
922
NB,
923
L,
924
self.minimal_common_undegeneration(
925
s_enh_profile, o_enh_profile)
926
),
927
L)
928
for L in degenerations]
929
return self.ELGsum(list(summands))
930
931
def normal_bundle(self, enh_profile, ambient=None):
932
"""
933
Normal bundle of enh_profile in ambient.
934
935
Note that this is equivalent to cnb(enh_profile, enh_profile, ambient).
936
937
Args:
938
enh_profile (tuple): enhanced profile
939
ambient (tuple, optional): enhanced profile. Defaults to None.
940
941
Raises:
942
ValueError: Raised if enh_profile is not a codim 1 degeneration of ambient
943
944
Returns:
945
ELGTautClass: Normal bundle N_{enh_profile, ambient}
946
"""
947
if ambient is None:
948
ambient = ((), 0)
949
else:
950
ambient = (tuple(ambient[0]), ambient[1])
951
if len(enh_profile[0]) != len(ambient[0]) + \
952
1 or not self.is_degeneration(enh_profile, ambient):
953
raise ValueError(
954
"%r is not a codim 1 degeneration of %r" %
955
(enh_profile, ambient))
956
return self.cnb(enh_profile, enh_profile, ambient)
957
958
# This is an element of CH^s(ambient) where s is the cardinality of the intersection of profiles
959
# or equivalently in CH^(c+s)(B) where c is the codimension of ambient.
960
@cached_method
961
def cnb(self, s_enh_profile, o_enh_profile, amb_enh_profile=None):
962
"""
963
Common Normal bundle of two graphs in an ambient graph.
964
965
Note that for a trivial normal bundle (transversal intersection)
966
we return 1 (int) and NOT self.ONE !!
967
968
The reason is that the correct ONE would be the ambient graph and that
969
is a pain to keep track of in intersection....
970
971
Raises a RuntimeError if s_enh_profile or o_enh_profile do not
972
degenerate from amb_enh_profile.
973
974
INPUT:
975
976
s_enh_profile (tuple): enhanced profile
977
o_enh_profile (tuple): enhanced profile
978
amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.
979
980
OUTPUT:
981
982
ELGTautClass: Product of normal bundles appearing.
983
1 if the intersection is transversal.
984
"""
985
# check/normalise input:
986
if amb_enh_profile is None:
987
amb_enh_profile = ((), 0)
988
else:
989
amb_enh_profile = (tuple(amb_enh_profile[0]), amb_enh_profile[1])
990
if not self.is_degeneration(s_enh_profile, amb_enh_profile):
991
raise RuntimeError("%r is not a degeneration of %r" %
992
(s_enh_profile, amb_enh_profile))
993
if not self.is_degeneration(o_enh_profile, amb_enh_profile):
994
raise RuntimeError("%r is not a degeneration of %r" %
995
(o_enh_profile, amb_enh_profile))
996
min_com = self.minimal_common_undegeneration(
997
s_enh_profile, o_enh_profile)
998
if min_com == amb_enh_profile:
999
return 1 # terminating condition, transversal
1000
else:
1001
assert self.codim_one_common_undegenerations(s_enh_profile, o_enh_profile, amb_enh_profile),\
1002
"minimal common undegeneration is %r, ambient profile is %r, but there aren't codim one common undegenerations!"\
1003
% (min_com, amb_enh_profile)
1004
return_list = []
1005
for ep in self.codim_one_common_undegenerations(
1006
s_enh_profile, o_enh_profile, amb_enh_profile):
1007
p, i = ep
1008
# This is the "difference" between ep and amb_enh_profile:
1009
# i.e. the inserted level, i in paper notation
1010
squished_level = get_squished_level(ep, amb_enh_profile)
1011
ll = self.bics[p[squished_level]].ell
1012
xi_top = self.xi_at_level(squished_level, ep, quiet=True)
1013
xi_bot = self.xi_at_level(squished_level + 1, ep, quiet=True)
1014
xis = -xi_top + xi_bot
1015
summand = 1 / QQ(ll) * self.gen_pullback_taut(xis, min_com, ep)
1016
# calL pulled back to min_com:
1017
summand -= 1 / \
1018
QQ(ll) * self.gen_pullback_taut(self.calL(ep,
1019
squished_level), min_com, ep)
1020
if summand == 0:
1021
# product is zero!
1022
return self.ZERO
1023
assert summand.is_equidimensional(),\
1024
"Not all summands in %s of same degree!" % summand
1025
return_list.append(summand)
1026
# product over normal bundles:
1027
if not return_list:
1028
return 1 # empty product => transversal
1029
NBprod = return_list[0]
1030
for nb in return_list[1:]:
1031
NBprod = self.intersection(NBprod, nb, min_com)
1032
assert NBprod.is_equidimensional(), "Not all summands in %s of same degree!" % NBprod
1033
return NBprod
1034
1035
@cached_method
1036
def gen_pullback(self, add_gen, o_enh_profile, amb_enh_profile=None):
1037
"""
1038
Generalised pullback of additive generator to o_enh_profile in amb_enh_profile.
1039
1040
Args:
1041
add_gen (AdditiveGenerator): additive generator on a degeneration of amb_enh_profile.
1042
o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)
1043
amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.
1044
1045
Raises:
1046
RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.
1047
1048
Returns:
1049
ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.
1050
"""
1051
# check input:
1052
if amb_enh_profile is None:
1053
amb_enh_profile = ((), 0)
1054
if not self.is_degeneration(o_enh_profile, amb_enh_profile):
1055
raise RuntimeError("%r is not a degeneration of %r" %
1056
(o_enh_profile, amb_enh_profile))
1057
s_enh_profile = add_gen.enh_profile
1058
if not self.is_degeneration(s_enh_profile, amb_enh_profile):
1059
raise RuntimeError("%r is not a degeneration of %r" %
1060
(s_enh_profile, amb_enh_profile))
1061
degenerations = self.common_degenerations(s_enh_profile, o_enh_profile)
1062
# if there are no common degenerations, pullback is 0
1063
if not degenerations:
1064
return self.ZERO
1065
NB = self.cnb(s_enh_profile, o_enh_profile, amb_enh_profile)
1066
# stop condition
1067
if NB == 0 or NB == self.ZERO:
1068
return 0
1069
return_list = []
1070
for L in degenerations:
1071
if NB == 1:
1072
# transversal
1073
return_list.append(add_gen.pull_back(L))
1074
else:
1075
return_list.append(
1076
self.intersection(
1077
self.gen_pullback_taut(
1078
NB,
1079
L,
1080
self.minimal_common_undegeneration(
1081
s_enh_profile,
1082
o_enh_profile)),
1083
add_gen.pull_back(L),
1084
L))
1085
return_value = self.ELGsum(return_list)
1086
if return_value != 0:
1087
return return_value
1088
else:
1089
return self.ZERO
1090
1091
def gen_pullback_taut(self, taut_class, o_enh_profile,
1092
amb_enh_profile=None):
1093
"""
1094
Generalised pullback of tautological class to o_enh_profile in amb_enh_profile.
1095
1096
This simply returns the ELGSum of gen_pullback of all AdditiveGenerators.
1097
1098
Args:
1099
taut_class (ELGTautClass): tautological class each summand on a degeneration of amb_enh_profile.
1100
o_enh_profile (tuple): enhanced profile (degeneration of amb_enh_profile)
1101
amb_enh_profile (tuple, optional): enhanced profile. Defaults to None.
1102
1103
Raises:
1104
RuntimeError: If one of the above is not actually a degeneration of amb_enh_profile.
1105
1106
Returns:
1107
ELGTautClass: Tautological class on common degenerations of AdditiveGenerator profile and o_enh_profile.
1108
"""
1109
return_list = []
1110
for c, AG in taut_class.psi_list:
1111
return_list.append(
1112
c * self.gen_pullback(AG, o_enh_profile, amb_enh_profile))
1113
return self.ELGsum(return_list)
1114
1115
# TODO: There should be a better way for this, using just BICs and where
1116
# marked points go ... (see discussion above)
1117
@cached_method
1118
def explicit_edge_becomes_long(self, enh_profile, edge):
1119
"""
1120
A list of enhanced profiles where the (explicit) edge 'edge' became long.
1121
1122
We go through the codim one degenerations of enh_profile and check
1123
each graph, if edge became long (under any degeneration).
1124
1125
However, we count each graph only once, even if there are several ways
1126
to undegenerate (see examples).
1127
1128
Raises a RuntimeError if the leg is not a leg of the graph of enh_profile.
1129
1130
INPUT:
1131
1132
enh_profile (tuple): enhanced profile: (profile, index).
1133
1134
edge (tuple): edge of the LevelGraph associated to enh_profile:
1135
(start leg, end leg).
1136
1137
OUTPUT:
1138
1139
The list of enhanced profiles.
1140
1141
EXAMPLES::
1142
1143
sage: from admcycles.diffstrata import *
1144
sage: X=Stratum((1,1))
1145
sage: V=[ep for ep in X.enhanced_profiles_of_length(1) if X.lookup_graph(*ep).level(0)._h0 == 2]
1146
sage: epV=V[0]
1147
sage: VLG=X.lookup_graph(*epV).LG
1148
sage: assert len(X.explicit_edge_becomes_long(epV, VLG.edges[1])) == 1
1149
sage: assert X.explicit_edge_becomes_long(epV, VLG.edges[1]) == X.explicit_edge_becomes_long(epV, VLG.edges[1])
1150
1151
"""
1152
ep_list = []
1153
for ep in self.codim_one_degenerations(enh_profile):
1154
g = self.lookup_graph(*ep)
1155
if g.LG.has_long_edge:
1156
for leg_map in self.explicit_leg_maps(enh_profile, ep):
1157
try:
1158
if g.LG.is_long((leg_map[edge[0]], leg_map[edge[1]])):
1159
ep_list.append(ep)
1160
# Not sure, if we want to record several
1161
# occurrences...
1162
break
1163
except KeyError:
1164
raise RuntimeError(
1165
"%r does not seem to be an edge of %r" %
1166
(edge, enh_profile))
1167
return ep_list
1168
1169
@cached_method
1170
def explicit_edges_between_levels(
1171
self, enh_profile, start_level, stop_level):
1172
"""
1173
Edges going from (relative) level start_level to (relative) level stop_level.
1174
1175
Note that we assume here that edges respect the level structure, i.e.
1176
start on start_level and end on end_level!
1177
1178
Args:
1179
enh_profile (tuple): enhanced profile
1180
start_level (int): relative level number (0...codim)
1181
stop_level (int): relative level number (0...codim)
1182
1183
Returns:
1184
list: list of edges, i.e. tuples (start_point,end_point)
1185
1186
EXAMPLES::
1187
1188
sage: from admcycles.diffstrata import *
1189
sage: X=Stratum((2,))
1190
1191
Compact type:
1192
1193
sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 1]) == 1
1194
1195
Banana:
1196
1197
sage: assert len([ep for ep in X.enhanced_profiles_of_length(1) if len(X.explicit_edges_between_levels(ep,0,1)) == 2]) == 1
1198
1199
"""
1200
G = self.lookup_graph(*enh_profile)
1201
# TODO: There should be a way smarter way for doing this...
1202
edges = [
1203
e for e in G.LG.edges if (
1204
G.LG.level_number(
1205
G.LG.levelofleg(
1206
e[0])) == start_level and G.LG.level_number(
1207
G.LG.levelofleg(
1208
e[1])) == stop_level)]
1209
return edges
1210
1211
# Finding codimension one degenerations:
1212
# This is not very fancy yet.
1213
# At the moment, we take a profile and check at which places we can compatibly
1214
# insert a BIC (similarly to creating the lookup_list).
1215
# We then check "by hand", if this is ok with the enhanced structure, i.e.
1216
# on connected components.
1217
# Note that this check is bypassed if the input profile is irreducible.
1218
@cached_method
1219
def codim_one_degenerations(self, enh_profile):
1220
"""
1221
Degenerations of enh_profile with one more level.
1222
1223
Raises a RuntimeError if we find a degeneration that doesn't squish
1224
back to the graph we started with.
1225
1226
INPUT:
1227
1228
enh_profile (enhanced profile): tuple (profile, index)
1229
1230
OUTPUT:
1231
1232
list of enhanced profiles.
1233
1234
EXAMPLES::
1235
1236
sage: from admcycles.diffstrata import *
1237
sage: X=GeneralisedStratum([Signature((4,))])
1238
sage: assert all(len(p) == 2 for p, _ in X.codim_one_degenerations(((2,),0)))
1239
1240
Empty profile gives all bics:
1241
1242
sage: assert X.codim_one_degenerations(((),0)) == [((0,), 0), ((1,), 0), ((2,), 0), ((3,), 0), ((4,), 0), ((5,), 0), ((6,), 0), ((7,), 0)]
1243
"""
1244
profile = list(enh_profile[0])
1245
# empty profile gives all bics:
1246
if not profile:
1247
return [((b,), 0) for b in range(len(self.bics))]
1248
deg_list = []
1249
# build all length 1 profile extensions:
1250
# The first and last entry don't have any compatibility conditions:
1251
# add all top degenerations of the first guy
1252
for bic in self.DG.top_to_bic(profile[0]).values():
1253
deg_list.append(tuple([bic] + profile[:]))
1254
# and all bottom degenerations of the last guy
1255
for bic in self.DG.bot_to_bic(profile[-1]).values():
1256
deg_list.append(tuple(profile[:] + [bic]))
1257
# For the "middle" entries of the profile, we have to check
1258
# compatibility
1259
for i in range(len(profile) - 1):
1260
for bic in self.DG.bot_to_bic(profile[i]).values(): # candidates
1261
if bic in self.DG.top_to_bic(profile[i + 1]).values():
1262
deg_list.append(
1263
tuple(profile[:i + 1] + [bic] + profile[i + 1:]))
1264
deg_list = list(set(deg_list)) # remove duplicates
1265
# We now build the list of enhanced profiles:
1266
enh_list = []
1267
for p in deg_list:
1268
for i in range(len(self.lookup(p))):
1269
if self.is_degeneration((p, i), enh_profile):
1270
enh_list.append((p, i))
1271
return enh_list
1272
1273
@cached_method
1274
def codim_one_common_undegenerations(
1275
self, s_enh_profile, o_enh_profile, amb_enh_profile=None):
1276
"""
1277
Profiles that are 1-level degenerations of amb_enh_profile and include
1278
s_enh_profile and o_enh_profile.
1279
1280
INPUT:
1281
1282
s_enh_profile (tuple): enhanced profile
1283
o_enh_profile (tuple): enhanced profile
1284
amb_enh_profile (tuple): enhanced profile
1285
1286
OUTPUT:
1287
1288
list of enhanced profiles
1289
"""
1290
if amb_enh_profile is None:
1291
amb_enh_profile = ((), 0)
1292
profile_list = []
1293
for ep in self.codim_one_degenerations(amb_enh_profile):
1294
if self.is_degeneration(
1295
s_enh_profile,
1296
ep) and self.is_degeneration(
1297
o_enh_profile,
1298
ep):
1299
profile_list.append(ep)
1300
return profile_list
1301
1302
@cached_method
1303
def minimal_common_undegeneration(self, s_enh_profile, o_enh_profile):
1304
"""
1305
The minimal dimension graph that is undegeneration of both s_enh_profile
1306
and o_enh_profile.
1307
1308
Raises a RuntimeError if there are no common undgenerations in the intersection profile.
1309
1310
INPUT:
1311
1312
s_enh_profile (tuple): enhanced profile
1313
o_enh_profile (tuple): enhanced profile
1314
1315
OUTPUT:
1316
1317
tuple: enhanced profile
1318
"""
1319
s_profile = s_enh_profile[0]
1320
o_profile = o_enh_profile[0]
1321
# build "sorted" intersection
1322
intersection = []
1323
for b in s_profile:
1324
if b in o_profile:
1325
intersection.append(b)
1326
# make hashable
1327
intersection = tuple(intersection)
1328
# if the intersection profile is irreducible, we are done:
1329
if len(self.lookup(intersection)) == 1:
1330
return (intersection, 0)
1331
else:
1332
for i in range(len(self.lookup(intersection))):
1333
if (self.is_degeneration(s_enh_profile, (intersection, i))
1334
and self.is_degeneration(o_enh_profile, (intersection, i))):
1335
return (intersection, i)
1336
else:
1337
raise RuntimeError(
1338
"No common undegeneration in profile %r" % intersection)
1339
1340
@cached_method
1341
def is_degeneration(self, s_enh_profile, o_enh_profile):
1342
"""
1343
Check if s_enh_profile is a degeneration of o_enh_profile.
1344
1345
Args:
1346
s_enh_profile (tuple): enhanced profile
1347
o_enh_profile (tuple): enhanced profile
1348
1349
Returns:
1350
bool: True if the graph associated to s_enh_profile is a degeneration
1351
of the graph associated to o_enh_profile, False otherwise.
1352
1353
EXAMPLES::
1354
1355
sage: from admcycles.diffstrata import *
1356
sage: X=GeneralisedStratum([Signature((4,))])
1357
sage: assert X.is_degeneration(((7,),0),((7,),0))
1358
1359
The empty tuple corresponds to the stratum:
1360
1361
sage: assert X.is_degeneration(((2,),0),((),0))
1362
"""
1363
s_profile = s_enh_profile[0]
1364
o_profile = o_enh_profile[0]
1365
# first check: subset:
1366
if not set(o_profile) <= set(s_profile):
1367
return False
1368
# in the irreducible case, we are done:
1369
if len(self.lookup(s_profile)) == len(self.lookup(o_profile)) == 1:
1370
assert self.explicit_leg_maps(o_enh_profile, s_enh_profile),\
1371
"%r and %r contain only one graph, but these are not degenerations!"\
1372
% (o_enh_profile, s_enh_profile)
1373
return True
1374
else:
1375
# otherwise: check if an undegeneration map exists:
1376
try:
1377
if self.explicit_leg_maps(
1378
o_enh_profile, s_enh_profile, only_one=True) is None:
1379
return False
1380
else:
1381
return True
1382
except UserWarning:
1383
# This is raised if there is no undegeneration inside the
1384
# expected profile...
1385
return False
1386
1387
@cached_method
1388
def squish(self, enh_profile, l):
1389
"""
1390
Squish level l of the graph associated to enh_profile. Returns the enhanced profile
1391
associated to the squished graph.
1392
1393
Args:
1394
enh_profile (tuple): enhanced profile
1395
l (int): level of graph associated to enhanced profile
1396
1397
Raises:
1398
RuntimeError: Raised if a BIC is squished at a level other than 0.
1399
RuntimeError: Raised if the squished graph is not found in the squished profile.
1400
1401
Returns:
1402
tuple: enhanced profile.
1403
1404
EXAMPLES::
1405
1406
sage: from admcycles.diffstrata import *
1407
sage: X=Stratum((2,))
1408
sage: assert all(X.squish(ep,0) == ((),0) for ep in X.enhanced_profiles_of_length(1))
1409
sage: assert all(X.squish((p,i),1-l) == ((p[l],),0) for p, i in X.enhanced_profiles_of_length(2) for l in range(2))
1410
"""
1411
p, i = enh_profile
1412
if len(p) == 1:
1413
if l != 0:
1414
raise RuntimeError(
1415
"BIC can only be squished at level 0!")
1416
return ((), 0)
1417
new_p = list(p)
1418
new_p.pop(l)
1419
new_p = tuple(new_p)
1420
enhancements = []
1421
for j in range(len(self.lookup(new_p))):
1422
if self.is_degeneration(enh_profile, (new_p, j)):
1423
enhancements.append(j)
1424
if len(enhancements) != 1:
1425
raise RuntimeError(
1426
"Cannot squish %r at level %r! No unique graph found in %r!" %
1427
(enh_profile, l, new_p))
1428
return (new_p, enhancements[0])
1429
1430
# Partial order
1431
# The lookup graph gives a partial order on the BICs (the 3-level graph (i,j)
1432
# implies i > j).
1433
@cached_method
1434
def lies_over(self, i, j):
1435
"""
1436
Determine if (i,j) is a 3-level graph.
1437
1438
INPUT:
1439
1440
i (int): Index of BIC.
1441
1442
j (int): Index of BIC.
1443
1444
OUTPUT:
1445
1446
bool: True if (i,j) is a 3-level graph, False otherwise.
1447
"""
1448
if j in self.DG.bot_to_bic(i).values():
1449
assert i in self.DG.top_to_bic(j).values(),\
1450
"%r is a bottom degeneration of %r, but %r is not a top degeneration of %r!"\
1451
% (j, i, i, j)
1452
return True
1453
else:
1454
assert i not in self.DG.top_to_bic(j).values(),\
1455
"%r is not a bottom degeneration of %r, but %r is a top degeneration of %r!"\
1456
% (j, i, i, j)
1457
return False
1458
1459
# Merging profiles (with respect to lies_over)
1460
@cached_method
1461
def merge_profiles(self, p, q):
1462
"""
1463
Merge profiles with respect to the ordering "lies_over".
1464
1465
Args:
1466
p (iterable): sorted profile
1467
q (iterable): sorted profile
1468
1469
Returns:
1470
tuple: sorted profile or None if no such sorted profile exists.
1471
1472
EXAMPLES::
1473
1474
sage: from admcycles.diffstrata import *
1475
sage: X=GeneralisedStratum([Signature((4,))])
1476
sage: X.merge_profiles((5,),(5,))
1477
(5,)
1478
"""
1479
# input profiles should be sorted:
1480
assert all(self.lies_over(p[i], p[i + 1]) for i in range(len(p) - 1)),\
1481
"Profile %r not sorted!" % (p,)
1482
assert all(self.lies_over(q[i], q[i + 1]) for i in range(len(q) - 1)),\
1483
"Profile %r not sorted!" % (q,)
1484
new_profile = []
1485
next_p = 0
1486
next_q = 0
1487
while next_p < len(p) and next_q < len(q):
1488
if p[next_p] == q[next_q]:
1489
new_profile.append(p[next_p])
1490
next_p += 1
1491
next_q += 1
1492
else:
1493
if self.lies_over(p[next_p], q[next_q]):
1494
new_profile.append(p[next_p])
1495
next_p += 1
1496
else:
1497
if self.lies_over(q[next_q], p[next_p]):
1498
new_profile.append(q[next_q])
1499
else:
1500
return None
1501
next_q += 1
1502
# pick up rest (one of these is empty!):
1503
new_profile += p[next_p:]
1504
new_profile += q[next_q:]
1505
return tuple(new_profile)
1506
1507
# Better graph lookup:
1508
# Here we should really work with "enhanced dominos", because
1509
# otherwise it's not clear how the list indices of degenerations are related
1510
# to each other.
1511
# Therefore, arguments are:
1512
# * a sorted(!) list of BICs, i.e. an element of the lookup_list
1513
# * a (consistent) choice of components of the involved 3-level graph (i.e.
1514
# enhanced dominos)
1515
# This can consistently produce a graph.
1516
##
1517
# For now, we use the workaround to forcibly only work with sorted profiles
1518
# where the indexing is at least consistent.
1519
###
1520
1521
def lookup_graph(self, bic_list, index=0):
1522
"""
1523
Return the graph associated to an enhanced profile.
1524
1525
Note that starting in SAGE 9.0 profile numbering will change between sessions!
1526
1527
Args:
1528
bic_list (iterable): (sorted!) tuple/list of indices of bics.
1529
index (int, optional): Index in lookup list. Defaults to 0.
1530
1531
Returns:
1532
EmbeddedLevelGraph: graph associated to the enhanced (sorted) profile
1533
(None if empty).
1534
1535
EXAMPLES::
1536
1537
sage: from admcycles.diffstrata import *
1538
sage: X=Stratum((2,))
1539
sage: X.lookup_graph(())
1540
EmbeddedLevelGraph(LG=LevelGraph([2],[[1]],[],{1: 2},[0],True),dmp={1: (0, 0)},dlevels={0: 0})
1541
1542
Note that an enhanced profile needs to be unpacked with *:
1543
1544
sage: X.lookup_graph(*X.enhanced_profiles_of_length(2)[0]) # 'unsafe' (edge ordering may change) # doctest:+SKIP
1545
EmbeddedLevelGraph(LG=LevelGraph([1, 0, 0],[[1], [2, 3, 4], [5, 6, 7]],[(1, 4), (2, 6), (3, 7)],{1: 0, 2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2},[0, -1, -2],True),dmp={5: (0, 0)},dlevels={0: 0, -1: -1, -2: -2})
1546
1547
"""
1548
# this is a bit stupid, but whatever...
1549
if all(self.lies_over(bic_list[i], bic_list[i + 1])
1550
for i in range(len(bic_list) - 1)):
1551
return self.lookup(bic_list)[index]
1552
else:
1553
return None
1554
1555
def lookup(self, bic_list, quiet=True):
1556
"""
1557
Take a profile (i.e. a list of indices of BIC) and return the corresponding
1558
EmbeddedLevelGraphs (i.e. the product of these BICs).
1559
1560
Note that this will be a one element list "most of the time", but
1561
it can happen that this is not irreducible:
1562
1563
This implementation is not dependent on the order (!) (we look in top and
1564
bottom degenerations and clutch...)
1565
1566
However, for caching purposes, it makes sense to use only the sorted profiles...
1567
1568
NOTE THAT IN PYTHON3 PROFILES ARE NO LONGER DETERMINISTIC!!!!!
1569
1570
(they typically change with every python session...)
1571
1572
Args:
1573
bic_list (iterable): list of indices of bics
1574
1575
Returns:
1576
list: The list of EmbeddedLevelGraphs corresponding to the profile.
1577
1578
EXAMPLES::
1579
1580
sage: from admcycles.diffstrata import *
1581
sage: X=GeneralisedStratum([Signature((4,))])
1582
1583
This is independent of the order.
1584
1585
sage: p, _ = X.enhanced_profiles_of_length(2)[0]
1586
sage: assert any(X.lookup(p)[0].is_isomorphic(G) for G in X.lookup((p[1],p[0])))
1587
1588
Note that the profile can be empty or reducible.
1589
1590
"""
1591
if not quiet:
1592
print("Looking up enhanced profiles in %r..." % (bic_list,))
1593
sys.stdout.flush() # MPI computer has congestion issues...
1594
lookup_key = tuple(bic_list)
1595
if not bic_list: # empty
1596
if not quiet:
1597
print("Empty profile, returning smooth_LG. Done.")
1598
sys.stdout.flush()
1599
return [self.smooth_LG]
1600
if len(bic_list) == 1:
1601
if not quiet:
1602
print("BIC, profile irreducible by definition. Done.")
1603
sys.stdout.flush()
1604
return [self.bics[bic_list[0]]]
1605
try:
1606
cached_list = self._lookup[lookup_key]
1607
if not quiet:
1608
print("Using cached lookup. Done.")
1609
sys.stdout.flush()
1610
return cached_list
1611
except KeyError:
1612
bic_list = list(bic_list) # in case we are passed a tuple...
1613
# otherwise, making a copy if we're about to manipulate is also not
1614
# such a bad idea...
1615
i = bic_list.pop() # index in self.bics
1616
B = self.bics[i] # this might build bics (!)
1617
# We split the remainder of bic_list into those coming from
1618
# degenerations of the top component and those from bottom.
1619
# Note that these lists will contain their indices in B.top
1620
# and B.bot, respectively.
1621
# Moreover, they have to be nested in case there are multiple
1622
# components.
1623
top_lists = [[]]
1624
bot_lists = [[]]
1625
for j in bic_list:
1626
if not quiet:
1627
print("Looking at BIC %r:" % j, end=' ')
1628
sys.stdout.flush()
1629
# a bic is either in the image of top_to_bic
1630
# or bot_to_bic.
1631
# If it isn't in any image, the intersection is empty
1632
# and we return None.
1633
# Note that again this might build the maps.
1634
try:
1635
top_bics = self.DG.top_to_bic_inv(i)[j]
1636
if not quiet:
1637
print(
1638
"Adding %r BICs from top component to top_lists..." %
1639
len(top_bics))
1640
sys.stdout.flush()
1641
# if there are several components, we "branch out":
1642
new_top_lists = []
1643
for b in top_bics:
1644
for top_list in top_lists:
1645
new_top_lists.append(top_list + [b])
1646
top_lists = new_top_lists
1647
except KeyError:
1648
try:
1649
bot_bics = self.DG.bot_to_bic_inv(i)[j]
1650
if not quiet:
1651
print(
1652
"Adding %r BICs from bottom component to bot_lists..." %
1653
len(bot_bics))
1654
sys.stdout.flush()
1655
# if there are several components, we "branch out":
1656
new_bot_lists = []
1657
for b in bot_bics:
1658
for bot_list in bot_lists:
1659
new_bot_lists.append(bot_list + [b])
1660
bot_lists = new_bot_lists
1661
except KeyError:
1662
# Intersection empty.
1663
return []
1664
if not quiet:
1665
print("Done building top_lists and bot_lists.")
1666
print(
1667
"This gives us %r profiles in %s and %r profiles in %s that we will now clutch pairwise and recursively." %
1668
(len(top_lists), B.top, len(bot_lists), B.bot))
1669
sys.stdout.flush()
1670
graph_list = [admcycles.diffstrata.stratatautring.clutch(
1671
self,
1672
top,
1673
bot,
1674
B.clutch_dict,
1675
B.emb_top,
1676
B.emb_bot
1677
)
1678
for top_list, bot_list in itertools.product(top_lists, bot_lists)
1679
for top in B.top.lookup(top_list, quiet=quiet)
1680
for bot in B.bot.lookup(bot_list, quiet=quiet)
1681
]
1682
# we might have picked up isomorphic guys (e.g. v-graph)
1683
if not quiet:
1684
print(
1685
"For profile %r in %s, we have thus obtained %r graphs." %
1686
(bic_list, self, len(graph_list)))
1687
print("Sorting these by isomorphism class...", end=' ')
1688
sys.stdout.flush()
1689
rep_list = admcycles.diffstrata.bic.isom_rep(graph_list)
1690
self._lookup[lookup_key] = rep_list
1691
if not quiet:
1692
print("Done. Found %r isomorphism classes." % len(rep_list))
1693
sys.stdout.flush() # MPI computer has congestion issues...
1694
return rep_list
1695
1696
@cached_method
1697
def sub_graph_from_level(self, enh_profile, l,
1698
direction='below', return_split_edges=False):
1699
"""
1700
Extract an EmbeddedLevelGraph from the subgraph of enh_profile that is either
1701
above or below level l.
1702
1703
This is embedded into the top/bottom component of the bic at profile[l-1].
1704
In particular, this is a 'true' sub graph, i.e. the names of the vertices and
1705
legs are the same as in enh_profile.
1706
1707
Note: For l==0 or l==number_of_levels we just return enh_profile.
1708
1709
INPUT:
1710
1711
l (int): (relative) level number.
1712
direction (str, optional): 'above' or 'below'. Defaults to 'below'.
1713
return_split_edges (bool, optional. Defaults to False): also return a tuple
1714
of the edges split across level l.
1715
1716
OUTPUT:
1717
1718
EmbeddedLevelGraph: Subgraph of top/bottom component of the bic at profile[l-1].
1719
1720
If return_split_edges=True: Returns tuple (G,e) where
1721
1722
* G is the EmbeddedLevelGraph
1723
* e is a tuple of edges of enh_profile that connect legs above level
1724
l with those below (i.e. those edges needed for clutching!)
1725
1726
EXAMPLES::
1727
1728
sage: from admcycles.diffstrata import *
1729
sage: X=Stratum((2,))
1730
sage: ep = X.enhanced_profiles_of_length(2)[0]
1731
sage: X.sub_graph_from_level(ep, 1, 'above')
1732
EmbeddedLevelGraph(LG=LevelGraph([1],[[1]],[],{1: 0},[0],True),dmp={1: (0, 0)},dlevels={0: 0})
1733
sage: X.sub_graph_from_level(ep, 1, 'below') # 'unsafe' (edge order might change) # doctest:+SKIP
1734
EmbeddedLevelGraph(LG=LevelGraph([0, 0],[[2, 3, 4], [5, 6, 7]],[(2, 6), (3, 7)],{2: 0, 3: 0, 4: -2, 5: 2, 6: -2, 7: -2},[-1, -2],True),dmp={5: (0, 0), 4: (0, 1)},dlevels={-1: -1, -2: -2})
1735
sage: X.bics[ep[0][0]].top
1736
LevelStratum(sig_list=[Signature((0,))],res_cond=[],leg_dict={1: (0, 0)})
1737
sage: X.bics[ep[0][1]].bot
1738
LevelStratum(sig_list=[Signature((2, -2, -2))],res_cond=[[(0, 1), (0, 2)]],leg_dict={3: (0, 0), 4: (0, 1), 5: (0, 2)})
1739
"""
1740
G = self.lookup_graph(*enh_profile)
1741
if l == 0:
1742
if direction == 'below':
1743
if return_split_edges:
1744
return (G, tuple())
1745
return G
1746
if return_split_edges:
1747
return (None, tuple())
1748
return None
1749
if l == G.number_of_levels:
1750
if direction == 'above':
1751
if return_split_edges:
1752
return (G, tuple())
1753
return G
1754
if return_split_edges:
1755
return (None, tuple())
1756
return None
1757
profile, _i = enh_profile
1758
# The BIC that will give us the level is BIC l-1 in the profile:
1759
bic_number = profile[l - 1]
1760
B = self.bics[bic_number]
1761
# We extract the subgraph from the underlying LevelGraph, so we have
1762
# to work with the internal level numbering:
1763
internal_l = G.LG.internal_level_number(l)
1764
# Actually only three things depend on above/below:
1765
# * The choice of vertices in the subgraph.
1766
# * The choice of level to embed into (top/bottom of B).
1767
# * The new level dictionary (as extracting does not change the levels,
1768
# this just consists of the relevant part of G.dlevels)
1769
# Note that in the 'below' case we consider levels <= l, while in 'above'
1770
# we consider > l (we want to cut level passage l!)
1771
if direction == 'below':
1772
new_vertices = [v for v in range(len(G.LG.genera))
1773
if G.LG.levelofvertex(v) <= internal_l]
1774
# in this case, the level we want to embed into is the bottom of B
1775
L = B.bot
1776
# the levels <= internal_l survive into dlevels
1777
new_dlevels = {k: v for k, v in G.dlevels.items()
1778
if k <= internal_l}
1779
else:
1780
assert direction == 'above'
1781
new_vertices = [v for v in range(len(G.LG.genera))
1782
if G.LG.levelofvertex(v) > internal_l]
1783
# in this case, the level we want to embed into is the top of B
1784
L = B.top
1785
# the levels >= internal_l survive into dlevels
1786
new_dlevels = {k: v for k, v in G.dlevels.items()
1787
if k > internal_l}
1788
vertex_set = set(new_vertices)
1789
new_edges = [e for e in G.LG.edges
1790
if G.LG.vertex(e[0]) in vertex_set and
1791
G.LG.vertex(e[1]) in vertex_set]
1792
new_LG = G.LG.extract(new_vertices, new_edges)
1793
leg_set = set(flatten(new_LG.legs))
1794
# Next, we take the part of dmp that we still need:
1795
# Note that G.dmp maps legs of G to points of X, but we want is a map
1796
# to points of L.
1797
# We get this from the following observation:
1798
# We have
1799
# * L.leg_dict: points of B -> points of L
1800
# * B.dmp_inv: points of X -> points of B
1801
# Therefore the composition gives the desired map
1802
# points of G -> points of L
1803
new_dmp = {k: L.leg_dict[B.dmp_inv[v]]
1804
for k, v in G.dmp.items() if k in leg_set}
1805
# The only thing missing is to add the marked points of the edges
1806
# that we have cut:
1807
# We do this in no particular order, as the clutching information will
1808
# have to be retrieved anyways when actually splitting the graph.
1809
# Note that != is boolean xor (!)
1810
split_edges = [e for e in G.LG.edges
1811
if (e[0] in leg_set) != (e[1] in leg_set)]
1812
split_half_edges = [e[0] if e[0] in leg_set else e[1]
1813
for e in split_edges]
1814
# To place these into new_dmp, we pick an undegeneration map G -> B
1815
# Note that the choice of map *should* not matter, as they should differ
1816
# only by an automorphism of B... (except for psi classes, where we have
1817
# to be careful with xi_on_level!!!)
1818
B_to_G = self.explicit_leg_maps(
1819
((bic_number,), 0), enh_profile, only_one=True)
1820
assert B_to_G # G is actually a degeneration of B!
1821
G_to_B = {v: k for k, v in B_to_G.items()}
1822
# check the points we already placed are consistent:
1823
assert all(L.leg_dict[G_to_B[leg]] == new_dmp[leg] for leg in new_dmp)
1824
while split_half_edges:
1825
leg = split_half_edges.pop()
1826
new_dmp[leg] = L.leg_dict[G_to_B[leg]]
1827
# some more checks:
1828
legs_in_new_edges = set(flatten(new_edges))
1829
marked_points = set(new_dmp.keys())
1830
assert legs_in_new_edges.isdisjoint(marked_points)
1831
assert leg_set == (legs_in_new_edges | marked_points)
1832
sub_graph = admcycles.diffstrata.embeddedlevelgraph.EmbeddedLevelGraph(
1833
L, new_LG, new_dmp, new_dlevels)
1834
if return_split_edges:
1835
return (sub_graph, tuple(split_edges))
1836
return sub_graph
1837
1838
# @cached_method
1839
def split_graph_at_level(self, enh_profile, l):
1840
"""
1841
Splits enh_profile self into top and bottom component at level l.
1842
1843
(Note that the 'cut' occurs right above level l, i.e. to get the top level
1844
and the rest, l should be 1! (not 0))
1845
1846
The top and bottom components are EmbeddedLevelGraphs, embedded into
1847
top and bottom of the corresponding BIC (obtained via sub_graph_from_level).
1848
1849
The result is made so that it can be fed into clutch.
1850
1851
Args:
1852
enh_profile (tuple): enhanced profile.
1853
l (int): (relative) level of enh_profile.
1854
1855
Returns:
1856
dict: dictionary consising of
1857
* X: GeneralisedStratum self.X
1858
* top: EmbeddedLevelGraph: top component
1859
* bottom: EmbeddedLevelGraph: bottom component
1860
* clutch_dict: clutching dictionary mapping ex-half-edges on
1861
top to their partners on bottom (both as points in the
1862
respective strata via dmp!)
1863
* emb_dict_top: a dictionary embedding top into the stratum of self
1864
* emb_dict_bot: a dictionary embedding bot into the stratum of self
1865
* leg_dict: a dictionary legs of enh_profile -> legs of top/bottom
1866
1867
Note that clutch_dict, emb_top and emb_bot are dictionaries between
1868
points of strata, i.e. after applying dmp to the points!
1869
1870
EXAMPLES::
1871
1872
In particular, we can use this to "glue" the BICs of 10^top into (10,9,6) and
1873
obtain all components of the profile.
1874
1875
"""
1876
# Split the graph into top and bottom components at level l:
1877
top_graph, se_top = self.sub_graph_from_level(
1878
enh_profile, l, direction='above', return_split_edges=True)
1879
bot_graph, se_bot = self.sub_graph_from_level(
1880
enh_profile, l, direction='below', return_split_edges=True)
1881
assert se_top == se_bot
1882
split_edges = se_top
1883
# We construct the clutching info by splitting the BIC that corresponds
1884
# to level l:
1885
p, _i = enh_profile
1886
# TODO: edge cases
1887
B = self.bics[p[l - 1]]
1888
clutching_info = B.split()
1889
# we simply replace the top and bottom components of B by our graphs:
1890
assert clutching_info['top'] == top_graph.X == B.top
1891
clutching_info['top'] = top_graph
1892
assert clutching_info['bottom'] == bot_graph.X == B.bot
1893
clutching_info['bottom'] = bot_graph
1894
# the clutch_dict has to be replaced by the split_edges:
1895
# Note that these are currently edges of enh_profile, so they need to be
1896
# translated to points on the corresponding stratum via the embedding
1897
# of top_graph/bot_graph:
1898
# WARNING: We use here (once again) implicitly that e[0] is above e[1]!
1899
clutching_info['clutch_dict'] = {
1900
top_graph.dmp[e[0]]: bot_graph.dmp[e[1]] for e in split_edges}
1901
return clutching_info
1902
1903
# @cached_method
1904
def doublesplit(self, enh_profile):
1905
"""
1906
Splits embedded 3-level graph into top, middle and bottom component, along with
1907
all the information required (by clutch) to reconstruct self.
1908
1909
We return a dictionary so that the result can be fed into clutch (naming of
1910
optional arguments...)
1911
1912
This is mainly a technical backend for doublesplit_graph_before_and_after_level.
1913
1914
Note that in contrast to EmbeddedLevelGraph.split, we want to feed a length-2-profile
1915
so that we can really split into the top and bottom of the associated BICs (the only
1916
strata we can control!)
1917
1918
This method is mainly intended for being fed into clutch.
1919
1920
Raises a ValueError if self is not a 3-level graph.
1921
1922
INPUT:
1923
1924
enh_profile (tuple): enhanced profile.
1925
1926
Returns:
1927
1928
A dictionary consisting of
1929
1930
* X: GeneralisedStratum self,
1931
* top: LevelStratum top level of top BIC,
1932
* bottom: LevelStratum bottom level of bottom BIC,
1933
* middle: LevelStratum level -1 of enh_profile,
1934
* emb_dict_top: dict: points of top stratum -> points of X,
1935
* emb_dict_bot: dict: points of bottom stratum -> points of X,
1936
* emb_dict_mid: dict: points of middle stratum -> points of X,
1937
* clutch_dict: dict: points of top stratum -> points of middle stratum,
1938
* clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,
1939
* clutch_dict_long: dict: points of top stratum -> points of bottom stratum.
1940
"""
1941
p, i = enh_profile
1942
if not len(p) == 2:
1943
raise ValueError("Error: Not a 3-level graph! %r" % self)
1944
G = self.lookup_graph(p, i)
1945
# Here it is important that we pick top and bot of the corresponding BICs and identify them with
1946
# level(0) and level(2) of G (as these might not be the same (e.g.
1947
# switched components!)!)
1948
top = self.bics[p[0]].top
1949
middle = G.level(1)
1950
bottom = self.bics[p[1]].bot
1951
# To construct the embedding dictionaries, we have to identify legs of G
1952
# with (stratum) points of top/middle/bottom as keys and points on self as
1953
# values.
1954
#
1955
# The values (points on self) are given by G.dmp.
1956
#
1957
# The keys for middle are given via leg_dict.
1958
#
1959
# For top and bottom, we have to first fix a map from G to
1960
# p[0] and p[1] and then combine self.dmp with the leg_dicts of the LevelStrata.
1961
# It *shouldn't* matter, which undegeneration we take:
1962
top_to_G = self.explicit_leg_maps(
1963
((p[0],), 0), enh_profile, only_one=True)
1964
G_to_top = {v: k for k, v in top_to_G.items()}
1965
bot_to_G = self.explicit_leg_maps(
1966
((p[1],), 0), enh_profile, only_one=True)
1967
G_to_bot = {v: k for k, v in bot_to_G.items()}
1968
# More precisely: We now have the following maps (e.g. for top):
1969
#
1970
# G_to_top: points in G -> points in p[0]
1971
# top.leg_dict: points in p[0] -> stratum points of top
1972
#
1973
# and
1974
#
1975
# top.leg_dict_inv: stratum points of top -> points in p[0]
1976
# top_to_G: points in p[0] -> points in G
1977
# G.dmp: points in G -> stratum points on self
1978
#
1979
# i.e. emb_top is the composition of the inverse of the leg_dict
1980
# of top, i.e. top.stratum_number, with top_to_G and G.dmp
1981
# (giving a map from the points of top to the points of self)
1982
# and the same for middle and bottom.
1983
#
1984
# We implement this by iterating over the marked points of G on top level,
1985
# which are exactly the keys of G.dmp that are on top level.
1986
#
1987
# For this, we have to compose with G_to_top and top.leg_dict again.
1988
#
1989
# Note that we make extra sure that we didn't mess up the level numbering by
1990
# using the relative level numbering (where the top level is guaranteed to be 0,
1991
# the middle is 1 and the bottom level is 2 (positive!)).
1992
emb_dict_top = {top.leg_dict[G_to_top[l]]: G.dmp[l]
1993
for l in iter(G.dmp)
1994
if G.LG.level_number(G.LG.levelofleg(l)) == 0}
1995
emb_dict_mid = {middle.leg_dict[l]: G.dmp[l]
1996
for l in iter(G.dmp)
1997
if G.LG.level_number(G.LG.levelofleg(l)) == 1}
1998
emb_dict_bot = {bottom.leg_dict[G_to_bot[l]]: G.dmp[l]
1999
for l in iter(G.dmp)
2000
if G.LG.level_number(G.LG.levelofleg(l)) == 2}
2001
# Because this is a 3-level graph, all edges of self are cut in this process
2002
# and this gives us exactly the dictionary we must remember:
2003
# Note however, that we have to check if the edge connects top - middle, middle - bottom
2004
# or top - bottom.
2005
# Note that all these dictionaries map points of GeneralisedStrata to each
2006
# other so we must take the corresponding stratum_number!
2007
clutch_dict = {}
2008
clutch_dict_lower = {}
2009
clutch_dict_long = {}
2010
# If the edges are not sorted with e[0] above e[1], we complain.
2011
for e in G.LG.edges:
2012
if G.LG.level_number(G.LG.levelofleg(e[0])) == 0:
2013
if G.LG.level_number(G.LG.levelofleg(e[1])) == 1:
2014
clutch_dict[top.stratum_number(
2015
G_to_top[e[0]])] = middle.stratum_number(e[1])
2016
else:
2017
assert G.LG.level_number(G.LG.levelofleg(e[1])) == 2
2018
clutch_dict_long[top.stratum_number(
2019
G_to_top[e[0]])] = bottom.stratum_number(G_to_bot[e[1]])
2020
else:
2021
assert G.LG.level_number(G.LG.levelofleg(e[0])) == 1
2022
assert G.LG.level_number(G.LG.levelofleg(e[1])) == 2
2023
clutch_dict_lower[middle.stratum_number(
2024
e[0])] = bottom.stratum_number(G_to_bot[e[1]])
2025
return {
2026
'X': self,
2027
'top': top,
2028
'bottom': bottom,
2029
'middle': middle,
2030
'emb_dict_top': emb_dict_top,
2031
'emb_dict_mid': emb_dict_mid,
2032
'emb_dict_bot': emb_dict_bot,
2033
'clutch_dict': clutch_dict,
2034
'clutch_dict_lower': clutch_dict_lower,
2035
'clutch_dict_long': clutch_dict_long}
2036
2037
@cached_method
2038
def three_level_profile_for_level(self, enh_profile, l):
2039
"""
2040
Find the 3-level graph that has level l of enh_profile as its middle level.
2041
2042
A RuntimeError is raised if no unique (or no) 3-level graph is found.
2043
2044
INPUT:
2045
2046
enh_profile (tuple): enhanced profile
2047
l (int): (relative) level number
2048
2049
OUTPUT:
2050
2051
tuple: enhanced profile of the 3-level graph.
2052
"""
2053
profile, _ = enh_profile
2054
three_level_profile = (profile[l - 1], profile[l])
2055
# in case this is reducible, we have to find the correct enhanced
2056
# profile:
2057
possible_enhancements = len(self.lookup(three_level_profile))
2058
assert possible_enhancements > 0, "No 3-level graph for subprofile %r of %r found!" % (
2059
three_level_profile, profile)
2060
enhancements = []
2061
for i in range(possible_enhancements):
2062
if self.is_degeneration(enh_profile, (three_level_profile, i)):
2063
enhancements.append(i)
2064
if len(enhancements) != 1:
2065
raise RuntimeError(
2066
"No unique 3-level undegeneration in %r around level %r! %r" %
2067
(three_level_profile, l, enhancements))
2068
return (three_level_profile, enhancements[0])
2069
2070
# @cached_method
2071
def doublesplit_graph_before_and_after_level(self, enh_profile, l):
2072
"""
2073
Split the graph enh_profile directly above and below level l.
2074
2075
This can be used for gluing an arbitrary degeneration of level l into enh_profile.
2076
2077
The result is made so that it can be fed into clutch.
2078
2079
To ensure compatibility with top/bot/middle_to_bic when gluing, we have
2080
to make sure that everything is embedded into the "correct" generalised strata.
2081
2082
We denote the 3-level graph around level l by H.
2083
2084
Then the top part will be embedded into the top of the top BIC of H,
2085
the bottom part will be embedded into the bot of the bottom BIC of H
2086
and the middle will be the middle level of H.
2087
2088
For a 3-level graph is (almost) equivalent to doublesplit(), the only difference
2089
being that here we return the 0-level graph for each level.
2090
2091
Args:
2092
enh_profile (tuple): enhanced profile.
2093
l (int): (relative) level of enh_profile.
2094
2095
Raises:
2096
ValueError: Raised if l is 0 or lowest level.
2097
RuntimeError: Raised if we don't find a unique 3-level graph around l.
2098
2099
Returns:
2100
dict: A dictionary consisting of:
2101
X: GeneralisedStratum self.X,
2102
top: LevelStratum top level of top BIC of H,
2103
bottom: LevelStratum bottom level of bottom BIC of H,
2104
middle: LevelStratum middle level of H,
2105
emb_dict_top: dict: points of top stratum -> points of X,
2106
emb_dict_bot: dict: points of bottom stratum -> points of X,
2107
emb_dict_mid: dict: points of middle stratum -> points of X,
2108
clutch_dict: dict: points of top stratum -> points of middle stratum,
2109
clutch_dict_lower: dict: points of middle stratum -> points of bottom stratum,
2110
clutch_dict_long: dict: points of top stratum -> points of bottom stratum.
2111
2112
EXAMPLES::
2113
2114
sage: from admcycles.diffstrata import *
2115
sage: X=GeneralisedStratum([Signature((4,))])
2116
sage: assert all(clutch(**X.doublesplit_graph_before_and_after_level(ep,l)).is_isomorphic(X.lookup_graph(*ep)) for levels in range(3,X.dim()+1) for ep in X.enhanced_profiles_of_length(levels-1) for l in range(1,levels-1))
2117
sage: X=GeneralisedStratum([Signature((2,2,-2))])
2118
sage: assert all(clutch(**X.doublesplit_graph_before_and_after_level(ep,l)).is_isomorphic(X.lookup_graph(*ep)) for levels in range(3,X.dim()+2) for ep in X.enhanced_profiles_of_length(levels-1) for l in range(1,levels-1)) # long time
2119
"""
2120
p, i = enh_profile
2121
if l == 0 or l == len(p) + 1:
2122
raise ValueError("Doublesplit must occur at 'inner' level! %r" % l)
2123
G = self.lookup_graph(p, i)
2124
# Split the graph into top and bottom components around level l:
2125
top_graph, se_top = self.sub_graph_from_level(
2126
enh_profile, l, direction='above', return_split_edges=True)
2127
bot_graph, se_bot = self.sub_graph_from_level(
2128
enh_profile, l + 1, direction='below', return_split_edges=True)
2129
# We construct the clutching info by splitting the 3-level graph around l
2130
# Note that the middle level is really the same as that of enh_profile (that's
2131
# why we have to care about components of the profile here), but the leg
2132
# numbering might be different, so we still have to work with an
2133
# undegeneration map:
2134
t_l_enh_profile = self.three_level_profile_for_level(enh_profile, l)
2135
clutching_info = self.doublesplit(t_l_enh_profile)
2136
assert top_graph.X == clutching_info['top']
2137
assert bot_graph.X == clutching_info['bottom']
2138
L = clutching_info['middle']
2139
assert L == self.lookup_graph(*t_l_enh_profile).level(1)
2140
# we simply replace the top and bottom components of B by our graphs:
2141
clutching_info['top'] = top_graph
2142
clutching_info['bottom'] = bot_graph
2143
# Now we have to match up the edges:
2144
# Note that se_top consists of the edges connecting top_graph to any vertex
2145
# on or below level l
2146
# We therefore start by distinguishing those edges ending on level l from the others
2147
# (long edges):
2148
# WARNING: We use here (once again) implicitly that e[0] is above e[1]!
2149
top_to_l = []
2150
top_to_bot = []
2151
for e in se_top:
2152
if G.LG.level_number(G.LG.levelofleg(e[1])) == l:
2153
top_to_l.append(e)
2154
else:
2155
top_to_bot.append(e)
2156
# the same for se_bot:
2157
bot_to_l = []
2158
bot_to_top = []
2159
for e in se_bot:
2160
if G.LG.level_number(G.LG.levelofleg(e[0])) == l:
2161
bot_to_l.append(e)
2162
else:
2163
bot_to_top.append(e)
2164
assert set(top_to_bot) == set(bot_to_top)
2165
# Translating the edges into points on the strata immediately gives the
2166
# three clutching dictionaries:
2167
# Note that instead of directly using leg_dict for the middle level,
2168
# we first pick an undegeneration map to the 3-level graph and compose
2169
# with (the inverse of) that:
2170
middle_leg_map = self.explicit_leg_maps(
2171
t_l_enh_profile, enh_profile, only_one=True)
2172
ep_to_m = {v: k for k, v in middle_leg_map.items()}
2173
# WARNING: We use here (once again) implicitly that e[0] is above e[1]!
2174
clutching_info['clutch_dict'] = {
2175
top_graph.dmp[e[0]]: L.leg_dict[ep_to_m[e[1]]] for e in top_to_l}
2176
clutching_info['clutch_dict_lower'] = {
2177
L.leg_dict[ep_to_m[e[0]]]: bot_graph.dmp[e[1]] for e in bot_to_l}
2178
clutching_info['clutch_dict_long'] = {
2179
top_graph.dmp[e[0]]: bot_graph.dmp[e[1]] for e in top_to_bot}
2180
return clutching_info
2181
2182
# @cached_method
2183
def splitting_info_at_level(self, enh_profile, l):
2184
"""
2185
Retrieve the splitting and embedding dictionaries for splitting at level l,
2186
as well as the level in 'standard form', i.e. as either:
2187
2188
* a top of a BIC
2189
* a bot of a BIC
2190
* a middle of a 3-level graph
2191
2192
This is essentially only a frontend for split_graph_at_level and
2193
doublesplit_graph_before_and_after_level and saves us the annoying
2194
case distinction.
2195
2196
This is important, because when we glue we should *always* use the
2197
dmp's of the splitting dictionary, which can (and will) be different
2198
from leg_dict of the level!
2199
2200
INPUT:
2201
2202
enh_profile (tuple): enhanced profile
2203
l (int): (relative) level number
2204
2205
OUTPUT:
2206
2207
tuple: (splitting dict, leg_dict, level) where
2208
splitting dict is the splitting dictionary:
2209
2210
* X: GeneralisedStratum self.X
2211
* top: EmbeddedLevelGraph: top component
2212
* bottom: EmbeddedLevelGraph: bottom component
2213
* clutch_dict: clutching dictionary mapping ex-half-edges on
2214
top to their partners on bottom (both as points in the
2215
respective strata via dmp!)
2216
* emb_dict_top: a dictionary embedding top into the stratum of self
2217
* emb_dict_bot: a dictionary embedding bot into the stratum of self
2218
2219
leg_dict is the dmp at the current level (to be used instead
2220
of leg_dict of G.level(l)!!!)
2221
2222
and level is the 'standardised' LevelStratum at l (as described above).
2223
2224
Note that clutch_dict, emb_top and emb_bot are dictionaries between
2225
points of strata, i.e. after applying dmp to the points!
2226
"""
2227
profile, _ = enh_profile
2228
# For this, we have to distinguish again, if we're gluing into the middle
2229
# (two cuts) or at one end of the profile (1 cut):
2230
if l == 0:
2231
d = self.split_graph_at_level(enh_profile, 1)
2232
assert d['top'].is_isomorphic(d['top'].X.smooth_LG)
2233
return d, d['top'].dmp, d['top'].X
2234
if l == len(profile):
2235
d = self.split_graph_at_level(enh_profile, l)
2236
assert d['bottom'].is_isomorphic(d['bottom'].X.smooth_LG)
2237
return d, d['bottom'].dmp, d['bottom'].X
2238
d = self.doublesplit_graph_before_and_after_level(enh_profile, l)
2239
three_level_profile = self.three_level_profile_for_level(
2240
enh_profile, l)
2241
assert self.lookup_graph(*three_level_profile).level(1) == d['middle']
2242
# for the middle level, we have to use the undegeneration map to
2243
# the 3-level graph:
2244
middle_leg_map = self.explicit_leg_maps(
2245
three_level_profile, enh_profile, only_one=True)
2246
L_to_m = {v: d['middle'].leg_dict[k] for k, v in middle_leg_map.items()
2247
if k in d['middle'].leg_dict}
2248
return d, L_to_m, d['middle']
2249
2250
@cached_method
2251
def enhanced_profiles_of_length(self, l, quiet=True):
2252
"""
2253
A little helper for generating all enhanced profiles in self of a given length.
2254
2255
Note that this generates the *entire* lookup_list first!
2256
For large strata this can take a long time!
2257
2258
Args:
2259
l (int): length (codim) of profiles to be generated.
2260
2261
Returns:
2262
tuple: tuple of enhanced profiles
2263
2264
EXAMPLES::
2265
2266
sage: from admcycles.diffstrata import *
2267
sage: X=Stratum((4,))
2268
sage: len(X.lookup_list[2])
2269
17
2270
sage: len(X.enhanced_profiles_of_length(2))
2271
19
2272
2273
"""
2274
if not quiet:
2275
print('Generating enhanced profiles of length %r...' % l)
2276
sys.stdout.flush()
2277
if l >= len(self.lookup_list):
2278
return tuple()
2279
ep_list = []
2280
for c, p in enumerate(self.lookup_list[l]):
2281
if not quiet:
2282
print('Building all graphs in %r (%r/%r)...' %
2283
(p, c + 1, len(self.lookup_list[l])))
2284
sys.stdout.flush()
2285
# quiet=False gives A LOT of output here...
2286
for i in range(len(self.lookup(p, quiet=True))):
2287
ep_list.append((p, i))
2288
return tuple(ep_list)
2289
2290
#########################################################
2291
# Checks
2292
#########################################################
2293
2294
def check_dims(self, codim=None, quiet=False):
2295
"""
2296
Check if, for each non-horizontal level graph of codimension codim
2297
the dimensions of the levels add up to the dimension of the level graph
2298
(dim of stratum - codim).
2299
2300
If codim is omitted, check the entire stratum.
2301
2302
EXAMPLES::
2303
2304
sage: from admcycles.diffstrata import *
2305
sage: X=GeneralisedStratum([Signature((1,1))])
2306
sage: X.check_dims()
2307
Codimension 0 Graph 0: Level sums ok!
2308
Codimension 1 Graph 0: Level sums ok!
2309
Codimension 1 Graph 1: Level sums ok!
2310
Codimension 1 Graph 2: Level sums ok!
2311
Codimension 1 Graph 3: Level sums ok!
2312
Codimension 2 Graph 0: Level sums ok!
2313
Codimension 2 Graph 1: Level sums ok!
2314
Codimension 2 Graph 2: Level sums ok!
2315
Codimension 2 Graph 3: Level sums ok!
2316
Codimension 3 Graph 0: Level sums ok!
2317
True
2318
2319
sage: X=GeneralisedStratum([Signature((4,))])
2320
sage: X.check_dims(quiet=True)
2321
True
2322
2323
sage: X=GeneralisedStratum([Signature((10,0,-10))])
2324
sage: X.check_dims()
2325
Codimension 0 Graph 0: Level sums ok!
2326
Codimension 1 Graph 0: Level sums ok!
2327
Codimension 1 Graph 1: Level sums ok!
2328
Codimension 1 Graph 2: Level sums ok!
2329
Codimension 1 Graph 3: Level sums ok!
2330
Codimension 1 Graph 4: Level sums ok!
2331
Codimension 1 Graph 5: Level sums ok!
2332
Codimension 1 Graph 6: Level sums ok!
2333
Codimension 1 Graph 7: Level sums ok!
2334
Codimension 1 Graph 8: Level sums ok!
2335
Codimension 1 Graph 9: Level sums ok!
2336
Codimension 1 Graph 10: Level sums ok!
2337
Codimension 1 Graph 11: Level sums ok!
2338
True
2339
2340
sage: X=GeneralisedStratum([Signature((2,2,-2))])
2341
sage: X.check_dims(quiet=True) # long time (3 seconds)
2342
True
2343
"""
2344
return_value = True
2345
if codim is None:
2346
codims = range(self.dim())
2347
else:
2348
codims = [codim]
2349
for c in codims:
2350
for i, emb_g in enumerate(self.all_graphs[c]):
2351
g = emb_g.LG
2352
dimsum = 0
2353
if not quiet:
2354
print("Codimension", c, "Graph", repr(i) + ":", end=" ")
2355
for l in range(g.numberoflevels()):
2356
L = g.stratum_from_level(l)
2357
if L.dim() == -1:
2358
if quiet:
2359
print("Codimension", c, "Graph",
2360
repr(i) + ":", end=" ")
2361
print("Error: Level", l, "is of dimension -1!")
2362
return_value = False
2363
dimsum += L.dim()
2364
if dimsum != self.dim() - c:
2365
if quiet:
2366
print("Codimension", c, "Graph",
2367
repr(i) + ":", end=" ")
2368
print("Error: Level dimensions add up to",
2369
dimsum, "not", self.dim() - c, "!")
2370
return_value = False
2371
else:
2372
if not quiet:
2373
print("Level sums ok!")
2374
return return_value
2375
2376
###########
2377
# Chern class calculation:
2378
def psi(self, leg):
2379
"""
2380
CURRENTLY ONLY ALLOWED FOR CONNECTED STRATA!!!!
2381
2382
The psi class on the open stratum at leg.
2383
2384
Args:
2385
leg (int): leg number (as index of signature, not point of stratum!!!)
2386
2387
Returns:
2388
ELGTautClass: Tautological class associated to psi.
2389
"""
2390
psi = self.additive_generator([tuple(), 0], {leg: 1})
2391
return psi.as_taut()
2392
2393
# @cached_method
2394
def taut_from_graph(self, profile, index=0):
2395
"""
2396
Tautological class from the graph with enhanced profile (profile, index).
2397
2398
INPUT:
2399
2400
profile (iterable): profile
2401
index (int, optional): Index of profile. Defaults to 0.
2402
2403
OUTPUT:
2404
2405
ELGTautClass: Tautological class consisting just of this one graph.
2406
"""
2407
return self.additive_generator((tuple(profile), index)).as_taut()
2408
2409
def ELGsum(self, L):
2410
"""
2411
Sum of tautological classes.
2412
2413
This is generally faster than += (i.e. sum()), because reduce is only called
2414
once at the end and not at every step.
2415
2416
Args:
2417
L (iterable): Iterable of ELGTautClasses on self.
2418
2419
Returns:
2420
ELGTautClass: Sum over input classes.
2421
"""
2422
new_psi_list = []
2423
for T in L:
2424
if T == 0:
2425
continue
2426
new_psi_list.extend(T.psi_list)
2427
return admcycles.diffstrata.elgtautclass.ELGTautClass(
2428
self, new_psi_list)
2429
2430
def pow(self, T, k, amb=None):
2431
"""
2432
Calculate T^k with ambient amb.
2433
2434
Args:
2435
T (ELGTautClass): Tautological class on self.
2436
k (int): positive integer.
2437
amb (tuple, optional): enhanced profile. Defaults to None.
2438
2439
Returns:
2440
ELGTautClass: T^k in CH(amb).
2441
"""
2442
if amb is None:
2443
amb = ((), 0)
2444
ONE = self.ONE
2445
else:
2446
ONE = self.taut_from_graph(*amb)
2447
prod = ONE
2448
for _ in range(k):
2449
prod = self.intersection(prod, T, amb)
2450
return prod
2451
2452
def exp(self, T, amb=None, quiet=True, prod=True, stop=None):
2453
"""
2454
(Formal) exp of a Tautological Class.
2455
2456
This is done (by default) by calculating exp of every AdditiveGenerator
2457
(which is cached) and calculating the product of these.
2458
2459
Alternatively, prod=False computes sums of powers of T.
2460
2461
Args:
2462
T (ELGTautClass): Tautological Class on X.
2463
2464
Returns:
2465
ELGTautClass: Tautological Class on X.
2466
"""
2467
N = self.dim()
2468
if amb is None:
2469
amb = ((), 0)
2470
if not prod:
2471
if not quiet:
2472
print("Calculating exp of %s..." % T)
2473
2474
def _status(i):
2475
# primitive, but whatever
2476
if not quiet:
2477
print("Calculating power %r..." % i)
2478
return 1
2479
return self.ELGsum(
2480
[_status(i) * QQ((1, factorial(i))) * self.pow(T, i, amb)
2481
for i in range(N + 1)])
2482
# Calculate instead product of exp(AG):
2483
e = self.taut_from_graph(*amb)
2484
if not quiet:
2485
print("Calculating exp as product of %r factors..." %
2486
len(T.psi_list), end=' ')
2487
sys.stdout.flush()
2488
for c, AG in T.psi_list:
2489
f = AG.exp(c, amb, stop)
2490
if f == 0 or f == self.ZERO:
2491
return self.ZERO
2492
e = self.intersection(e, f, amb)
2493
if not quiet:
2494
print('Done!')
2495
return e
2496
2497
@cached_method
2498
def exp_bic(self, i):
2499
l = self.bics[i].ell
2500
AG = self.additive_generator(((i,), 0))
2501
return AG.exp(l, amb=None) - self.ONE
2502
2503
def td_contrib(self, l, T, amb=None):
2504
"""
2505
(Formal) td^-1 contribution, i.e. (1-exp(-l*T))/T.
2506
2507
Args:
2508
l (int): weight
2509
T (ELGTautClass): Tautological class on self.
2510
2511
Returns:
2512
ELGTautClass: Tautological class on self.
2513
"""
2514
N = self.dim()
2515
if amb is None:
2516
amb = ((), 0)
2517
return self.ELGsum([QQ(-l)**k / QQ(factorial(k + 1)) *
2518
self.pow(T, k, amb) for k in range(N + 1)])
2519
2520
@property
2521
def xi(self):
2522
"""
2523
xi of self in terms of psi and BICs according to Sauvaget's formula.
2524
2525
Note that we first find an "optimal" leg.
2526
2527
Returns:
2528
ELGTautClass: psi class on smooth stratum + BIC contributions (all
2529
with multiplicities...)
2530
2531
EXAMPLES::
2532
2533
sage: from admcycles.diffstrata import *
2534
sage: X=Stratum((2,))
2535
sage: print(X.xi) # 'unsafe' (order of summands might change) # doctest:+SKIP
2536
Tautological class on Stratum: (2,)
2537
with residue conditions: []
2538
<BLANKLINE>
2539
3 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +
2540
-1 * Graph ((0,), 0) +
2541
-1 * Graph ((1,), 0) +
2542
<BLANKLINE>
2543
"""
2544
try:
2545
return self._xi
2546
except AttributeError:
2547
self._xi = self.xi_with_leg(quiet=True)
2548
return self._xi
2549
2550
@cached_method
2551
def xi_pow(self, n):
2552
"""
2553
Cached method for calculating powers of xi.
2554
2555
Args:
2556
n (int): non-negative integer (exponent)
2557
2558
Returns:
2559
ELGTautClass: xi^n
2560
"""
2561
if n == 0:
2562
return self.ONE
2563
return self.xi * self.xi_pow(n - 1)
2564
2565
@cached_method
2566
def xi_with_leg(self, leg=None, quiet=True, with_leg=False):
2567
"""
2568
xi class of self expressed using Sauvaget's relation (with optionally a choice of leg)
2569
2570
INPUT:
2571
2572
leg (tuple, optional): leg on self, i.e. tuple (i,j) for the j-th element
2573
of the signature of the i-th component. Defaults to None. In this case,
2574
an optimal leg is chosen.
2575
2576
quiet (bool, optional): No output. Defaults to False.
2577
2578
with_leg (bool, optional): Return choice of leg. Defaults to False.
2579
2580
OUTPUT:
2581
2582
ELGTautClass: xi in terms of psi and bics according to Sauvaget.
2583
(ELGTautClass, tuple): if with_leg=True, where tuple is the corresponding
2584
leg on the level i.e. (component, signature index) used.
2585
2586
EXAMPLES:
2587
2588
In the stratum (2,-2) the pole is chosen by default (there is no 'error term')::
2589
2590
sage: from admcycles.diffstrata import *
2591
sage: X=Stratum((2,-2))
2592
sage: print(X.xi)
2593
Tautological class on Stratum: (2, -2)
2594
with residue conditions: []
2595
<BLANKLINE>
2596
-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +
2597
<BLANKLINE>
2598
sage: print(X.xi_with_leg(leg=(0,1)))
2599
Tautological class on Stratum: (2, -2)
2600
with residue conditions: []
2601
<BLANKLINE>
2602
-1 * Psi class 2 with exponent 1 on level 0 * Graph ((), 0) +
2603
<BLANKLINE>
2604
2605
We can specify the zero instead and pick up the extra divisor::
2606
2607
sage: print(X.xi_with_leg(leg=(0,0))) # 'unsafe' (order of summands might change) # doctest:+SKIP
2608
Tautological class on Stratum: (2, -2)
2609
with residue conditions: []
2610
<BLANKLINE>
2611
3 * Psi class 1 with exponent 1 on level 0 * Graph ((), 0) +
2612
-1 * Graph ((0,), 0) +
2613
<BLANKLINE>
2614
"""
2615
if not quiet:
2616
print(
2617
"Applying Sauvaget's relation to express xi for %r..." %
2618
self)
2619
if leg is None:
2620
# choose a "good" leg:
2621
l, k, bot_bic_list = self._choose_leg_for_sauvaget_relation(quiet)
2622
else:
2623
l = leg
2624
k = self._sig_list[l[0]].sig[l[1]]
2625
bot_bic_list = self.bics_with_leg_on_bottom(l)
2626
# find internal leg number on smooth graph corresponding to l:
2627
G = self.lookup_graph(tuple())
2628
internal_leg = G.dmp_inv[l] # leg number on graph
2629
xi = (k + 1) * self.psi(internal_leg)
2630
add_gens = [self.additive_generator([(b,), 0]) for b in bot_bic_list]
2631
self._xi = xi + admcycles.diffstrata.elgtautclass.ELGTautClass(
2632
self, [(-self.bics[bot_bic_list[i]].ell, AG) for i, AG in enumerate(add_gens)])
2633
# self._xi = xi + sum([QQ(1)/QQ(AG.stack_factor)*AG.as_taut() \
2634
# for i, AG in enumerate(add_gens)])
2635
if with_leg:
2636
return (self._xi, l)
2637
else:
2638
return self._xi
2639
2640
def _choose_leg_for_sauvaget_relation(self, quiet=True):
2641
"""
2642
Choose the best leg for Sauvaget's relation, i.e. the one that appears on bottom
2643
level for the fewest BICs.
2644
2645
Returns:
2646
tuple: tuple (leg, order, bic_list) where:
2647
* leg (tuple), as a tuple (number of conn. comp., index of the signature tuple),
2648
* order (int) the order at leg, and
2649
* bic_list (list of int) is a list of indices of self.bics where leg
2650
is on bottom level.
2651
2652
EXAMPLES::
2653
2654
sage: from admcycles.diffstrata import *
2655
sage: X=Stratum((2,-2))
2656
sage: X._choose_leg_for_sauvaget_relation()
2657
((0, 1), -2, [])
2658
2659
In the minimal stratum, we always find all BICS:
2660
2661
sage: X=Stratum((2,))
2662
sage: X._choose_leg_for_sauvaget_relation()
2663
((0, 0), 2, [0, 1])
2664
"""
2665
best_case = len(self.bics)
2666
best_leg = -1
2667
# points of the stratum are best accessed through the embedding of the smooth graph:
2668
# (we sort for better testing...)
2669
leg_list = sorted(list(self.smooth_LG.dmp_inv.keys()),
2670
key=lambda x: x[1])
2671
for l in leg_list:
2672
bot_list = self.bics_with_leg_on_bottom(l)
2673
# none is best we can do:
2674
if not bot_list:
2675
order = self._sig_list[l[0]].sig[l[1]]
2676
if not quiet:
2677
print(
2678
"Choosing leg %r (of order %r) because it never appears on bottom level." %
2679
(l, order))
2680
return (l, order, [])
2681
on_bottom = len(bot_list)
2682
if on_bottom <= best_case:
2683
best_case = on_bottom
2684
best_leg = l
2685
best_bot_list = bot_list[:] # copy!
2686
assert best_leg != -1, "No best leg found for %r!" % self
2687
order = self._sig_list[best_leg[0]].sig[best_leg[1]]
2688
if not quiet:
2689
print(
2690
"Choosing leg %r (of order %r), because it only appears on bottom %r out of %r times." %
2691
(best_leg, order, best_case, len(
2692
self.bics)))
2693
return (best_leg, order, best_bot_list)
2694
2695
def bics_with_leg_on_bottom(self, l):
2696
"""
2697
A list of BICs where l is on bottom level.
2698
2699
Args:
2700
l (tuple): leg on self (i.e. (i,j) for the j-th element of the signature
2701
of the i-th component)
2702
2703
Returns:
2704
list: list of indices self.bics
2705
2706
EXAMPLES::
2707
2708
sage: from admcycles.diffstrata import *
2709
sage: X=GeneralisedStratum([Signature((2,))])
2710
sage: X.bics_with_leg_on_bottom((0,0))
2711
[0, 1]
2712
"""
2713
bot_list = []
2714
# the corresponding point on each EmbeddedLevelGraph is leg
2715
for i, B in enumerate(self.bics):
2716
# reminder: l is leg on stratum, i.e. (i,j)
2717
# dmp_inv maps this to a leg on a graph (integer)
2718
leg = B.dmp_inv[l]
2719
leg_level = B.dlevels[B.LG.levelofleg(leg)]
2720
assert leg_level in [
2721
0, -1], "Leg %r of BIC %r is not on level 0 or -1!" % (leg, B)
2722
if leg_level == -1:
2723
bot_list.append(i)
2724
return bot_list
2725
2726
@cached_method
2727
def xi_at_level(self, l, enh_profile, leg=None, quiet=True):
2728
"""
2729
Pullback of xi on level l to enh_profile.
2730
2731
This corresponds to xi_Gamma^[i] in the paper.
2732
2733
RuntimeError raised if classes produced by xi on the level have
2734
unexpected codimension. ValueError is raised if the leg provided is not
2735
found on the level.
2736
2737
INPUT:
2738
2739
l (int): level number (0,...,codim)
2740
enh_profile (tuple): enhanced profile
2741
leg (int, optional): leg (as a leg of enh_profile!!!), to be used
2742
in Sauvaget's relation. Defaults to None, i.e. optimal choice.
2743
2744
OUTPUT:
2745
2746
ELGTautClass: tautological class consisting of psi classes on
2747
enh_profile and graphs with oner more level.
2748
2749
EXAMPLES:
2750
2751
Compare multiplication with xi to xi_at_level (for top-degree)::
2752
2753
sage: from admcycles.diffstrata import *
2754
sage: X=Stratum((2,-2,0))
2755
sage: assert all(X.xi_at_level(0, ((i,),0)) == X.xi*X.taut_from_graph((i,)) for i in range(len(X.bics)))
2756
"""
2757
if enh_profile == ((), 0):
2758
assert l == 0
2759
if leg:
2760
level_leg = self.smooth_LG.dmp[leg]
2761
return self.xi_with_leg(level_leg)
2762
return self.xi
2763
# we need to use splitting info instead of direct level extraction,
2764
# because the embeddings might differ by an automorphism!
2765
d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)
2766
inv_leg_dict = {v: k for k, v in leg_dict.items()}
2767
assert set(leg_dict.values()) == set(L.leg_dict.values())
2768
if leg is None:
2769
l_xi, level_leg = L.xi_with_leg(with_leg=True, quiet=quiet)
2770
else:
2771
if not (leg in leg_dict):
2772
raise ValueError('Leg %r is not on level %r of %r!' %
2773
(leg, l, enh_profile))
2774
level_leg = leg_dict[leg]
2775
l_xi = L.xi_with_leg(level_leg, quiet=quiet)
2776
taut_list = []
2777
if l_xi == 0:
2778
return self.ZERO
2779
for c, AG in l_xi.psi_list:
2780
if AG.codim == 0:
2781
# psi class on L:
2782
new_leg_dict = {}
2783
for AGleg in AG.leg_dict:
2784
leg_on_G = inv_leg_dict[L.smooth_LG.dmp[AGleg]]
2785
new_leg_dict[leg_on_G] = AG.leg_dict[AGleg]
2786
next_taut = (c, self.additive_generator(
2787
enh_profile, leg_dict=new_leg_dict))
2788
elif AG.codim == 1:
2789
coeff, glued_AG = self.glue_AG_at_level(AG, enh_profile, l)
2790
next_taut = (c * coeff, glued_AG)
2791
else:
2792
raise RuntimeError(
2793
"Classes in xi should all be of codim 0 or 1! %s" % l_xi)
2794
taut_list.append(next_taut)
2795
return admcycles.diffstrata.elgtautclass.ELGTautClass(self, taut_list)
2796
2797
@cached_method
2798
def glue_AG_at_level(self, AG, enh_profile, l):
2799
"""
2800
Glue an AdditiveGenerator into level l of enh_profile.
2801
2802
Note that AG must be an AdditiveGenerator on the level obtained via
2803
self.splitting_info_at_level!
2804
2805
Currently this is only implemented for graphs (and only really tested
2806
for BICs!!!)
2807
2808
TODO: Test for AGs that are not BICs and psi classes.
2809
2810
Args:
2811
AG (AdditiveGenerator): AdditiveGenerator on level
2812
enh_profile (tuple): enhanced profile of self.
2813
l (int): level number of enh_profile.
2814
2815
Raises:
2816
RuntimeError: raised if the new profile is empty.
2817
2818
Returns:
2819
tuple: A tuple consisting of the stackfactor (QQ) and the
2820
AdditiveGenerator of the glued graph.
2821
"""
2822
# TODO: Check if longer profiles work + psis!
2823
#
2824
# First, we figure out the profile of the new graph of self.
2825
# For this, we must translate the profile (inside L) of the AG
2826
# into an extended profile (of self) as a degeneration of enh_profile:
2827
profile, _comp = enh_profile
2828
AGprofile, AGcomp = AG.enh_profile
2829
# We start by deciding where something must be inserted into enh_profile:
2830
#
2831
# We observe that level l is either:
2832
# * B^top of the first BIC in profile (level 0),
2833
# * B^bot of the last BIC in profile (lowest level), or
2834
# * the middle of the 3-level graph (profile[l-1],profile[l]).
2835
#
2836
# There is also the "degenerate case" of an empty profile that
2837
# we should exclude first:
2838
if len(profile) == 0:
2839
assert l == 0
2840
# level stratum == stratum
2841
# stack_factor = QQ(AG.stack_factor)
2842
return (1, self.additive_generator((AGprofile, AGcomp)))
2843
elif l == 0:
2844
new_bics = [self.DG.top_to_bic(
2845
profile[l])[bic_index] for bic_index in AGprofile]
2846
elif l == len(profile):
2847
new_bics = [self.DG.bot_to_bic(
2848
profile[l - 1])[bic_index] for bic_index in AGprofile]
2849
else: # we are in the middle of the corresponding 3-level graph:
2850
three_level_profile, enhancement = self.three_level_profile_for_level(
2851
enh_profile, l)
2852
new_bics = [self.DG.middle_to_bic((three_level_profile, enhancement))[
2853
bic_index] for bic_index in AGprofile]
2854
p = list(profile)
2855
p = tuple(p[:l] + new_bics + p[l:])
2856
# Now we know the profile, we have to figure out, which component
2857
# we're on.
2858
# For this, we split the enh_profile apart, replace one part by the BIC and
2859
# and glue it back together again.
2860
comp_list = []
2861
assert len(self.lookup(p)) > 0, "Error: Glued into empty profile %r" % p
2862
# The splitting information and the level in 'standard form' (i.e. one
2863
# of the three above possibilities), is given by
2864
# splitting_info_at_level:
2865
d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)
2866
if AG._X is not L:
2867
print(
2868
"Warning! Additive Generator should live on level %r of %r! I hope you know what you're doing...." %
2869
(l, enh_profile))
2870
# We first build the "big" graph, i.e. glue in the AG.
2871
# For this, we have to distinguish again, if we're gluing into the middle
2872
# (two cuts) or at one end of the profile (1 cut):
2873
if l == 0:
2874
assert d['top'].X is L
2875
# we glue into top:
2876
d['top'] = d['top'].X.lookup_graph(*AG.enh_profile)
2877
elif l == len(profile):
2878
assert d['bottom'].X is L
2879
# we glue into bottom:
2880
d['bottom'] = d['bottom'].X.lookup_graph(*AG.enh_profile)
2881
else:
2882
assert d['middle'] is L
2883
# we glue into middle:
2884
d['middle'] = d['middle'].lookup_graph(*AG.enh_profile)
2885
glued_graph = admcycles.diffstrata.stratatautring.clutch(**d)
2886
# Now we check the components of p for glued_graph:
2887
for i, H in enumerate(self.lookup(p)):
2888
if glued_graph.is_isomorphic(H):
2889
comp_list.append(i)
2890
if len(comp_list) != 1:
2891
raise RuntimeError("%r is not a unique degeneration of %r! %r" % (
2892
p, enh_profile, comp_list))
2893
i = comp_list[0]
2894
glued_AG = self.additive_generator((p, i))
2895
GAG = self.additive_generator(enh_profile)
2896
stack_factor = 1
2897
for i in range(len(AGprofile)):
2898
stack_factor *= QQ(self.bics[new_bics[i]].ell) / \
2899
QQ(L.bics[AGprofile[i]].ell)
2900
stack_factor *= QQ(len(glued_graph.automorphisms)) / \
2901
QQ(len(AG._G.automorphisms) * len(GAG._G.automorphisms))
2902
return (stack_factor, glued_AG)
2903
2904
def calL(self, enh_profile=None, l=0):
2905
"""
2906
The error term of the normal bundle on level l of enh_profile * -ll
2907
(pulled back to enh_profile)
2908
2909
Args:
2910
enh_profile (tuple, optional): enhanced profile. Defaults to None.
2911
l (int, optional): level. Defaults to 0.
2912
2913
Returns:
2914
ELGTautClass: Tautological class on self
2915
"""
2916
result = []
2917
if enh_profile is None or enh_profile == ((), 0):
2918
for i, B in enumerate(self.bics):
2919
ll = self.bics[i].ell
2920
result.append(ll * self.taut_from_graph((i,)))
2921
else:
2922
# Morally, L = G.level(squished_level)
2923
# but we have to use splitting_info_at_level to glue in safely!
2924
d, leg_dict, L = self.splitting_info_at_level(enh_profile, l)
2925
for i, B in enumerate(L.bics):
2926
BAG = L.additive_generator(((i,), 0))
2927
sf, glued_AG = self.glue_AG_at_level(BAG, enh_profile, l)
2928
coeff = QQ(sf * B.ell)
2929
result.append(coeff * glued_AG.as_taut())
2930
if not result:
2931
return self.ZERO
2932
return self.ELGsum(result)
2933
2934
##############################################################
2935
# SEC 9 FORMULAS #
2936
##############################################################
2937
# The following formulas check various identities used in #
2938
# and around sec 9 of the paper. They also serve as examples #
2939
# for the methods introduced above. #
2940
##############################################################
2941
2942
@property
2943
def c1_E(self):
2944
"""
2945
The first chern class of Omega^1(log) (Thm 1.1).
2946
2947
OUTPUT:
2948
2949
ELGTautClass: c_1(E) according to Thm 1.1.
2950
"""
2951
N = self.dim() + 1
2952
c1E = [N * self.xi]
2953
for i, B in enumerate(self.bics):
2954
Ntop = B.top.dim() + 1
2955
l = B.ell
2956
c1E.append(((N - Ntop) * l) * self.taut_from_graph((i,)))
2957
return self.ELGsum(c1E)
2958
2959
@property
2960
def c2_E(self):
2961
"""
2962
A direct formula for the second Chern class.
2963
2964
Returns:
2965
ELGTautClass: c_2 of the Tangent bundle of self.
2966
"""
2967
N = QQ(self.dim() + 1)
2968
c2E = [N * (N - 1) / QQ(2) * (self.xi_pow(2))]
2969
for i, B in enumerate(self.bics):
2970
Ntop = B.top.dim() + 1
2971
xitop = self.xi_at_level(0, ((i,), 0))
2972
xibot = self.xi_at_level(1, ((i,), 0))
2973
l = QQ(B.ell)
2974
c2E.append(l / 2 * ((N * (N - 1) - Ntop * (Ntop - 1)) * xitop +
2975
((N - Ntop)**2 + Ntop - N) * xibot))
2976
for ep in self.enhanced_profiles_of_length(2):
2977
p, _ = ep
2978
delta0 = self.bics[p[0]]
2979
delta1 = self.bics[p[1]]
2980
Nd0 = delta0.top.dim() + 1
2981
Nd1 = delta1.top.dim() + 1
2982
ld0 = QQ(delta0.ell)
2983
ld1 = QQ(delta1.ell)
2984
factor = QQ(1) / QQ(2) * ld0 * ld1 * \
2985
(N * (N - 2 * Nd0) - Nd1 * (Nd1 - 2 * Nd0) - N + Nd1)
2986
c2E.append(factor * self.taut_from_graph(*ep))
2987
return self.ELGsum(c2E)
2988
2989
@cached_method
2990
def ch1_pow(self, n):
2991
"""
2992
A direct formula for powers of ch_1
2993
2994
Args:
2995
n (int): exponent
2996
2997
Returns:
2998
ELGTautClass: ch_1(T)^n
2999
"""
3000
N = QQ(self.dim() + 1)
3001
chpow = [QQ(N**n) / QQ(factorial(n)) * self.xi_pow(n)]
3002
for L in range(1, n + 1):
3003
summand = []
3004
for ep in self.enhanced_profiles_of_length(L):
3005
p, _ = ep
3006
delta = [self.bics[b] for b in p]
3007
ld = [B.ell for B in delta]
3008
Nd = [B.top.dim() + 1 for B in delta]
3009
exi = self.exp(N * self.xi_at_level(0, ep), amb=ep)
3010
factor = 1
3011
td_prod = self.taut_from_graph(*ep)
3012
for i in range(L):
3013
factor *= (N - Nd[i]) * ld[i]
3014
td_prod = self.intersection(td_prod,
3015
self.td_contrib(-ld[i] * (N - Nd[i]),
3016
self.cnb(ep, ep, self.squish(ep, i)), ep),
3017
ep)
3018
prod = self.intersection(exi, td_prod, ep)
3019
summand.append(factor * prod.degree(n))
3020
chpow.append(self.ELGsum(summand))
3021
return factorial(n) * self.ELGsum(chpow)
3022
3023
@property
3024
def ch2_E(self):
3025
"""
3026
A direct formula for ch_2.
3027
3028
Returns:
3029
ELGTautClass: ch_2
3030
"""
3031
N = QQ(self.dim() + 1)
3032
ch2E = [N / QQ(2) * (self.xi_pow(2))]
3033
for i, B in enumerate(self.bics):
3034
Ntop = B.top.dim() + 1
3035
xitop = self.xi_at_level(0, ((i,), 0))
3036
xibot = self.xi_at_level(1, ((i,), 0))
3037
l = QQ(B.ell)
3038
ch2E.append(l / 2 * ((N - Ntop) * (xitop + xibot)))
3039
for ep in self.enhanced_profiles_of_length(2):
3040
p, _ = ep
3041
delta0 = self.bics[p[0]]
3042
delta1 = self.bics[p[1]]
3043
Nd1 = delta1.top.dim() + 1
3044
ld0 = QQ(delta0.ell)
3045
ld1 = QQ(delta1.ell)
3046
factor = QQ(1) / QQ(2) * ld0 * ld1 * (N - Nd1)
3047
ch2E.append(factor * self.taut_from_graph(*ep))
3048
return self.ELGsum(ch2E)
3049
3050
def ch_E_alt(self, d):
3051
"""
3052
A formula for the Chern character.
3053
3054
Args:
3055
d (int): cut-off degree
3056
3057
Returns:
3058
ELGTautClass: sum of ch_0 to ch_d.
3059
"""
3060
N = QQ(self.dim() + 1)
3061
ch_E = [N / QQ(factorial(d)) * self.xi_pow(d)]
3062
for L in range(1, d + 1):
3063
summand = []
3064
for ep in self.enhanced_profiles_of_length(L):
3065
p, _ = ep
3066
ld = [self.bics[b].ell for b in p]
3067
Nd = self.bics[p[-1]].top.dim() + 1
3068
ld_prod = 1
3069
for l in ld:
3070
ld_prod *= l
3071
factor = ld_prod * (N - Nd)
3072
td_prod = self.ONE
3073
for i in range(L):
3074
td_prod = self.intersection(
3075
td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)
3076
inner_sum = []
3077
for j in range(d - L + 1):
3078
pr = self.intersection(self.pow(self.xi_at_level(
3079
0, ep), j, ep), td_prod.degree(d - j), ep)
3080
inner_sum.append(QQ(1) / QQ(factorial(j)) * pr)
3081
summand.append(factor * self.ELGsum(inner_sum))
3082
ch_E.append(self.ELGsum(summand))
3083
return self.ELGsum(ch_E)
3084
3085
@cached_method
3086
def exp_xi(self, quiet=True):
3087
"""
3088
Calculate exp(xi) using that no powers higher than 2g appear for connected
3089
holomorphic strata.
3090
3091
Args:
3092
quiet (bool, optional): No output. Defaults to True.
3093
3094
Returns:
3095
ELGTautClass: exp(xi)
3096
"""
3097
if not self._polelist and len(self._g) == 1:
3098
stop = 2 * self._g[0]
3099
else:
3100
stop = None
3101
if not quiet:
3102
if stop:
3103
stop_str = stop - 1
3104
else:
3105
stop_str = stop
3106
print('Stoping exp(xi) at degree %r' % stop_str)
3107
return self.exp(self.xi, quiet=quiet, stop=stop)
3108
3109
def xi_at_level_pow(self, level, enh_profile, exponent):
3110
"""
3111
Calculate powers of xi_at_level (using ambient enh_profile).
3112
3113
Note that when working with xi_at_level on enh_profile, multiplication
3114
should always take place in CH(enh_profile), i.e. using intersection
3115
instead of ``*``. This is simplified for powers by this method.
3116
3117
Moreover, by Sauvaget, xi^n = 0 for n >= 2g for connected holomorphic
3118
strata, so we check this before calculating.
3119
3120
INPUT:
3121
3122
level (int): level of enh_profile.
3123
enh_profile (tuple): enhanced profile of self.
3124
exponent (int): exponent
3125
3126
OUTPUT:
3127
3128
ELGTautClass: Pushforward of (xi_{enh_profile}^[l])^n to self.
3129
"""
3130
G = self.lookup_graph(*enh_profile)
3131
L = G.level(level)
3132
if not L._polelist and len(L._g) == 1:
3133
if exponent >= 2 * L._g[0]:
3134
return self.ZERO
3135
if enh_profile == ((), 0):
3136
assert level == 0
3137
return self.xi_pow(exponent)
3138
# ambient!
3139
power = self.taut_from_graph(*enh_profile)
3140
# maybe consecutive squaring is better? Seems that it isn't :/
3141
# xi = self.xi_at_level(level, enh_profile)
3142
# def _rec(x, n):
3143
# if n == 0:
3144
# return self.taut_from_graph(*enh_profile)
3145
# if n == 1:
3146
# return x
3147
# if n % 2 == 0:
3148
# return _rec(self.intersection(x, x, enh_profile), n // 2)
3149
# return self.intersection(x, _rec(self.intersection(x, x, enh_profile), (n - 1) // 2), enh_profile)
3150
# return _rec(xi, exponent)
3151
xi = self.xi_at_level(level, enh_profile)
3152
for _ in range(exponent):
3153
power = self.intersection(power, xi, enh_profile)
3154
return power
3155
3156
@cached_method
3157
def exp_L(self, quiet=True):
3158
"""
3159
exp(calL)
3160
3161
Args:
3162
quiet (bool, optional): No output. Defaults to True.
3163
3164
Returns:
3165
ELGTautClass: exp(calL)
3166
"""
3167
return self.exp(self.calL(), quiet=quiet)
3168
3169
@property
3170
def P_B(self):
3171
"""
3172
The twisted Chern character of self, see sec 9 of the paper.
3173
3174
Returns:
3175
ELGTautClass: class of P_B
3176
"""
3177
# Prop. 9.2
3178
N = QQ(self.dim() + 1)
3179
PB = [N * self.exp_xi() + (-1) * self.ONE]
3180
for L in range(1, N):
3181
inner = []
3182
for enh_profile in self.enhanced_profiles_of_length(L):
3183
p, _ = enh_profile
3184
B = self.bics[p[0]]
3185
Ntop = B.top.dim() + 1
3186
summand = (-1)**L * (Ntop * self.exp_xi() + (-1) * self.ONE)
3187
prod_list = []
3188
for i in range(L):
3189
ll = self.bics[p[i]].ell
3190
squish = self.squish(enh_profile, i)
3191
td_NB = ll * \
3192
self.td_contrib(ll, self.cnb(
3193
enh_profile, enh_profile, squish), enh_profile)
3194
prod_list.append(td_NB)
3195
if prod_list:
3196
prod = prod_list[0]
3197
for f in prod_list[1:]:
3198
# multiply with ambient Gamma (=enh_profile)!
3199
prod = self.intersection(prod, f, enh_profile)
3200
const = prod.degree(0)
3201
prod += (-1) * const
3202
summand *= (prod + const *
3203
self.taut_from_graph(*enh_profile))
3204
inner.append(summand)
3205
PB.append(self.ELGsum(inner))
3206
return self.ELGsum(PB)
3207
3208
def charToPol(self, ch, upto=None, quiet=True):
3209
"""
3210
Newton's identity to recursively translate the Chern character into the
3211
Chern polynomial.
3212
3213
Args:
3214
ch (ELGTautClass): Chern character
3215
upto (int, optional): Calculate polynomial only up to this degree. Defaults to None (full polynomial).
3216
quiet (bool, optional): No output. Defaults to True.
3217
3218
Returns:
3219
list: Chern polynomial as list of ELGTautClasses (indexed by degree)
3220
"""
3221
if not quiet:
3222
print('Starting charToPol...')
3223
C = ch.list_by_degree()
3224
# throw out factorials:
3225
p = [factorial(k) * c for k, c in enumerate(C)]
3226
# calculate recursively using Newton's identity:
3227
E = [self.ONE]
3228
if upto is None:
3229
upto = self.dim()
3230
for k in range(1, upto + 1):
3231
if not quiet:
3232
print('Calculating c_%r...' % k)
3233
ek = []
3234
for i in range(1, k + 1):
3235
ek.append((-1)**(i - 1) * E[k - i] * p[i])
3236
E.append(QQ(1) / QQ(k) * self.ELGsum(ek))
3237
return E
3238
3239
def top_chern_class_alt(self, quiet=True):
3240
"""
3241
Top chern class from Chern polynomial.
3242
3243
Args:
3244
quiet (bool, optional): No output. Defaults to True.
3245
3246
Returns:
3247
ELGTautClass: c_top of the tangent bundle of self.
3248
"""
3249
ch = self.ch_E_fast(quiet=quiet).list_by_degree()
3250
top_c = []
3251
N = self.dim()
3252
for p in partitions(N):
3253
l = sum(p.values())
3254
factor = (-1)**(N - l)
3255
# for r, n in enumerate(p.values()):
3256
# factor *= QQ(factorial(r)**n)/QQ(factorial(n))
3257
ch_prod = self.ONE
3258
for i, n in p.items():
3259
factor *= QQ(factorial(i - 1)**n) / QQ(factorial(n))
3260
if i == 1:
3261
ch_prod *= self.ch1_pow(n)
3262
else:
3263
ch_prod *= ch[i]**n
3264
top_c.append(factor * ch_prod)
3265
return self.ELGsum(top_c)
3266
3267
def top_chern_class_direct(self, quiet=True):
3268
"""
3269
A direct formula for the top Chern class using only xi_at_level.
3270
3271
Args:
3272
quiet (bool, optional): No output. Defaults to True.
3273
3274
Returns:
3275
ELGTautClass: c_top of the Tangent bundle of self.
3276
"""
3277
N = self.dim()
3278
top_c = []
3279
for L in range(N + 1):
3280
if not quiet:
3281
print('Going through %r profiles of length %r...' %
3282
(len(self.enhanced_profiles_of_length(L)), L))
3283
summand = []
3284
for ep in self.enhanced_profiles_of_length(L):
3285
p, _ = ep
3286
ld = [self.bics[b].ell for b in p]
3287
ld_prod = 1
3288
for l in ld:
3289
ld_prod *= l
3290
inner = []
3291
for K in WeightedIntegerVectors(N - L, [1] * (L + 1)):
3292
xi_prod = self.taut_from_graph(*ep)
3293
for i, k in enumerate(K):
3294
xi_prod = self.intersection(
3295
xi_prod, self.xi_at_level_pow(i, ep, k), ep)
3296
inner.append((K[0] + 1) * xi_prod)
3297
summand.append(ld_prod * self.ELGsum(inner))
3298
top_c.append(self.ELGsum(summand))
3299
return self.ELGsum(top_c)
3300
3301
def top_xi_at_level_comparison(self, ep, quiet=False):
3302
"""
3303
Comparison of level-wise computation vs xi_at_level.
3304
3305
Args:
3306
ep (tuple): enhanced profile
3307
quiet (bool, optional): no output. Defaults to False.
3308
3309
Returns:
3310
bool: Should always be True.
3311
3312
EXAMPLES::
3313
3314
sage: from admcycles.diffstrata import *
3315
sage: X=Stratum((2,))
3316
sage: assert all(X.top_xi_at_level_comparison(ep, quiet=True) for l in range(len(X.lookup_list)) for ep in X.enhanced_profiles_of_length(l))
3317
"""
3318
N = self.dim()
3319
p, _ = ep
3320
L = len(p)
3321
ld = [self.bics[b].ell for b in p]
3322
Nvec = [self.bics[b].top.dim() + 1 for b in p]
3323
Nvec.append(N + 1)
3324
ld_prod = 1
3325
for l in ld:
3326
ld_prod *= l
3327
xi_prod = self.xi_at_level_pow(0, ep, Nvec[0] - 1)
3328
for i in range(1, L + 1):
3329
xi_prod = self.intersection(
3330
xi_prod, self.xi_at_level_pow(i, ep, Nvec[i] - Nvec[i - 1] - 1), ep)
3331
xi_at_level_prod = (Nvec[0] * xi_prod).evaluate(quiet=True)
3332
if not quiet:
3333
print("Product of xis at levels: %r" % xi_at_level_prod)
3334
G = self.lookup_graph(*ep)
3335
AG = self.additive_generator(ep)
3336
top_xi_at_level = [
3337
(G.level(i).xi_at_level_pow(
3338
0, ((), 0), G.level(i).dim())).evaluate(
3339
quiet=True) for i in range(
3340
L + 1)]
3341
if not quiet:
3342
print(top_xi_at_level)
3343
prod = Nvec[0]
3344
for x in top_xi_at_level:
3345
prod *= x
3346
tot_prod = AG.stack_factor * prod
3347
if not quiet:
3348
print("Stack factor: %r" % AG.stack_factor)
3349
print("Product: %r" % prod)
3350
print("Total product: %r" % tot_prod)
3351
return tot_prod == xi_at_level_prod
3352
3353
def top_xi_at_level(self, ep, level, quiet=True):
3354
"""
3355
Evaluate the top xi power on a level.
3356
3357
Note that this is _not_ computed on self but on the GeneralisedStratum
3358
corresponding to level l of ep (the result is a number!).
3359
3360
Moreover, all results are cached and the cache is synchronised with
3361
the ``XI_TOPS`` cache.
3362
3363
The key for the cache is L.dict_key (where L is the LevelStratum).
3364
3365
Args:
3366
ep (tuple): enhanced profile
3367
level (int): level number of ep
3368
quiet (bool, optional): No output. Defaults to True.
3369
3370
Returns:
3371
QQ: integral of the top xi power against level l of ep.
3372
3373
EXAMPLES::
3374
3375
sage: from admcycles.diffstrata import *
3376
sage: X=Stratum((2,))
3377
sage: X.top_xi_at_level(((),0), 0)
3378
-1/640
3379
3380
TESTS:
3381
3382
Check that the cache can be deactivated and reactivated::
3383
3384
sage: from admcycles.diffstrata import Stratum
3385
sage: import admcycles.diffstrata.cache
3386
sage: tmp = admcycles.diffstrata.cache.TOP_XIS
3387
sage: admcycles.diffstrata.cache.TOP_XIS = admcycles.diffstrata.cache.FakeCache()
3388
sage: X = Stratum((2,))
3389
sage: X.top_xi_at_level(((),0), 0) is X.top_xi_at_level(((),0), 0)
3390
False
3391
sage: admcycles.diffstrata.cache.TOP_XIS = tmp
3392
sage: X.top_xi_at_level(((),0), 0) is X.top_xi_at_level(((),0), 0)
3393
True
3394
"""
3395
G = self.lookup_graph(*ep)
3396
L = G.level(level)
3397
key = L.dict_key()
3398
from .cache import TOP_XIS
3399
if key not in TOP_XIS:
3400
N = L.dim()
3401
if not quiet:
3402
print('(calc)', end=' ')
3403
sys.stdout.flush()
3404
top_xi = L.xi_at_level_pow(0, ((), 0), N)
3405
answer = TOP_XIS[key] = top_xi.evaluate(quiet=True)
3406
return answer
3407
else:
3408
if not quiet:
3409
print('(cache)', end=' ')
3410
sys.stdout.flush()
3411
return TOP_XIS[key]
3412
3413
def euler_char_immediate_evaluation(self, quiet=True):
3414
"""
3415
Calculate the (Orbifold) Euler characteristic of self by evaluating top xi
3416
powers on levels.
3417
3418
This is (by far) the fastest way of computing Euler characteristics.
3419
3420
Note that only combinatorial information about the degeneration graph
3421
of self is used (enhanced_profiles_of_length(L)) and top_xi_at_level
3422
the values of which are cached and synched with ``TOP_XIS`` cache.
3423
3424
Args:
3425
quiet (bool, optional): No output. Defaults to True.
3426
3427
Returns:
3428
QQ: (Orbifold) Euler characteristic of self.
3429
3430
EXAMPLES::
3431
3432
sage: from admcycles.diffstrata import *
3433
sage: X=Stratum((2,))
3434
sage: X.euler_char_immediate_evaluation()
3435
-1/40
3436
"""
3437
N = self.dim()
3438
ec = 0
3439
for L in range(N + 1):
3440
if not quiet:
3441
total = len(self.enhanced_profiles_of_length(L, quiet=False))
3442
print('Going through %r profiles of length %r...' % (total, L))
3443
for i, ep in enumerate(self.enhanced_profiles_of_length(L)):
3444
if not quiet:
3445
print('%r / %r, %r:' % (i + 1, total, ep), end=' ')
3446
sys.stdout.flush()
3447
p, _ = ep
3448
ld = [self.bics[b].ell for b in p]
3449
if p:
3450
NGammaTop = self.bics[p[0]].top.dim() + 1
3451
else:
3452
NGammaTop = N + 1
3453
ld_prod = 1
3454
for l in ld:
3455
ld_prod *= l
3456
AG = self.additive_generator(ep)
3457
prod = ld_prod * NGammaTop * AG.stack_factor
3458
if not quiet:
3459
print("Calculating xi at", end=' ')
3460
sys.stdout.flush()
3461
for i in range(L + 1):
3462
if not quiet:
3463
print('level %r' % i, end=' ')
3464
sys.stdout.flush()
3465
prod *= self.top_xi_at_level(ep, i, quiet=quiet)
3466
if prod == 0:
3467
if not quiet:
3468
print("Product 0.", end=' ')
3469
sys.stdout.flush()
3470
break
3471
if not quiet:
3472
print('Done.')
3473
sys.stdout.flush()
3474
ec += prod
3475
return (-1)**N * ec
3476
3477
def euler_characteristic(self):
3478
"""
3479
Calculate the (Orbifold) Euler characteristic of self by evaluating top xi
3480
powers on levels. See also euler_char_immediate_evaluation.
3481
3482
Returns:
3483
QQ: (Orbifold) Euler characteristic of self.
3484
3485
EXAMPLES::
3486
3487
sage: from admcycles.diffstrata import *
3488
sage: X=Stratum((2,))
3489
sage: X.euler_characteristic()
3490
-1/40
3491
"""
3492
return self.euler_char_immediate_evaluation()
3493
3494
def euler_char(self, quiet=True, alg='direct'):
3495
"""
3496
Calculate the (Orbifold) Euler characteristic of self by computing the top
3497
Chern class and evaluating this.
3498
3499
Note that this is significantly slower than using self.euler_characteristic!
3500
3501
The optional keyword argument alg determines how the top Chern class
3502
is computed and can be either:
3503
* direct (default): using top_chern_class_direct
3504
* alt: using top_chern_class_alt
3505
* other: using top_chern_class
3506
3507
Args:
3508
quiet (bool, optional): no output. Defaults to True.
3509
alg (str, optional): algorithm (see above). Defaults to 'direct'.
3510
3511
Returns:
3512
QQ: (Orbifold) Euler characteristic of self.
3513
3514
EXAMPLES::
3515
3516
sage: from admcycles.diffstrata import *
3517
sage: X=Stratum((2,))
3518
sage: X.euler_char()
3519
-1/40
3520
sage: X.euler_char(alg='alt')
3521
-1/40
3522
sage: X.euler_char(alg='other')
3523
-1/40
3524
"""
3525
if alg == 'direct':
3526
tcc = self.top_chern_class_direct(quiet=quiet)
3527
elif alg == 'alt':
3528
tcc = self.top_chern_class_alt(quiet=quiet)
3529
else:
3530
tcc = self.top_chern_class(quiet=quiet, alg=alg)
3531
if not quiet:
3532
print('Evaluating...')
3533
return (-1)**self.dim() * tcc.evaluate(quiet=True)
3534
3535
def top_chern_class(self, inside=True, prod=True,
3536
top=False, quiet=True, alg='fast'):
3537
"""
3538
Compute the top Chern class from the Chern polynomial via the Chern character.
3539
3540
This uses chern_poly.
3541
3542
INPUT:
3543
3544
inside: bool (optional)
3545
Passed to chern_poly. Defaults to True.
3546
3547
prod: bool (optional)
3548
Passed to chern_poly. Defaults to True.
3549
3550
top: bool (optional)
3551
Passed to chern_poly. Defaults to False.
3552
3553
quiet: bool (optional)
3554
Passed to chern_poly. Defaults to True.
3555
3556
alg: str (optional)
3557
Passed to chern_poly. Defaults to 'fast'.
3558
3559
OUTPUT:
3560
3561
c_top(T) of self.
3562
"""
3563
return self.chern_poly(inside=inside, prod=prod,
3564
top=top, quiet=quiet, alg=alg)[-1]
3565
3566
def chern_poly(self, inside=True, prod=True, top=False,
3567
quiet=True, alg='fast', upto=None):
3568
"""
3569
The Chern polynomial calculated from the Chern character.
3570
3571
The optional keyword argument alg determines how the Chern character
3572
is computed and can be either:
3573
3574
* fast (default): use ch_E_fast
3575
* bic_prod: use ch_E_prod
3576
* other: use ch_E
3577
3578
INPUT:
3579
3580
inside: bool (optional)
3581
Passed to ch_E. Defaults to True.
3582
3583
prod: bool (optional)
3584
Passed to ch_E. Defaults to True.
3585
3586
top: bool (optional)
3587
Passed to ch_E. Defaults to False.
3588
3589
quiet: bool (optional)
3590
No output. Defaults to True.
3591
3592
alg: str (optional)
3593
Algorithm used (see above). Defaults to 'fast'.
3594
3595
upto: integer (optional)
3596
highest degree of polynomial to calculate. Defaults to None (i.e. dim so the whole polynomial).
3597
3598
OUTPUT:
3599
3600
The Chern polynomial as list of ELGTautClasses (indexed by degree)
3601
"""
3602
if alg == 'bic_prod':
3603
ch = self.ch_E_prod(quiet=quiet)
3604
elif alg == 'fast':
3605
ch = self.ch_E_fast(quiet=quiet)
3606
else:
3607
ch = self.ch_E(inside=inside, prod=prod, top=top, quiet=quiet)
3608
return self.charToPol(ch, quiet=quiet, upto=upto)
3609
3610
def chern_class(self, n, quiet=True):
3611
"""
3612
A direct formula for the n-th Chern class of the tangent bundle of self.
3613
3614
Args:
3615
n (int): degree
3616
quiet (bool, optional): No output. Defaults to True.
3617
3618
Returns:
3619
ELGTautClass: c_n(T) of self.
3620
"""
3621
N = self.dim() + 1
3622
c_n = []
3623
for L in range(N):
3624
if not quiet:
3625
print('Going through %r profiles of length %r...' %
3626
(len(self.enhanced_profiles_of_length(L)), L))
3627
summand = []
3628
for ep in self.enhanced_profiles_of_length(L):
3629
if not quiet:
3630
print("Profile: %r" % (ep,), end=' ')
3631
p, _ = ep
3632
delta = [self.bics[b] for b in p]
3633
ld = [B.ell for B in delta]
3634
Nd = [B.top.dim() + 1 for B in delta]
3635
ld_prod = 1
3636
for l in ld:
3637
ld_prod *= l
3638
inner = []
3639
for K in WeightedIntegerVectors(n - L, [1] * (L + 1)):
3640
if not quiet:
3641
print('xi coefficient: k_0:', K[0], end=' ')
3642
print('N-L-sum:', N - L - sum(K[1:]), end=' ')
3643
print('Binomial:', binomial(N - L - sum(K[1:]), K[0]))
3644
factor = binomial(N - L - sum(K[1:]), K[0])
3645
prod = self.xi_at_level_pow(0, ep, K[0])
3646
for i, k in list(enumerate(K))[1:]:
3647
if not quiet:
3648
print('k_%r: %r' % (i, k), end=' ')
3649
print('r_Gamma,i:', (N - Nd[i - 1]), end=' ')
3650
print('L-i: %r, sum: %r' %
3651
(L - i, sum(K[i + 1:])), end=' ')
3652
print('Binomial:', binomial(
3653
(N - Nd[i - 1]) - (L - i) - sum(K[i + 1:]), k + 1))
3654
factor *= binomial((N - Nd[i - 1]) -
3655
(L - i) - sum(K[i + 1:]), k + 1)
3656
squish = self.squish(ep, i - 1)
3657
X_pow = self.pow(
3658
ld[i - 1] * self.cnb(ep, ep, squish), k, ep)
3659
prod = self.intersection(prod, X_pow, ep)
3660
inner.append(factor * prod)
3661
summand.append(ld_prod * self.ELGsum(inner))
3662
c_n.append(self.ELGsum(summand))
3663
return self.ELGsum(c_n)
3664
3665
def ch_E_prod(self, quiet=True):
3666
"""
3667
The product version of the Chern character formula.
3668
3669
Args:
3670
quiet (bool, optional): No output. Defaults to True.
3671
3672
Returns:
3673
ELGTautClass: Chern character of the tangent bundle.
3674
"""
3675
N = QQ(self.dim() + 1)
3676
ch_E = [N * self.ONE]
3677
for L, profiles in enumerate(self.lookup_list):
3678
if not quiet:
3679
print('Going through %r profiles of length %r...' %
3680
(len(profiles), L))
3681
summand = []
3682
for p in profiles:
3683
if not p:
3684
continue
3685
Nd = self.bics[p[-1]].top.dim() + 1
3686
if N == Nd: # factor == 0
3687
continue
3688
factor = (N - Nd)
3689
bic_prod = self.ONE
3690
for Di in p:
3691
bic_prod *= self.exp_bic(Di)
3692
summand.append(factor * bic_prod)
3693
ch_E.append(self.ELGsum(summand))
3694
return self.exp_xi(quiet=quiet) * self.ELGsum(ch_E)
3695
3696
def ch_E_fast(self, quiet=True):
3697
"""
3698
A more direct (and faster) formula for the Chern character (see sec 9 of the paper).
3699
3700
Args:
3701
quiet (bool, optional): No output. Defaults to True.
3702
3703
Returns:
3704
ELGTautClass: Chern character of the tangent bundle.
3705
"""
3706
N = QQ(self.dim() + 1)
3707
ch_E = [N * self.exp_xi(quiet=quiet)]
3708
for L in range(1, N):
3709
if not quiet:
3710
print('Going through %r profiles of length %r...' %
3711
(len(self.enhanced_profiles_of_length(L)), L))
3712
summand = []
3713
for ep in self.enhanced_profiles_of_length(L):
3714
p, _ = ep
3715
ld = [self.bics[b].ell for b in p]
3716
Nd = self.bics[p[-1]].top.dim() + 1
3717
if N == Nd: # factor == 0
3718
continue
3719
ld_prod = 1
3720
for l in ld:
3721
ld_prod *= l
3722
factor = ld_prod * (N - Nd)
3723
td_prod = self.ONE
3724
for i in range(L):
3725
td_prod = self.intersection(
3726
td_prod, self.td_contrib(-ld[i], self.cnb(ep, ep, self.squish(ep, i)), ep), ep)
3727
exi = self.exp(self.xi_at_level(0, ep), ep)
3728
pr = self.intersection(exi, td_prod, ep)
3729
summand.append(factor * pr)
3730
ch_E.append(self.ELGsum(summand))
3731
return self.ELGsum(ch_E)
3732
3733
def top_chern_alt(self):
3734
"""
3735
The top Chern class of self.
3736
3737
This is computed by calculating the Chern polynomial
3738
from the Chern character as P_B*exp(L) and taking the top-degree part.
3739
3740
Returns:
3741
ELGTautClass: top Chern class of the tangent bundle.
3742
"""
3743
return self.charToPol(self.P_B * self.exp_L())[-1]
3744
3745
def first_term(self, top=False, quiet=True):
3746
"""
3747
The calculation of (N*self.exp_xi() - self.ONE)*self.exp_L() split into
3748
pieces with more debugging outputs (calculation can take a LONG time!)
3749
3750
Args:
3751
top (bool, optional): Do calculations on level. Defaults to False.
3752
quiet (bool, optional): No output. Defaults to True.
3753
3754
Returns:
3755
ELGTautClass: First term of ch.
3756
"""
3757
if not quiet:
3758
print('Calculating first term...')
3759
N = QQ(self.dim() + 1)
3760
BICs = []
3761
for i, B in enumerate(self.bics):
3762
BICs.append((B.ell, self.additive_generator(((i,), 0))))
3763
L = admcycles.diffstrata.elgtautclass.ELGTautClass(
3764
self, BICs, reduce=False)
3765
if top:
3766
if not quiet:
3767
print('Calculating exp_xi_L...')
3768
exp_xi_L = self.ELGsum([N * B.ell * self.exp(self.xi_at_level(0, ((i,), 0)), ((
3769
i,), 0), quiet=quiet) for i, B in enumerate(self.bics)] + [(-1) * L])
3770
last = exp_xi_L
3771
if not quiet:
3772
print('Calculating recursive exponential factors: ', end=' ')
3773
for k in range(1, N - 1):
3774
if not quiet:
3775
print(k, end=' ')
3776
last = QQ(1) / QQ(k + 1) * L * last
3777
if last == self.ZERO:
3778
break
3779
exp_xi_L._psi_list.extend(last.psi_list)
3780
if not quiet:
3781
print('Done!')
3782
print('Adding exp_xi...')
3783
res = self.ELGsum(
3784
[N * self.exp_xi(quiet=quiet), -self.ONE, exp_xi_L])
3785
else:
3786
if not quiet:
3787
print('Calculating exp(xi+L)...')
3788
res = N * self.exp(self.xi + L, quiet=quiet)
3789
if not quiet:
3790
print('Subtracting exp_L...')
3791
res -= self.exp_L(quiet=quiet)
3792
if not quiet:
3793
print('Done calculating first term!')
3794
return res
3795
3796
def ch_E(self, inside=True, prod=True, top=False, quiet=True):
3797
"""
3798
The Chern character (according to sec. 9 of the paper)
3799
3800
Args:
3801
inside (bool, optional): work with ambient. Defaults to True.
3802
prod (bool, optional): product instead of sum. Defaults to True.
3803
top (bool, optional): work on level. Defaults to False.
3804
quiet (bool, optional): no output. Defaults to True.
3805
3806
Returns:
3807
ELGTautClass: Chern character of the tangent bundle of self.
3808
"""
3809
# Prop. 9.2
3810
N = QQ(self.dim() + 1)
3811
# ch = [(N*self.exp_xi() + (-1)*self.ONE)*self.exp_L()]
3812
ch = [self.first_term(top=top, quiet=quiet)]
3813
for L in range(1, N):
3814
inner = []
3815
if not quiet:
3816
print('Going through profiles of length %r...' % L)
3817
for enh_profile in self.enhanced_profiles_of_length(L):
3818
p, _ = enh_profile
3819
B = self.bics[p[0]]
3820
Ntop = B.top.dim() + 1
3821
if not inside:
3822
summand = (-1)**L * (Ntop * self.exp_xi() - self.ONE)
3823
else:
3824
if not quiet:
3825
print('Calculating inner exp(xi): ', end=' ')
3826
summand = (-1)**L * (Ntop * self.exp(self.xi_at_level(0, enh_profile),
3827
enh_profile, quiet=quiet) - self.taut_from_graph(*enh_profile))
3828
prod_list = []
3829
for i in range(L):
3830
ll = self.bics[p[i]].ell
3831
squish = self.squish(enh_profile, i)
3832
td_NB = ll * \
3833
self.td_contrib(-ll, self.cnb(enh_profile,
3834
enh_profile, squish), enh_profile)
3835
prod_list.append(td_NB)
3836
if prod_list:
3837
prod = prod_list[0]
3838
for f in prod_list[1:]:
3839
# multiply with ambient Gamma (=enh_profile)!
3840
prod = self.intersection(prod, f, enh_profile)
3841
if prod:
3842
for l in range(len(p) + 1):
3843
prod = self.intersection(
3844
prod,
3845
self.exp(
3846
self.calL(
3847
enh_profile,
3848
l),
3849
enh_profile),
3850
enh_profile)
3851
else:
3852
prod = self.intersection(
3853
prod,
3854
self.exp(
3855
self.ELGsum(
3856
self.calL(
3857
enh_profile,
3858
l) for l in range(
3859
len(p) +
3860
1)),
3861
enh_profile),
3862
enh_profile)
3863
if inside:
3864
prod = self.intersection(prod, summand, enh_profile)
3865
# multiply constant term with Gamma (for i_*)
3866
const = prod.degree(0)
3867
prod += (-1) * const
3868
if inside:
3869
summand = prod
3870
else:
3871
summand *= (prod + const *
3872
self.taut_from_graph(*enh_profile))
3873
inner.append(summand)
3874
ch.append(self.ELGsum(inner))
3875
return self.ELGsum(ch)
3876
3877
################################################################
3878
# END OF SEC 9 FORMULAS #
3879
################################################################
3880
3881
def res_stratum_class(self, cond, debug=False):
3882
"""
3883
The class of the stratum cut out by cond inside self.
3884
3885
INPUT:
3886
3887
cond (list): list of a residue condition, i.e. a list of poles of self.
3888
3889
OUTPUT:
3890
3891
Tautological class of Prop. 9.3
3892
"""
3893
st_class = -1 * self.xi_with_leg(quiet=True)
3894
bic_list = []
3895
if debug:
3896
print(
3897
"Calculating the class of the stratum cut out by %r in %r..." %
3898
(cond, self))
3899
print("-xi = %s" % st_class)
3900
for i, B in enumerate(self.bics):
3901
if debug:
3902
print("Checking BIC %r:" % i)
3903
top = B.top
3904
# we restrict/translate cond to top:
3905
poles_on_bic = [B.dmp_inv[p] for p in cond]
3906
cond_on_top = [top.leg_dict[leg]
3907
for leg in poles_on_bic if leg in top.leg_dict]
3908
# if there are RCs on top, we must check that they don't change the
3909
# rank
3910
if cond_on_top:
3911
MT = top.matrix_from_res_conditions([cond_on_top])
3912
top_G = top.smooth_LG
3913
RT = top_G.full_residue_matrix
3914
if (MT.stack(RT)).rank() != RT.rank():
3915
assert (MT.stack(RT)).rank() > RT.rank()
3916
if debug:
3917
print("Discarding (because of top).")
3918
continue
3919
l = B.ell
3920
if debug:
3921
print("Appending with coefficient -%r" % l)
3922
bic_list.append((l, i))
3923
st_class += self.ELGsum([-l * self.taut_from_graph((i,), 0)
3924
for l, i in bic_list])
3925
return st_class
3926
3927
def adm_evaluate(self, stgraph, psis, sig, g, quiet=False,
3928
admcycles_output=False):
3929
"""
3930
Evaluate the psi monomial on a (connected) stratum without residue conditions
3931
using admcycles.
3932
3933
stgraph should be the one-vertex graph associated to the stratum sig.
3934
3935
We use admcycles Strataclass to calculate the class of the stratum inside
3936
Mbar_{g,n} and multiply this with psis (in admcycles) and evaluate the product.
3937
3938
The result is cached and synched with the ``ADM_EVALS`` cache.
3939
3940
Args:
3941
stgraph (stgraph): admcycles stgraph
3942
psis (dict): psi polynomial on stgraph
3943
sig (tuple): signature tuple
3944
g (int): genus of sig
3945
quiet (bool, optional): No output. Defaults to False.
3946
admcycles_output (bool, optional): Print the admcycles classes. Defaults to False.
3947
3948
Returns:
3949
QQ: integral of psis on stgraph.
3950
3951
TESTS:
3952
3953
Check that the cache can be deactivated and reactivated::
3954
3955
sage: from admcycles import StableGraph
3956
sage: from admcycles.diffstrata import Stratum
3957
sage: import admcycles.diffstrata.cache
3958
sage: X = Stratum((1,1))
3959
sage: stg = StableGraph([2], [[1,2]], [])
3960
sage: psis = {1: 1, 2: 3}
3961
sage: sig = (1, 1)
3962
sage: g = 2
3963
sage: tmp = admcycles.diffstrata.cache.ADM_EVALS
3964
sage: admcycles.diffstrata.cache.ADM_EVALS = admcycles.diffstrata.cache.FakeCache()
3965
sage: X.adm_evaluate(stg, psis, sig, g, quiet=True) is X.adm_evaluate(stg, psis, sig, g, quiet=True)
3966
False
3967
sage: admcycles.diffstrata.cache.ADM_EVALS = tmp
3968
sage: X.adm_evaluate(stg, psis, sig, g, quiet=True) is X.adm_evaluate(stg, psis, sig, g, quiet=True)
3969
True
3970
"""
3971
# key = (tuple(sorted(psis.items())), tuple(sig))
3972
key = adm_key(sig, psis)
3973
from .cache import ADM_EVALS
3974
if key not in ADM_EVALS:
3975
DS = admcycles.admcycles.decstratum(stgraph, psi=psis)
3976
Stratum_class = admcycles.stratarecursion.Strataclass(g, 1, sig)
3977
if not quiet or admcycles_output:
3978
print("DS: %r\n Stratum_class: %r" % (DS, Stratum_class))
3979
product = Stratum_class * DS # in admcycles!
3980
if not quiet or admcycles_output:
3981
print("Product: %r" % product.evaluate())
3982
answer = ADM_EVALS[key] = product.evaluate() # in admcycles!
3983
return answer
3984
else:
3985
return ADM_EVALS[key]
3986
3987
def remove_res_cond(self, psis=None):
3988
"""
3989
Remove residue conditions until the rank drops (or there are none left).
3990
3991
We return the stratum with fewer residue conditions and, in
3992
case the rank dropped, with the product of the stratum class.
3993
3994
Note that this does *not* ensure that all residue conditions are removed!
3995
3996
Args:
3997
psis (dict, optional): Psi dictionary on self. Defaults to None.
3998
3999
Returns:
4000
ELGTautClass: ELGTautClass on Stratum with less residue conditions
4001
(or self if there were none!)
4002
4003
EXAMPLES::
4004
4005
sage: from admcycles.diffstrata import *
4006
sage: X=GeneralisedStratum([Signature((1,1,-2,-2))], res_cond=[[(0,2)], [(0,3)]])
4007
sage: print(X.remove_res_cond())
4008
Tautological class on Stratum: Signature((1, 1, -2, -2))
4009
with residue conditions:
4010
dimension: 1
4011
leg dictionary: {}
4012
<BLANKLINE>
4013
1 * Psi class 3 with exponent 1 on level 0 * Graph ((), 0) +
4014
<BLANKLINE>
4015
sage: X.evaluate(quiet=True) == X.remove_res_cond().evaluate()
4016
True
4017
"""
4018
if psis is None:
4019
psis = {}
4020
4021
if not self.res_cond:
4022
return self.additive_generator(((), 0), psis).as_taut()
4023
4024
try:
4025
new_leg_dict = deepcopy(self._leg_dict)
4026
except AttributeError:
4027
new_leg_dict = {}
4028
4029
# Create new stratum with one residue condition less:
4030
new_rc = deepcopy(self._res_cond)
4031
# conditions from RT:
4032
RT_M = self.smooth_LG.residue_matrix_from_RT
4033
# we remove conditions until the rank drops:
4034
while new_rc:
4035
lost_cond = new_rc.pop()
4036
new_M = self.matrix_from_res_conditions(new_rc)
4037
if new_M:
4038
full_M = new_M.stack(RT_M)
4039
else:
4040
full_M = RT_M
4041
if full_M.rank() == self.smooth_LG.full_residue_matrix.rank() - 1:
4042
# rank dropped
4043
break
4044
new_stratum = LevelStratum(self._sig_list, new_rc, new_leg_dict)
4045
# Because only the RCs changed, X.smooth_LG still lives inside this stratum
4046
# so we can use it to build our new AdditiveGenerator:
4047
new_AG = new_stratum.additive_generator(((), 0), psis)
4048
if new_stratum.dim() == self.dim() + 1:
4049
new_class = new_AG.as_taut() * new_stratum.res_stratum_class(lost_cond)
4050
else:
4051
# rank did not drop so all residue conditions are gone:
4052
assert not new_rc
4053
new_class = new_AG.as_taut()
4054
4055
return new_class
4056
4057
def zeroStratumClass(self):
4058
"""
4059
Check if self splits, i.e. if a subset of vertices can be scaled
4060
independently (then the stratum class is ZERO).
4061
4062
We do this by checking if BICs B, B' exist with:
4063
* no edges
4064
* the top vertices of B are the bottom vertices of B'
4065
* the bottom vertices of B' are the top vertices of B.
4066
4067
Explicitly, we loop through all BICs with no edges, constructing for
4068
each one the BIC with the levels interchanged (as an EmbeddedLevelGraph)
4069
and check its legality.
4070
4071
Returns:
4072
boolean: True if splitting exists, False otherwise.
4073
4074
EXAMPLES::
4075
4076
sage: from admcycles.diffstrata import *
4077
sage: GeneralisedStratum([Signature((0,)),Signature((0,))]).zeroStratumClass()
4078
True
4079
sage: GeneralisedStratum([Signature((2,))]).zeroStratumClass()
4080
False
4081
sage: GeneralisedStratum([Signature((4,-2,-2,-2)),Signature((4,-2,-2,-2))], res_cond=[[(0,2),(1,2)]]).zeroStratumClass()
4082
True
4083
sage: GeneralisedStratum([Signature((2, -2, -2)), Signature((1, 1, -2, -2))],[[(0, 2), (1, 2)], [(0, 1), (1, 3)]]).zeroStratumClass()
4084
False
4085
"""
4086
bics_no_edges = [b for b in self.bics if not b.LG.edges]
4087
if not bics_no_edges:
4088
return False
4089
for b in bics_no_edges:
4090
internal_top = b.LG.internal_level_number(0)
4091
internal_bot = b.LG.internal_level_number(1)
4092
top_vertices = b.LG.verticesonlevel(internal_top)
4093
bot_vertices = b.LG.verticesonlevel(internal_bot)
4094
assert len(top_vertices) + len(bot_vertices) == len(b.LG.genera)
4095
# build graph levels exchanged:
4096
new_levels = [internal_bot if v in top_vertices else internal_top
4097
for v in range(len(b.LG.genera))]
4098
new_vertices = deepcopy(b.LG.genera)
4099
new_legs = deepcopy(b.LG.legs)
4100
new_edges = []
4101
new_poleorders = deepcopy(b.LG.poleorders)
4102
new_LG = admcycles.diffstrata.levelgraph.LevelGraph(
4103
new_vertices, new_legs, new_edges, new_poleorders, new_levels)
4104
new_ELG = admcycles.diffstrata.embeddedlevelgraph.EmbeddedLevelGraph(
4105
self, new_LG, deepcopy(b.dmp), deepcopy(b.dlevels))
4106
# check if new graph is legal:
4107
if new_ELG.is_legal():
4108
return True
4109
# no splitting found
4110
return False
4111
4112
def evaluate(self, psis={}, quiet=False, warnings_only=False,
4113
admcycles_output=False):
4114
"""
4115
Evaluate the psi monomial psis on self.
4116
4117
Psis is a dictionary legs of self.smooth_LG -> exponents encoding a psi monomial.
4118
4119
We translate residue conditions of self into intersections of simpler classes
4120
and feed the final pieces into admcycles for actual evaluation.
4121
4122
Args:
4123
psis (dict, optional): Psi monomial (as legs of smooth_LG -> exponent). Defaults to {}.
4124
quiet (bool, optional): No output. Defaults to False.
4125
warnings_only (bool, optional): Only warnings. Defaults to False.
4126
admcycles_output (bool, optional): adm_eval output. Defaults to False.
4127
4128
Raises:
4129
RuntimeError: raised if a required residue condition is not found.
4130
4131
Returns:
4132
QQ: integral of psis against self.
4133
"""
4134
G = self.smooth_LG
4135
LG = G.LG
4136
# Check if the rGRC doesn't cut down the dimension:
4137
# Recall:
4138
# * residue_matrix_from_RT has the RT on each component of G as rows
4139
# * full_residue_matrix is this + the res_cond of self
4140
if G.full_residue_matrix.rank() == G.residue_matrix_from_RT.rank():
4141
if self._h0 > 1:
4142
if not quiet or warnings_only:
4143
print("----------------------------------------------------")
4144
print("Level %r disconnected." % self)
4145
print("----------------------------------------------------")
4146
print("No residue conditions: contribution is 0.")
4147
return 0
4148
# stratum is connected!
4149
# 0 dimensional strata contribute 1
4150
if self.dim() == 0:
4151
return 1
4152
# We can just use admcycles to evaluate:
4153
return self.adm_evaluate(
4154
LG.stgraph,
4155
psis,
4156
self._sig_list[0].sig,
4157
LG.g(),
4158
quiet=quiet,
4159
admcycles_output=admcycles_output)
4160
# There *are* non-trivial residue conditions!
4161
if self._h0 > 1:
4162
if not quiet or warnings_only:
4163
print("----------------------------------------------------")
4164
print("Level %r disconnected." % self)
4165
print("----------------------------------------------------")
4166
# Check if graph of residue conditions is disconnected:
4167
if not LG.underlying_graph.is_connected():
4168
if not quiet or warnings_only:
4169
print("Level is product: contribution is 0.")
4170
return 0
4171
# Create new stratum with one residue condition less:
4172
new_rc = deepcopy(self._res_cond)
4173
# conditions from RT:
4174
RT_M = G.residue_matrix_from_RT
4175
# we remove conditions until the rank drops:
4176
while new_rc:
4177
lost_cond = new_rc.pop()
4178
new_M = self.matrix_from_res_conditions(new_rc)
4179
if new_M:
4180
full_M = new_M.stack(RT_M)
4181
else:
4182
full_M = RT_M
4183
if full_M.rank() == G.full_residue_matrix.rank() - 1:
4184
# rank dropped
4185
break
4186
else:
4187
raise RuntimeError(
4188
"No Conditions cause dimension to drop in %r!" %
4189
self._res_cond)
4190
try:
4191
new_leg_dict = deepcopy(self._leg_dict)
4192
except AttributeError:
4193
new_leg_dict = {}
4194
new_stratum = LevelStratum(self._sig_list, new_rc, new_leg_dict)
4195
if not quiet:
4196
print("Recursing into stratum %r" % new_stratum)
4197
assert new_stratum.dim() == self.dim() + 1
4198
# Because only the RCs changed, G still lives inside this stratum
4199
# so we can use it to build our new AdditiveGenerator:
4200
new_AG = new_stratum.additive_generator(((), 0), psis)
4201
new_class = new_AG.as_taut() * new_stratum.res_stratum_class(lost_cond)
4202
result = new_class.evaluate(quiet=quiet)
4203
return result
4204
4205
#################################################################
4206
#################################################################
4207
4208
def boundary_pullback(self, G=None, quiet=True):
4209
# while our graphs have underlying stable graphs, we don't
4210
# guarantee the numbering (this is the reason we work with
4211
# EmbeddedLevelGraphs). To use admcycles specialisation check
4212
# we have to remedy this:
4213
def _rename(ELG, stgraph):
4214
# make sure the marked points of stgraph are numbered 1,...,n
4215
num = 0
4216
mp_dict = {} # dict: leg of marked point -> number of mp
4217
for i in range(self._h0):
4218
for j in range(len(self._sig_list[i].sig)):
4219
num += 1
4220
mp_dict[ELG.dmp_inv[(i, j)]] = num
4221
assert num == self._n
4222
stgraph.rename_legs(mp_dict, shift=num) # shift edges accordingly
4223
if G is None:
4224
# total pullback
4225
# TODO
4226
return self.ZERO
4227
tautlist = []
4228
# go through all BICs and check if G is an undegeneration (in M_g,n)
4229
# of the BIC's underlying stable graph:
4230
for b, B in enumerate(self.bics):
4231
stgraph = B.LG.stgraph.copy() # original is immutable...
4232
_rename(B, stgraph) # fix leg numbering
4233
graph_morphisms = admcycles.admcycles.Astructures(stgraph, G)
4234
num_degenerations = len(graph_morphisms)
4235
if num_degenerations == 0:
4236
continue
4237
# calculate preimage of edge:
4238
# note that leg names were shifted by n above!
4239
e = G.edges()[0]
4240
pre_e = [(m[1][e[0]] - self._n, m[1][e[1]] - self._n)
4241
for m in graph_morphisms]
4242
coeff = sum(QQ(B.ell) / QQ(B.LG.prong(e)) for e in pre_e)
4243
if not quiet:
4244
print(
4245
'\nFound BIC %r with stable graph %r in preimage with %r degenerations' %
4246
(b, stgraph, num_degenerations))
4247
print('sum of involved edges (ell / prong): ', coeff)
4248
print('Any level pushs to ZERO: ', any(
4249
B.level(l).zeroStratumClass() for l in range(B.codim + 1)))
4250
tautlist.append(coeff * self.taut_from_graph((b,)))
4251
return self.ELGsum(tautlist)
4252
4253
#################################################################
4254
#################################################################
4255
4256
@property
4257
def kappa_1(self):
4258
"""
4259
The Mumford class of self.
4260
4261
Note that kappa_1_AC = kappa_1 + sum(psis)
4262
4263
Returns:
4264
ELGTautClass: kappa_1_AC - sum(psis)
4265
4266
EXAMPLES::
4267
4268
sage: from admcycles.diffstrata import *
4269
sage: X=Stratum((1,1,1,-5))
4270
sage: X.kappa_1.evaluate()
4271
-3
4272
"""
4273
if self._h0 > 1:
4274
return self.ZERO
4275
kappa = [self.kappa_1_AC]
4276
for i in range(1, self._n + 1):
4277
kappa.append(-self.psi(i))
4278
return self.ELGsum(kappa)
4279
4280
@property
4281
def kappa_1_AC(self):
4282
"""
4283
The Arbarello--Cornalba class of self.
4284
4285
Note that this 'corresponds' to admcycles kappaclass.
4286
4287
Returns:
4288
ELGTautClass: c_1(omega_log) + sum(l_Gamma * G_Gamma * bics) (-xi if hol)
4289
4290
EXAMPLES::
4291
4292
sage: from admcycles.diffstrata import *
4293
sage: X=Stratum((1,1,1,-5))
4294
sage: X.kappa_1_AC.evaluate()
4295
1
4296
sage: from admcycles import *
4297
sage: (kappaclass(1,0,4) * Strataclass(0, 1, [1,1,1,-5])).evaluate()
4298
1
4299
sage: from admcycles.diffstrata import *
4300
4301
sage: X=Stratum((2,))
4302
sage: (X.kappa_1_AC.to_prodtautclass().pushforward() - kappaclass(1, 2, 1)*Strataclass(2, 1, [2])).is_zero()
4303
True
4304
sage: X=Stratum((1,1))
4305
sage: (X.kappa_1_AC.to_prodtautclass().pushforward() - kappaclass(1, 2, 2)*Strataclass(2, 1, [1,1])).is_zero()
4306
True
4307
"""
4308
if self._h0 > 1:
4309
return self.ZERO
4310
kappa = [self.c1_E]
4311
for b, B in enumerate(self.bics):
4312
# note that every component has a pole!
4313
factor = B.ell * \
4314
(B.bot.smooth_LG.full_residue_matrix.rank() - B.bot._h0)
4315
if factor != 0:
4316
kappa.append(factor * self.taut_from_graph((b,)))
4317
if not self._polelist:
4318
# holomorphic!
4319
kappa.append(-self.xi)
4320
return self.ELGsum(kappa)
4321
4322
@property
4323
def kappa_1_dawei(self):
4324
if self._h0 > 1:
4325
return self.ZERO
4326
sig = self._sig_list[0].sig
4327
kappa = [sum(sig) * self.xi]
4328
kappa.extend([m * self.psi(i + 1) for i, m in enumerate(sig)])
4329
for b, B in enumerate(self.bics):
4330
# reminder: B.emb_bot: MP on bottom level -> MP on B
4331
sum_mi = sum(B.bot.stratum_point_order(p) for p in B.emb_bot)
4332
sum_k = sum(B.LG.prongs.values())
4333
factor = B.ell * (sum_mi - sum_k)
4334
kappa.append(factor * self.taut_from_graph((b,)))
4335
return self.ELGsum(kappa)
4336
4337
@property
4338
def kappa_1_dawei_alt(self):
4339
if self._h0 > 1:
4340
return self.ZERO
4341
kappa = [self.kappa_EKZ * self.xi]
4342
for b, B in enumerate(self.bics):
4343
sum_k = sum(B.LG.prongs.values())
4344
# reminder: B.emb_bot: MP on bottom level -> MP on B
4345
mis = [B.bot.stratum_point_order(p) for p in B.emb_bot]
4346
factor = B.ell * (sum(QQ(m * (m + 2)) / QQ(m + 1) for m in mis) - sum_k)
4347
kappa.append(factor * self.taut_from_graph((b,)))
4348
return self.ELGsum(kappa)
4349
4350
@property
4351
def kappa_EKZ(self):
4352
"""
4353
The EKZ kappa, i.e. sum m_i*(m_i+2)/(m_i+1) for self.
4354
4355
Raises:
4356
ValueError: If self has a simple pole (div by 0)
4357
4358
Returns:
4359
QQ: EKZ kappa factor of self.
4360
4361
EXAMPLES::
4362
4363
sage: from admcycles.diffstrata import *
4364
sage: Stratum((1,1)).kappa_EKZ
4365
3
4366
"""
4367
sigs = [m for sig in self._sig_list for m in sig.sig]
4368
if any(m == -1 for m in sigs):
4369
raise ValueError('Cannot compute EKZ kappa for simple poles!')
4370
return sum(QQ(m * (m + 2)) / QQ(m + 1) for m in sigs)
4371
4372
def horizontal_pushforward(self, graphs=None, psis=None):
4373
"""
4374
By default the total horizontal boundary.
4375
In general: takes an iterator (e.g. _horizontal_pf_iter) and
4376
sums over the second item (the pushforward classes)
4377
4378
Args:
4379
graphs (iterable, optional): an iterator yielding tuples of
4380
the form G, tautclass. By default _horizontal_pf_iter.
4381
Defaults to None.
4382
psis (dict, optional): psi dictionary for passing on.
4383
Defaults to None.
4384
4385
Returns:
4386
tautclass: the sum over the second items of graphs.
4387
"""
4388
if graphs is None:
4389
graphs = self._horizontal_pf_iter(psis)
4390
return sum(hpf for _, hpf in graphs)
4391
4392
def _horizontal_pf_iter(self, psis=None):
4393
"""
4394
Iterate over the components of the pushforward of the
4395
horizontal boundary.
4396
4397
Args:
4398
psis (dict, optional): Psi dictionary. Defaults to None.
4399
4400
Raises:
4401
StopIteration: If disconnected or done.
4402
4403
Yields:
4404
tuple: G, tautclass where G is StableGraph and tautclass
4405
is the pushed forward class of the horizontal divisor
4406
corresponding to G.
4407
"""
4408
if self._h0 > 1:
4409
raise StopIteration
4410
g = self._g[0]
4411
sig = self._sig_list[0].sig
4412
n = self._n
4413
if psis:
4414
H = admcycles.admcycles.StableGraph(
4415
[g], [list(range(1, n + 1))], [])
4416
psi_contr = admcycles.admcycles.tautclass(
4417
[admcycles.admcycles.decstratum(H, psi=psis)])
4418
else:
4419
psi_contr = 1
4420
if g > 0:
4421
# build horizontal stable graph:
4422
G = self.smooth_LG.LG.stgraph.copy()
4423
G.degenerate_nonsep(0)
4424
# put the strataclass of the g-1 stratum on a prodtautclass:
4425
new_sig = list(sig) + [-1, -1]
4426
if self._polelist:
4427
# meromorphic: have to add extra residue condition
4428
rc = [(0, n), (0, n + 1)]
4429
X = GeneralisedStratum(
4430
[admcycles.diffstrata.sig.Signature(new_sig)])
4431
tautlist = [X.res_stratum_class(
4432
rc).to_prodtautclass().pushforward()]
4433
else:
4434
# holomorphic: only residue cond is RT -> OK to work directly
4435
# with Strataclass
4436
tautlist = [
4437
admcycles.stratarecursion.Strataclass(g - 1, 1, new_sig)]
4438
ptc = admcycles.admcycles.prodtautclass(G, protaut=tautlist)
4439
stack_factor = QQ(1) / QQ(2)
4440
yield G, stack_factor * psi_contr * ptc.pushforward()
4441
if len(self._polelist) >= 2:
4442
# in this case, we also have horizontal divisors of compact type
4443
# Note that each component
4444
# * must have at least one pole
4445
# * the orders must sum to 2g_i - 2 + 1
4446
# We find all stable graphs with one edge and this property:
4447
for G in admcycles.admcycles.list_strata(g, n, 1):
4448
if len(G.genera()) == 1:
4449
continue
4450
# consider as disconnected stratum and resolve residue
4451
# condition
4452
gl, gr = G.genera()
4453
sigl = [sig[l - 1] for l in G.legs(0) if l <= n] + [-1]
4454
sigr = [sig[l - 1] for l in G.legs(1) if l <= n] + [-1]
4455
if sum(sigl) != 2 * gl - 2 or sum(sigr) != 2 * gr - 2:
4456
continue
4457
if all(a >= 0 for a in sigl) or all(a >= 0 for a in sigr):
4458
continue
4459
# we need the class of the disconnected stratum where the
4460
# residues at the simple poles add up to zero:
4461
rc = [(0, len(sigl) - 1), (1, len(sigr) - 1)]
4462
X = GeneralisedStratum([admcycles.diffstrata.sig.Signature(
4463
sigl), admcycles.diffstrata.sig.Signature(sigr)])
4464
ptc = X.res_stratum_class(rc).to_prodtautclass()
4465
if ptc == 0:
4466
continue
4467
# now we replace the underlying (disconnected!) graph
4468
# of ptc by the (connected!) graph G:
4469
ptc.gamma = G
4470
stack_factor = QQ(1) / QQ(G.automorphism_number())
4471
yield G, stack_factor * psi_contr * ptc.pushforward()
4472
4473
def masur_veech_volume(self):
4474
"""
4475
If self is a connected stratum of holomorphic differentials,
4476
calculate the Masur-Veech volume of self using the formula [CMSZ2020]_
4477
of Chen-Möller-Sauvaget-Zagier.
4478
Otherwise throws NotImplementedError.
4479
4480
EXAMPLES:
4481
4482
sage: from admcycles.diffstrata import Stratum
4483
sage: X = Stratum((0,))
4484
sage: X.masur_veech_volume()
4485
1/3*pi^2
4486
4487
sage: X = Stratum((2,))
4488
sage: X.masur_veech_volume()
4489
1/120*pi^4
4490
4491
sage: X = Stratum((1,1))
4492
sage: X.masur_veech_volume()
4493
1/135*pi^4
4494
4495
sage: X = Stratum((4,))
4496
sage: X.masur_veech_volume()
4497
61/108864*pi^6
4498
4499
sage: X = Stratum((-1,-1,0))
4500
sage: X.masur_veech_volume()
4501
Traceback (most recent call last):
4502
...
4503
NotImplementedError
4504
4505
"""
4506
if self._h0 != 1 or self._p != 0:
4507
raise NotImplementedError
4508
g = self._g[0]
4509
n = self._n
4510
return - 2 * (2 * I * pi)**(2 * g) / factorial(2 * g - 3 + n) * \
4511
(self.xi_pow(2 * g - 2) *
4512
prod(self.psi(l) for l in range(1, n + 1))).evaluate()
4513
4514
#################################################################
4515
#################################################################
4516
#################################################################
4517
#################################################################
4518
4519
4520
class Stratum(GeneralisedStratum):
4521
"""
4522
A simpler frontend for a GeneralisedStratum with one component and
4523
no residue conditions.
4524
"""
4525
4526
def __init__(self, sig):
4527
super().__init__(
4528
[admcycles.diffstrata.sig.Signature(sig)])
4529
4530
#################################################################
4531
#################################################################
4532
#################################################################
4533
#################################################################
4534
4535
4536
class LevelStratum(GeneralisedStratum):
4537
"""
4538
A stratum that appears as a level of a levelgraph.
4539
4540
This is a ``GeneralisedStratum`` together with a dictionary mapping the
4541
leg numbers of the (big) graph to the legs of the ``Generalisedstratum``.
4542
4543
Note that if this is initialised from an EmbeddedLevelGraph, we also
4544
have the attribute leg_orbits, a nested list giving the orbits of
4545
the points under the automorphism group of the graph.
4546
4547
* leg_dict : a (bijective!) dictionary mapping the leg numbers of a graph
4548
to the corresponding tuple (i,j), i.e. the point j on the component i.
4549
4550
* res_cond : a (nested) list of residue conditions given by the r-GRC when
4551
extracting a level.
4552
4553
"""
4554
4555
def __init__(self, sig_list, res_cond=None, leg_dict=None):
4556
super().__init__(sig_list, res_cond)
4557
if leg_dict is None:
4558
# assume the points were numbered 1...n
4559
self._leg_dict = {}
4560
for i in range(len(sig_list)):
4561
for j in range(sig_list[i].n):
4562
self._leg_dict[i + j + 1] = (i, j)
4563
else:
4564
self._leg_dict = leg_dict
4565
# build inverse dictionary
4566
self._inv_leg_dict = {v: k for k, v in self._leg_dict.items()}
4567
4568
def __repr__(self):
4569
return "LevelStratum(sig_list=%r,res_cond=%r,leg_dict=%r)" % (
4570
self._sig_list, self._res_cond, self.leg_dict)
4571
4572
def __str__(self):
4573
rep = ''
4574
if self._h0 > 1:
4575
rep += 'Product of Strata:\n'
4576
else:
4577
rep += 'Stratum: '
4578
for sig in self._sig_list:
4579
rep += repr(sig) + '\n'
4580
rep += 'with residue conditions: '
4581
for res in self._res_cond:
4582
rep += repr(res) + ' '
4583
rep += '\n'
4584
rep += 'dimension: ' + repr(self.dim()) + '\n'
4585
rep += 'leg dictionary: ' + repr(self._leg_dict) + '\n'
4586
try:
4587
rep += 'leg orbits: ' + repr(self.leg_orbits) + '\n'
4588
except AttributeError:
4589
pass
4590
return rep
4591
4592
@cached_method
4593
def dict_key(self):
4594
"""
4595
The hash-key for the cache of top-xi-powers.
4596
4597
More precisely, we sort each signature, sort this list and renumber
4598
the residue conditions accordingly. Finally, everything is made into a tuple.
4599
4600
Returns:
4601
tuple: nested tuple.
4602
"""
4603
rc_dict = {}
4604
sig = []
4605
for new_i, new_sign in enumerate(
4606
sorted(enumerate(self._sig_list), key=lambda k: k[1].sig)):
4607
i, sign = new_sign
4608
curr_sig = []
4609
for new_j, s in enumerate(
4610
sorted(enumerate(sign.sig), key=lambda k: k[1])):
4611
j, a = s
4612
curr_sig.append(a)
4613
rc_dict[(i, j)] = (new_i, new_j)
4614
sig.append(tuple(curr_sig))
4615
sig = tuple(sig)
4616
rc = sorted([sorted([rc_dict[cond] for cond in conds])
4617
for conds in self._res_cond])
4618
rc = tuple(tuple(c) for c in rc)
4619
return (sig, rc)
4620
4621
@property
4622
def leg_dict(self):
4623
return self._leg_dict
4624
4625
@property
4626
def inv_leg_dict(self):
4627
return self._inv_leg_dict
4628
4629
# Psi classes are numbered according to the points of the stratum, but we want
4630
# to use them for the points of the graph. The leg_dicts translate between these,
4631
# we make this a little more user friendly.
4632
def stratum_number(self, n):
4633
"""
4634
Returns a tuple (i,j) for the point j on the component i that corresponds
4635
to the leg n of the graph.
4636
"""
4637
return self._leg_dict[n]
4638
4639
def leg_number(self, n):
4640
"""
4641
Returns the leg number (of the graph G) that corresponds to the psi class
4642
number n.
4643
"""
4644
return self._inv_leg_dict[n]
4645
4646