def orbifold_euler_characteristics(g,n): """ Return the orbifold/virtual Euler characteristics of `M_{g,n}`, computed according to Harer-Zagier. """ if g==0: return factorial(n-3) * minus_one_exp(n-3) elif g==1: return Fraction(minus_one_exp(n), 12) * factorial(n-1) else: # g > 1 return bernoulli(2*g) * factorial(2*g+n-3) / (factorial(2*g-2) * 2*g) * minus_one_exp(n)
def euler_characteristics(g, n): """ Return Euler characteristics of `M_{g,n}`. The Euler characteristics is computed according to formulas and tables found in: * Bini-Gaiffi-Polito, arXiv:math/9806048, p.3 * Bini-Harer, arXiv:math/0506083, p. 10 """ if g == 0: # according to Bini-Gaiffi-Polito arXiv:math/9806048, p.3 return factorial(n - 3) * minus_one_exp(n - 3) elif g == 1: # according to Bini-Gaiffi-Polito, p. 15 if n > 4: return factorial(n - 1) * Fraction(minus_one_exp(n - 1), 12) else: es = [1, 1, 0, 0] return es[n - 1] # no n==0 computed in [BGP] elif g == 2: # according to Bini-Gaiffi-Polito, p. 14 if n > 6: return factorial(n + 1) * Fraction(minus_one_exp(n + 1), 240) else: es = [1, 2, 2, 0, -4, 0, -24] return es[n] elif g > 2: # according to Bini-Harer arXiv:math/0506083, p. 10 es = [ # n==1 n==2 n==3 n==4 n==5 n==6 n==7 n==8 [8, 6, 4, -10, 30, -660, 6540, 79200], # g==3 [-2, -10, -24, -24, -360, 2352, -37296, 501984], # g==4 [12, 26, 92, 182, 1674, -16716, 238980, -3961440], # g==5 [0, -46, -206, 188, -7512, 124296, -2068392, 37108656], # g==6 [38, 120, 676, -1862, 71866, -1058676, 21391644, -422727360], # g==7 [ -166, -630, -5362, 16108, -680616, 12234600, -259464240, 5719946400 ], # g==8 [ 748, 2132, 29632, -323546, 7462326, -164522628, 3771668220, -90553767840 ], # g==9 [ -1994, 6078, -213066, 4673496, -106844744, 2559934440, -64133209320, 1.664e+12 ], # g==10 ] # g=0,1,2 already done above return es[g - 3][n] else: raise ValueError("No Euler characteristics known for M_{g,n}," " where g=%s and n=%s" % (g, n))
def orbifold_euler_characteristics(g, n): """ Return the orbifold/virtual Euler characteristics of `M_{g,n}`, computed according to Harer-Zagier. """ if g == 0: return factorial(n - 3) * minus_one_exp(n - 3) elif g == 1: return Fraction(minus_one_exp(n), 12) * factorial(n - 1) else: # g > 1 return bernoulli(2 * g) * factorial(2 * g + n - 3) / ( factorial(2 * g - 2) * 2 * g) * minus_one_exp(n)
def euler_characteristics(g,n): """ Return Euler characteristics of `M_{g,n}`. The Euler characteristics is computed according to formulas and tables found in: * Bini-Gaiffi-Polito, arXiv:math/9806048, p.3 * Bini-Harer, arXiv:math/0506083, p. 10 """ if g==0: # according to Bini-Gaiffi-Polito arXiv:math/9806048, p.3 return factorial(n-3)*minus_one_exp(n-3) elif g==1: # according to Bini-Gaiffi-Polito, p. 15 if n>4: return factorial(n-1)*Fraction(minus_one_exp(n-1),12) else: es = [1,1,0,0] return es[n-1] # no n==0 computed in [BGP] elif g==2: # according to Bini-Gaiffi-Polito, p. 14 if n>6: return factorial(n+1)*Fraction(minus_one_exp(n+1),240) else: es = [1,2,2,0,-4,0,-24] return es[n] elif g>2: # according to Bini-Harer arXiv:math/0506083, p. 10 es = [# n==1 n==2 n==3 n==4 n==5 n==6 n==7 n==8 [ 8, 6, 4, -10, 30, -660, 6540, 79200], # g==3 [ -2, -10, -24, -24, -360, 2352, -37296, 501984], # g==4 [ 12, 26, 92, 182, 1674, -16716, 238980, -3961440], # g==5 [ 0, -46, -206, 188, -7512, 124296, -2068392, 37108656], # g==6 [ 38, 120, 676, -1862, 71866, -1058676, 21391644, -422727360], # g==7 [ -166, -630, -5362, 16108, -680616, 12234600, -259464240, 5719946400], # g==8 [ 748, 2132, 29632, -323546, 7462326, -164522628, 3771668220, -90553767840], # g==9 [-1994, 6078, -213066, 4673496, -106844744, 2559934440, -64133209320, 1.664e+12], # g==10 ] # g=0,1,2 already done above return es[g-3][n] else: raise ValueError("No Euler characteristics known for M_{g,n}," " where g=%s and n=%s" % (g,n))
def compute_homology(g, n): """ Compute homology ranks of the graph complex of `M_{g,n}`. Return array of homology ranks. """ timing.start("compute_homology(%d,%d)" % (g,n)) runtime.g = g runtime.n = n (G, D) = compute_graphs(g, n) logging.info("Stage III: Computing rank of homology modules ...") hs = list(reversed(D.compute_homology_ranks())) timing.stop("compute_homology(%d,%d)" % (g,n)) # compare orbifold Euler characteristics computed_chi = G.orbifold_euler_characteristics logging.info("Computed orbifold Euler characteristics: %s", computed_chi) expected_chi = orbifold_euler_characteristics(g,n) logging.info(" Expected orbifold Euler characteristics (according to Harer): %s", expected_chi) if computed_chi != expected_chi: logging.error("Expected and computed orbifold Euler characteristics do not match!" " (computed: %s, expected: %s)" % (computed_chi, expected_chi)) # compare Euler characteristics computed_e = 0 for i in xrange(len(hs)): computed_e += minus_one_exp(i)*hs[i] expected_e = euler_characteristics(g,n) logging.info("Computed Euler characteristics: %s" % computed_e) logging.info(" Expected Euler characteristics: %s" % expected_e) if computed_e != expected_e: logging.error("Computed and expected Euler characteristics do not match:" " %s vs %s" % (computed_e, expected_e)) # verify result against other known theorems if g>0: # from Harer's SLN1337, Theorem 7.1 if hs[1] != 0: logging.error("Harer's Theorem 7.1 requires h_1=0 when g>0") ## DISABLED 2009-03-27: Harer's statement seems to be incorrect, ## at least for low genus... ## # From Harer's SLN1337, Theorem 7.2 ## if g==1 or g==2: ## if hs[2] != n: ## logging.error("Harer's Theorem 7.2 requires h_2=%d when g=1 or g=2" % n) ## elif g>2: ## if hs[2] != n+1: ## logging.error("Harer's Theorem 7.2 requires h_2=%d when g>2" % n+1) return hs
def FatgraphComplex(g, n): """Return the fatgraph complex for given genus `g` and number of boundary components `n`. This is a factory method returning a `homology.ChainComplex` instance, populated with the correct vector spaces and differentials to compute the graph homology of the space `M_{g,n}`. """ ## Minimum number of edges is attained when there's only one ## vertex; so, by Euler's formula `V - E + n = 2 - 2*g`, we get: ## `E = 2*g + n - 1`. min_edges = 2 * g + n - 1 logging.debug(" Minimum number of edges: %d", min_edges) ## Maximum number of edges is reached in graphs with all vertices ## tri-valent, so, combining Euler's formula with `3*V = 2*E`, we ## get: `E = 6*g + 3*n - 6`. These are also graphs corresponding ## to top-dimensional cells. top_dimension = 6 * g + 3 * n - 6 logging.debug(" Maximum number of edges: %d", top_dimension) #: list of primitive graphs, graded by number of edges # generators = [ AggregateList() for dummy in xrange(top_dimension) ] C = MgnChainComplex(top_dimension) # gather graphs chi = Fraction(0) for graph in MgnGraphsIterator(g, n): grade = graph.num_edges - 1 pool = NumberedFatgraphPool(graph) # compute orbifold Euler characteristics (needs to include *all* graphs) chi += Fraction( minus_one_exp(grade - min_edges) * len(pool), pool.num_automorphisms) # discard non-orientable graphs if not pool.is_orientable: continue C.module[grade].aggregate(pool) C.orbifold_euler_characteristics = chi for i in xrange(top_dimension): logging.debug(" Initialized grade %d chain module (dimension %d)", i, len(C.module[i])) return C
def FatgraphComplex(g, n): """Return the fatgraph complex for given genus `g` and number of boundary components `n`. This is a factory method returning a `homology.ChainComplex` instance, populated with the correct vector spaces and differentials to compute the graph homology of the space `M_{g,n}`. """ ## Minimum number of edges is attained when there's only one ## vertex; so, by Euler's formula `V - E + n = 2 - 2*g`, we get: ## `E = 2*g + n - 1`. min_edges = 2*g + n - 1 logging.debug(" Minimum number of edges: %d", min_edges) ## Maximum number of edges is reached in graphs with all vertices ## tri-valent, so, combining Euler's formula with `3*V = 2*E`, we ## get: `E = 6*g + 3*n - 6`. These are also graphs corresponding ## to top-dimensional cells. top_dimension = 6*g + 3*n - 6 logging.debug(" Maximum number of edges: %d", top_dimension) #: list of primitive graphs, graded by number of edges #generators = [ AggregateList() for dummy in xrange(top_dimension) ] C = MgnChainComplex(top_dimension) # gather graphs chi = Fraction(0) for graph in MgnGraphsIterator(g,n): grade = graph.num_edges - 1 pool = NumberedFatgraphPool(graph) # compute orbifold Euler characteristics (needs to include *all* graphs) chi += Fraction(minus_one_exp(grade-min_edges)*len(pool), pool.num_automorphisms) # discard non-orientable graphs if not pool.is_orientable: continue C.module[grade].aggregate(pool) C.orbifold_euler_characteristics = chi for i in xrange(top_dimension): logging.debug(" Initialized grade %d chain module (dimension %d)", i, len(C.module[i])) return C
def facets(self, edge, other): """Iterate over facets obtained by contracting `edge` and projecting onto `other`. Each returned item is a triple `(j, k, s)`, where: - `j` is the index of a `NumberedFatgraph` in `self`; - `k` is the index of a `NumberedFatgraph` in `other`; - `s` is the sign by which `self[j].contract(edge)` projects onto `other[k]`. Only triples for which `s != 0` are returned. Examples:: >>> p0 = NumberedFatgraphPool(Fatgraph([Vertex([1, 2, 0, 1, 0]), Vertex([3, 3, 2])])) >>> p1 = NumberedFatgraphPool(Fatgraph([Vertex([0, 1, 0, 1, 2, 2])])) >>> list(NumberedFatgraphPool.facets(p0, 2, p1)) [(0, 0, 1), (1, 1, 1)] """ assert not self.graph.is_loop(edge) assert self.is_orientable assert other.is_orientable g0 = self.graph g1 = g0.contract(edge) g2 = other.graph assert len(g1.boundary_cycles) == len(g2.boundary_cycles) # compute isomorphism map `f1` from `g1` to `g2`: if there is # no such isomorphisms, then stop iteration (do this first so # then we do not waste time on computing if we need to abort # anyway) f1 = Fatgraph.isomorphisms(g1, g2).next() ## 1. compute map `phi0` induced on `g0.boundary_cycles` from the ## graph map `f0` which contracts `edge`. ## (e1, e2) = g0.endpoints(edge) assert set(g1.boundary_cycles) == set([g0.contract_boundary_cycle(bcy, e1, e2) for bcy in g0.boundary_cycles]), \ "NumberedFatgraphPool.facets():" \ " Boundary cycles of contracted graph are not the same" \ " as contracted boundary cycles of parent graph:" \ " `%s` vs `%s`" % (g1.boundary_cycles, [g0.contract_boundary_cycle(bcy, e1, e2) for bcy in g0.boundary_cycles]) phi0_inv = Permutation((i1, i0) for (i0, i1) in enumerate( g1.boundary_cycles.index(g0.contract_boundary_cycle(bc0, e1, e2)) for bc0 in g0.boundary_cycles)) ## 2. compute map `phi1` induced by isomorphism map `f1` on ## the boundary cycles of `g1` and `g2`. ## phi1_inv = Permutation((i1, i0) for (i0, i1) in enumerate( g2.boundary_cycles.index(f1.transform_boundary_cycle(bc1)) for bc1 in g1.boundary_cycles)) assert len(phi1_inv) == len(g1.boundary_cycles) assert len(phi1_inv) == len(g2.boundary_cycles) ## 3. Compute the composite map `f1^(-1) * f0`. ## ## For every numbering `nb` on `g0`, compute the (index of) ## corresponding numbering on `g2` (under the composition map ## `f1^(-1) * f0`) and return a triple `(index of nb, index of ## push-forward, sign)`. ## ## In the following: ## ## - `j` is the index of a numbering `nb` in `self.numberings`; ## - `k` is the index of the corresponding numbering in `other.numberings`, ## under the composition map `f1^(-1) * f0`; ## - `a` is the the unique automorphism `a` of `other.graph` such that:: ## ## self.numberings[j] = pull_back(<permutation induced by `a` applied to> other.numberings[k]) ## ## - `s` is the pull-back sign (see below). ## ## The pair `k`,`a` is computed using the ## `NumberedFatgraphPool._index` (which see), applied to each ## of `self.numberings`, rearranged according to the ## permutation of boundary cycles induced by `f1^(-1) * f0`. ## for (j, (k, a)) in enumerate( other._index(phi1_inv.rearranged(phi0_inv.rearranged(nb))) for nb in self.numberings): ## there are three components to the sign `s`: ## - the sign given by the ismorphism `f1` ## - the sign of the automorphism of `g2` that transforms the ## push-forward numbering into the chosen representative in the same orbit ## - the alternating sign from the homology differential s = f1.compare_orientations() \ * a.compare_orientations() \ * minus_one_exp(g0.edge_numbering[edge]) yield (j, k, s)
def facets(self, edge, other): """Iterate over facets obtained by contracting `edge` and projecting onto `other`. Each returned item is a triple `(j, k, s)`, where: - `j` is the index of a `NumberedFatgraph` in `self`; - `k` is the index of a `NumberedFatgraph` in `other`; - `s` is the sign by which `self[j].contract(edge)` projects onto `other[k]`. Only triples for which `s != 0` are returned. Examples:: >>> p0 = NumberedFatgraphPool(Fatgraph([Vertex([1, 2, 0, 1, 0]), Vertex([3, 3, 2])])) >>> p1 = NumberedFatgraphPool(Fatgraph([Vertex([0, 1, 0, 1, 2, 2])])) >>> list(NumberedFatgraphPool.facets(p0, 2, p1)) [(0, 0, 1), (1, 1, 1)] """ assert not self.graph.is_loop(edge) assert self.is_orientable assert other.is_orientable g0 = self.graph g1 = g0.contract(edge) g2 = other.graph assert len(g1.boundary_cycles) == len(g2.boundary_cycles) # compute isomorphism map `f1` from `g1` to `g2`: if there is # no such isomorphisms, then stop iteration (do this first so # then we do not waste time on computing if we need to abort # anyway) f1 = Fatgraph.isomorphisms(g1,g2).next() ## 1. compute map `phi0` induced on `g0.boundary_cycles` from the ## graph map `f0` which contracts `edge`. ## (e1, e2) = g0.endpoints(edge) assert set(g1.boundary_cycles) == set([ g0.contract_boundary_cycle(bcy, e1, e2) for bcy in g0.boundary_cycles ]), \ "NumberedFatgraphPool.facets():" \ " Boundary cycles of contracted graph are not the same" \ " as contracted boundary cycles of parent graph:" \ " `%s` vs `%s`" % (g1.boundary_cycles, [ g0.contract_boundary_cycle(bcy, e1, e2) for bcy in g0.boundary_cycles ]) phi0_inv = Permutation((i1,i0) for (i0,i1) in enumerate( g1.boundary_cycles.index(g0.contract_boundary_cycle(bc0, e1, e2)) for bc0 in g0.boundary_cycles )) ## 2. compute map `phi1` induced by isomorphism map `f1` on ## the boundary cycles of `g1` and `g2`. ## phi1_inv = Permutation((i1,i0) for (i0,i1) in enumerate( g2.boundary_cycles.index(f1.transform_boundary_cycle(bc1)) for bc1 in g1.boundary_cycles )) assert len(phi1_inv) == len(g1.boundary_cycles) assert len(phi1_inv) == len(g2.boundary_cycles) ## 3. Compute the composite map `f1^(-1) * f0`. ## ## For every numbering `nb` on `g0`, compute the (index of) ## corresponding numbering on `g2` (under the composition map ## `f1^(-1) * f0`) and return a triple `(index of nb, index of ## push-forward, sign)`. ## ## In the following: ## ## - `j` is the index of a numbering `nb` in `self.numberings`; ## - `k` is the index of the corresponding numbering in `other.numberings`, ## under the composition map `f1^(-1) * f0`; ## - `a` is the the unique automorphism `a` of `other.graph` such that:: ## ## self.numberings[j] = pull_back(<permutation induced by `a` applied to> other.numberings[k]) ## ## - `s` is the pull-back sign (see below). ## ## The pair `k`,`a` is computed using the ## `NumberedFatgraphPool._index` (which see), applied to each ## of `self.numberings`, rearranged according to the ## permutation of boundary cycles induced by `f1^(-1) * f0`. ## for (j, (k, a)) in enumerate(other._index(phi1_inv.rearranged(phi0_inv.rearranged(nb))) for nb in self.numberings): ## there are three components to the sign `s`: ## - the sign given by the ismorphism `f1` ## - the sign of the automorphism of `g2` that transforms the ## push-forward numbering into the chosen representative in the same orbit ## - the alternating sign from the homology differential s = f1.compare_orientations() \ * a.compare_orientations() \ * minus_one_exp(g0.edge_numbering[edge]) yield (j, k, s)