From 83dec060838498be75a4c91acafbe4687596aed5 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 09:20:34 +0200 Subject: [PATCH 01/17] implement max_size for fat graph list --- .../fat_graph_exhaustive_generation.py | 113 ++++++++++++------ 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/surface_dynamics/topology/fat_graph_exhaustive_generation.py b/surface_dynamics/topology/fat_graph_exhaustive_generation.py index 17f4fd77..294191b0 100644 --- a/surface_dynamics/topology/fat_graph_exhaustive_generation.py +++ b/surface_dynamics/topology/fat_graph_exhaustive_generation.py @@ -78,14 +78,15 @@ def augment1(cm, aut_grp, g, callback): aut_grp.reset_iterator() R = aut_grp + do_continue = True if cm._n == 0: cm._set_genus1_square() aaut_grp = cm.automorphism_group() - callback('augment1', True, cm, aaut_grp, g - 1) - if g > 1: - augment1(cm, aaut_grp, g - 1, callback) + do_continue = callback('augment1', True, cm, aaut_grp, g - 1) + if do_continue and g > 1: + do_continue = augment1(cm, aaut_grp, g - 1, callback) cm.remove_face_trisection(n) - return + return do_continue for i in R: j = i @@ -94,15 +95,18 @@ def augment1(cm, aut_grp, g, callback): for sk in range(fd[fl[i]] - sj + (i != j)): cm.trisect_face(i, j, k) test, aaut_grp = cm._is_canonical(n) - callback('augment1', test, cm, aaut_grp, g - 1) - if test and g > 1: - augment1(cm, aaut_grp, g - 1, callback) - + do_continue = callback('augment1', test, cm, aaut_grp, g - 1) + if do_continue and test and g > 1: + do_continue = augment1(cm, aaut_grp, g - 1, callback) cm.remove_face_trisection(n) + if not do_continue: + return False k = fp[k] j = fp[j] i = fp[i] + return True + # augment2: face split # (essentially the same as augment3) @@ -122,15 +126,16 @@ def augment2(cm, aut_grp, depth, callback): fd = cm._fd fl = cm._fl + do_continue = True if cm._n == 0: # trivial map -> loop (1 vertex, 2 faces) cm._set_genus0_loop() aaut_grp = cm.automorphism_group() - callback('augment2', True, cm, aaut_grp, depth - 1) - if depth > 1: - augment2(cm, aaut_grp, depth - 1, callback) + do_continue = callback('augment2', True, cm, aaut_grp, depth - 1) + if do_continue and depth > 1: + do_continue = augment2(cm, aaut_grp, depth - 1, callback) cm.remove_edge(0) - return + return do_continue if aut_grp is None: R = range(n) @@ -166,12 +171,16 @@ def augment2(cm, aut_grp, depth, callback): for _ in range(niter): cm.split_face(i, j) test, aaut_grp = cm._is_canonical(n) - callback('augment2', test, cm, aaut_grp, depth - 1) - if test and depth > 1: - augment2(cm, aaut_grp, depth - 1, callback) + do_continue = callback('augment2', test, cm, aaut_grp, depth - 1) + if do_continue and test and depth > 1: + do_continue = augment2(cm, aaut_grp, depth - 1, callback) cm.remove_edge(n) + if not do_continue: + return False j = fp[j] + return True + # augment3: vertex split def augment3(cm, aut_grp, depth, min_degree, callback): @@ -192,15 +201,16 @@ def augment3(cm, aut_grp, depth, min_degree, callback): vd = cm._vd vl = cm._vl + do_continue = True if cm._n == 0: # trivial map -> edge (2 vertices, 1 face) cm._set_genus0_edge() aaut_grp = cm.automorphism_group() - callback('augment3', True, cm, aaut_grp, depth - 1) - if depth > 1: - augment3(cm, aaut_grp, depth - 1, min_degree, callback) + do_continue = callback('augment3', True, cm, aaut_grp, depth - 1) + if do_continue and depth > 1: + do_continue = augment3(cm, aaut_grp, depth - 1, min_degree, callback) cm.contract_edge(0) - return + return do_continue if aut_grp is None: R = range(n) @@ -241,12 +251,16 @@ def augment3(cm, aut_grp, depth, min_degree, callback): assert vd[vl[i]] >= min_degree_loc assert vd[vl[j]] >= min_degree_loc test, aaut_grp = cm._is_canonical(n) - callback('augment3', test, cm, aaut_grp, depth - 1) - if test and depth > 1: - augment3(cm, aaut_grp, depth - 1, min_degree, callback) + do_continue = callback('augment3', test, cm, aaut_grp, depth - 1) + if do_continue and test and depth > 1: + do_continue = augment3(cm, aaut_grp, depth - 1, min_degree, callback) cm.contract_edge(n) + if not do_continue: + return False j = vp[j] + return True + # TODO # def augment4(cm): # r""" @@ -294,20 +308,22 @@ def __ne__(self, other): def __call__(self, cm, aut): self.count += ZZ(1) self.weighted_count += QQ((1, (1 if aut is None else aut.group_cardinality()))) + return True # callback to list elements class ListCallback: - def __init__(self, mutable=False): + def __init__(self, mutable=False, max_size=None): self._list = [] self._mutable = mutable + self._max_size = max_size def __call__(self, cm, aut): self._list.append(cm.copy(self._mutable)) + return self._max_size is None or len(self._list) < self._max_size def list(self): return self._list - # TODO: make it work again! This is the most precious piece of information # to enhance the exhaustive generation... # Callback for getting a full trace of the execution @@ -389,7 +405,7 @@ def summary(self, filename=None): f.close() def __call__(self, cm, aut): - pass + return True def add_vertex(self, s, aut_grp, depth): if self._verbosity >= 1: @@ -516,7 +532,8 @@ def __call__(self, caller, test, cm, aut, depth): nf >= self._fmin and \ (self._vertex_min_degree <= 1 or all(d >= self._vertex_min_degree for d in cm.vertex_degrees())) and \ (self._filter is None or self._filter(cm, aut)): - self._callback(cm, aut) + if not self._callback(cm, aut): + return False if caller == 'augment1': # augment1 creates fat graphs with a single vertex and a single face. @@ -526,12 +543,14 @@ def __call__(self, caller, test, cm, aut, depth): # more faces? nfdepth = min(self._fmax - 2, self._emax - ne - 1) if nfdepth: - augment2(cm, aut, nfdepth, self) + if not augment2(cm, aut, nfdepth, self): + return False # more vertices? if nf >= self._fmin: nvdepth = min(self._vmax - 2, self._emax - ne - 1) if nvdepth: - augment3(cm, aut, nvdepth, max(1, self._vertex_min_degree), self) + if not augment3(cm, aut, nvdepth, max(1, self._vertex_min_degree), self): + return False elif caller == 'augment2': # augment2 performs face splitting @@ -540,7 +559,8 @@ def __call__(self, caller, test, cm, aut, depth): # more vertices? nvdepth = min(self._vmax - 2, self._emax - ne - 1) if nvdepth: - augment3(cm, aut, nvdepth, max(1, self._vertex_min_degree), self) + if not augment3(cm, aut, nvdepth, max(1, self._vertex_min_degree), self): + return False elif caller == 'augment3': pass @@ -548,6 +568,8 @@ def __call__(self, caller, test, cm, aut, depth): else: raise RuntimeError('unknown caller') + return True + def run(self): # trivial map (g = 0, nv = 1, nf = 1) cm = FatGraph('()', '()', mutable=True) @@ -560,17 +582,21 @@ def run(self): self._fmin <= nf < self._fmax and self._gmin <= g < self._gmax and self._vertex_min_degree == 0 and (self._filter is None or self._filter(cm, aut))): - self._callback(cm, None) + if not self._callback(cm, None): + return cm._realloc(2 * self._emax - 2) if self._gmax > 1: - augment1(cm, None, self._gmax - 1, self) + if not augment1(cm, None, self._gmax - 1, self): + return if self._gmin == 0 and self._fmax > 2: assert cm._n == 0, cm - augment2(cm, None, self._fmax - 2, self) + if not augment2(cm, None, self._fmax - 2, self): + return if self._gmin == 0 and self._fmin == 1 and self._vmax > 2: assert cm._n == 0, cm - augment3(cm, None, self._vmax - 2, max(1, self._vertex_min_degree), self) + if not augment3(cm, None, self._vmax - 2, max(1, self._vertex_min_degree), self): + return ############## # Main class # @@ -602,6 +628,7 @@ class FatGraphs: ....: p = [cm.face_degrees()] ....: aut_card = 1 if aut is None else aut.group_cardinality() ....: poly += 2*cm.num_edges() // aut_card * t**cm.num_edges() + ....: return True sage: F.map_reduce(update) sage: poly 208494*t^7 + 24057*t^6 + 2916*t^5 + 378*t^4 + 54*t^3 + 9*t^2 + 2*t @@ -678,6 +705,7 @@ class FatGraphs: ....: cm.num_faces() < 2 or \ ....: cm.num_faces() >= 4: ....: raise ValueError(str(cm)) + ....: return True sage: F.map_reduce(check) sage: for nf in [2,3]: ....: for nv in [2,3]: @@ -888,7 +916,10 @@ def map_reduce(self, callback, filter=None): EXAMPLES:: sage: from surface_dynamics import FatGraphs - sage: FatGraphs(g=1, nf=2, nv=2).map_reduce(lambda x,y: print(x)) + sage: def my_callback(cm, aut): + ....: print(cm) + ....: return True + sage: FatGraphs(g=1, nf=2, nv=2).map_reduce(my_callback) FatGraph('(0,6,5,4,2,1,3)(7)', '(0,2,1,3,4,6,7)(5)') FatGraph('(0,6,2,1,3)(4,7,5)', '(0,2,1,3,6,4,7)(5)') FatGraph('(0,5,4,2,1,6,3)(7)', '(0,2,6,7,1,3,4)(5)') @@ -933,7 +964,7 @@ def weighted_cardinality(self, filter=None): self.map_reduce(N, filter) return N[1] - def list(self): + def list(self, max_size=None): r""" EXAMPLES:: @@ -949,8 +980,18 @@ def list(self): 1 sage: L12[0].num_vertices() 2 + + sage: FatGraphs(g=0, ne=6).list(max_size=3) + [FatGraph('(0,10,1)(2,5)(3)(4,7)(6,9)(8,11)', '(0,10,8,6,4,2,3,5,7,9,11)(1)'), + FatGraph('(0,11)(1,10,8)(2,5)(3)(4,7)(6,9)', '(0,8,6,4,2,3,5,7,9,10)(1,11)'), + FatGraph('(0,9)(1,10,6)(2,5)(3)(4,7)(8,11)', '(0,6,4,2,3,5,7,10,8)(1,9,11)')] + + sage: FatGraphs(g=2, nv=5, nf=7).list(max_size=3) + [FatGraph('(0,26,19,18,17,16,15,14,13,12,11,10,9,8,6,5,7,4,2,1,3)(20,23)(21)(22,25)(24,27)', '(0,2,1,3,4,6,5,7,8,10,12,14,16,18,26,24,22,20,21,23,25,27)(9)(11)(13)(15)(17)(19)'), + FatGraph('(0,26,17,16,15,14,13,12,11,10,9,8,6,5,7,4,2,1,3)(18,27,24,19)(20,23)(21)(22,25)', '(0,2,1,3,4,6,5,7,8,10,12,14,16,26,18,24,22,20,21,23,25,27)(9)(11)(13)(15)(17)(19)'), + FatGraph('(0,26,15,14,13,12,11,10,9,8,6,5,7,4,2,1,3)(16,27,24,19,18,17)(20,23)(21)(22,25)', '(0,2,1,3,4,6,5,7,8,10,12,14,26,16,18,24,22,20,21,23,25,27)(9)(11)(13)(15)(17)(19)')] """ - L = ListCallback() + L = ListCallback(mutable=False, max_size=max_size) self.map_reduce(L) return L.list() From cddd9c635e97eb1aa4510d5ea469b12f99772b17 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 09:22:39 +0200 Subject: [PATCH 02/17] symmetrization of multivariate generating series --- surface_dynamics/misc/factored_denominator.py | 37 ++++++++-- ...licative_multivariate_generating_series.py | 71 ++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/surface_dynamics/misc/factored_denominator.py b/surface_dynamics/misc/factored_denominator.py index 8cd39577..3d592d56 100644 --- a/surface_dynamics/misc/factored_denominator.py +++ b/surface_dynamics/misc/factored_denominator.py @@ -187,10 +187,10 @@ def __init__(self, data, V=None): elif isinstance(data, dict): if V is not None: self._dict = {} - for k, v in data.items(): - k = V(k) - k.set_immutable() - self._dict[k] = v + for vect, mult in data.items(): + vect = V(vect) + vect.set_immutable() + self._dict[vect] = mult else: self._dict = data @@ -211,6 +211,34 @@ def __init__(self, data, V=None): self._tuple = tuple(sorted(self._dict.items())) + def permutation_action(self, p, V): + r""" + Right action of the permutation ``p`` + + EXAMPLES:: + + sage: from surface_dynamics.misc.factored_denominator import FactoredDenominator + sage: V = ZZ**4 + sage: f1 = FactoredDenominator([((1,0,0,0), 2)], V) + sage: f1.permutation_action([1, 2, 3, 0], V) + {(0, 1, 0, 0): 2} + sage: f2 = FactoredDenominator([((0,1,2,3), 3), ((1,0,1,1), 1)], V) + sage: f2.permutation_action([2,0,1,3], V) + {(1, 2, 0, 3): 3, (0, 1, 1, 1): 1} + """ + new_dict = {} + for vect, mult in self._dict.items(): + new_vect = V() + for i, x in vect.items(): + new_vect[p[i]] = x + new_vect.set_immutable() + new_dict[new_vect] = mult + + ans = FactoredDenominator.__new__(FactoredDenominator) + ans._dict = new_dict + ans._tuple = tuple(sorted(new_dict.items())) + return ans + def __len__(self): r""" Return the number of factors (without multiplicities). @@ -1101,3 +1129,4 @@ def _element_constructor_(self, arg): """ num = self._polynomial_ring(arg) return self.element_class(self, [([], num)], self.free_module()) + diff --git a/surface_dynamics/misc/multiplicative_multivariate_generating_series.py b/surface_dynamics/misc/multiplicative_multivariate_generating_series.py index 41a57918..39239a34 100644 --- a/surface_dynamics/misc/multiplicative_multivariate_generating_series.py +++ b/surface_dynamics/misc/multiplicative_multivariate_generating_series.py @@ -121,6 +121,25 @@ def parse_latte_generating_series(M, s): return m +def mpoly_permutation_action(poly, perm): + r""" + TESTS:: + + sage: from surface_dynamics.misc.multiplicative_multivariate_generating_series import mpoly_permutation_action + sage: R. = QQ[] + sage: mpoly_permutation_action(x0 + 2 * x1^2 + x2 * x3, [1, 2, 3, 0]) + 2*x2^2 + x0*x3 + x1 + """ + new_dict = {} + for exp, coeff in poly.dict().items(): + new_exp = [0] * len(exp) + for i, x in zip(exp.nonzero_positions(), exp.nonzero_values()): + new_exp[perm[i]] = x + new_dict[tuple(new_exp)] = coeff + + return poly.parent()(new_dict) + + class MultiplicativeMultivariateGeneratingSeries(AbstractMSum): def _den_str(self, den): var_names = self.parent().polynomial_ring().variable_names() @@ -553,7 +572,7 @@ def factor(self): P = L.polynomial_ring() N = P(N) gt = False - for mon,mult in list(D._dict.items()): + for mon, mult in list(D._dict.items()): pmon = P.one() - P.monomial(*mon) q,r = N.quo_rem(pmon) while mult and not r: @@ -576,6 +595,56 @@ def as_symbolic(self): from sage.symbolic.ring import SR return SR(str(self)) + def permutation_action(self, p): + r""" + EXAMPLES:: + + sage: from surface_dynamics.misc.multiplicative_multivariate_generating_series import MultiplicativeMultivariateGeneratingSeriesRing + sage: M = MultiplicativeMultivariateGeneratingSeriesRing('x', 3) + sage: x = M.polynomial_ring().gens() + + sage: f = M.term(x[1], [([1,0,0],1)]) + sage: f + (x1)/((1 - x0)) + sage: f.permutation_action([2, 0, 1]) + (x0)/((1 - x2)) + + sage: f = M.term(1, [([1,3,0],1), ([1,0,-1],1)]) + M.term(1, [([1,1,0],1), ([1,0,-1],2)]) + sage: f + (1)/((1 - x0*x2^-1)*(1 - x0*x1^3)) + (1)/((1 - x0*x2^-1)^2*(1 - x0*x1)) + sage: f.permutation_action([2, 1, 0]) + (1)/((1 - x0^-1*x2)*(1 - x1^3*x2)) + (1)/((1 - x0^-1*x2)^2*(1 - x1*x2)) + """ + M = self.parent() + ans = M.zero() + for den, num in self._data.items(): + new_num = mpoly_permutation_action(num, p) + new_den = den.permutation_action(p, M.free_module()) + ans += M.term(new_num, new_den) + return ans + + def symmetrization(self): + r""" + Return the symmetrization + + EXAMPLES:: + + sage: from surface_dynamics.misc.multiplicative_multivariate_generating_series import MultiplicativeMultivariateGeneratingSeriesRing + sage: M = MultiplicativeMultivariateGeneratingSeriesRing('x', 3) + + sage: f = M.term(1, [([1,0,0],1)]) + sage: f.symmetrization() + (2)/((1 - x2)) + (2)/((1 - x1)) + (2)/((1 - x0)) + """ + import itertools + + M = self.parent() + V = M.free_module() + ans = M.zero() + for p in itertools.permutations(range(M.ngens())): + ans += self.permutation_action(p) + return ans + # TODO: this should actually be an algebra over QQ[x0, x1, ..., xn] # TODO: we should have two versions, as an algebra over the polynomial From f1a193560923236b3ceb0c3a818e7a3d4487e6da Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 09:21:45 +0200 Subject: [PATCH 03/17] homology of fat graph --- surface_dynamics/topology/fat_graph.py | 513 ++++++++++++++++++++++++- surface_dynamics/topology/homology.py | 454 ++++++++++++++++++++++ 2 files changed, 949 insertions(+), 18 deletions(-) create mode 100644 surface_dynamics/topology/homology.py diff --git a/surface_dynamics/topology/fat_graph.py b/surface_dynamics/topology/fat_graph.py index 2a5010cd..c5a895f8 100644 --- a/surface_dynamics/topology/fat_graph.py +++ b/surface_dynamics/topology/fat_graph.py @@ -106,10 +106,10 @@ class FatGraph(object): '_vp', # vertex permutation (array of length _n) '_fp', # face permutation (array of length _n) # TODO: think whether it is useful to keep any of these... - # labels (identify uniquely the vertices) + # labels (identify uniquely vertices and faces) '_vl', # vertex labels (array of length _n) '_fl', # face labels (array of length _n) - # numbers + # numbers (= length of _vl and _fl) '_nv', # number of vertices (non-negative integer) '_nf', # number of faces (non-negative integer) # degrees @@ -121,7 +121,7 @@ def __init__(self, vp=None, fp=None, max_num_dart=None, mutable=False, check=Tru self._vp = vp self._fp = fp if len(vp) != len(fp): - raise ValueError("invalid permutations") + raise ValueError("invalid permutations vp={} fp={}".format(vp, fp)) self._n = len(vp) # number of darts self._nf = 0 # number of faces @@ -171,7 +171,7 @@ def from_unicellular_word(w): # twins get labelled 1, 3, 5, ... fp[previous] = previous = 2*k + 1 else: - raise ValueError('invalid unicellular word') + raise ValueError('invalid unicellular word w={}'.format(w)) # consistency check assert previous == 2 * w[-1] + 1 @@ -179,7 +179,7 @@ def from_unicellular_word(w): return FatGraph(fp=fp) @staticmethod - def from_string(s): + def from_string(s, mutable=False): r""" Build a fat graph from a serialized string. @@ -196,12 +196,12 @@ def from_string(s): FatGraph('()', '()') """ if not isinstance(s, str) or s.count('_') != 2: - raise ValueError("invalid input") + raise ValueError("invalid input s={!r}".format(s)) n, vp, fp = s.split('_') n = int(n) vp = perm_from_base64_str(vp, n) fp = perm_from_base64_str(fp, n) - return FatGraph(vp, fp) + return FatGraph(vp, fp, mutable=mutable) def _check(self, error=RuntimeError): vp = self._vp @@ -852,33 +852,464 @@ def euler_characteristic(self): """ return self._nf - self._n // 2 + self._nv - # TODO: fix your mind about the meaning of dual!!! + def tree_cotree_decomposition(self, vertex_root=0, face_root=0): + r""" + Return a tree-cotree decomposition of this graph. + + A tree-cotree decomposition of a fat graph is a decomposition of its + into three subsets ``(t, d, r)`` where + - ``t``: is a tree + - ``d``: is a tree of the dual graph + - ``r``: are the remaining edges + + Their sizes are respectively the number of vertices minus one, the + number of faces minus one and twice the genus. + + The tree and cotree in this function are encoded as lists of half-edges + directed toward the root. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + + A genus 0, genus 1 and genus 2 examples with 3 vertices and 3 faces:: + + sage: fg = FatGraph('(0,6,3,2,1,4)(5)(7)', '(0,2,6,7)(1,4,5)(3)') + sage: fg.tree_cotree_decomposition() + ((-1, 5, 7), (-1, 1, 3), ()) + sage: fg.tree_cotree_decomposition(vertex_root=1, face_root=2) + ((4, -1, 7), (2, 1, -1), ()) + sage: fg.tree_cotree_decomposition(vertex_root=1, face_root=0) + ((4, -1, 7), (-1, 1, 3), ()) + + sage: fg = FatGraph('(0,6,5,2,7,10,3,4)(1,8,11)(9)', '(0,11,7)(1,4,6,2,10,8,9)(3,5)') + sage: fg.tree_cotree_decomposition() + ((-1, 1, 9), (-1, 10, 5), (2, 6)) + sage: fg.tree_cotree_decomposition(vertex_root=1, face_root=0) + ((0, -1, 9), (-1, 10, 5), (2, 6)) + + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.tree_cotree_decomposition() + ((-1, 1, 11), (-1, 3, 15), (4, 6, 8, 12)) + sage: fg.tree_cotree_decomposition(vertex_root=0, face_root=2) + ((-1, 1, 11), (7, 3, -1), (4, 8, 12, 14)) + + TESTS:: + + sage: def tree_to_root(fg, tree, v): + ....: seen = [0] * fg._nv + ....: while tree[v] != -1: + ....: if seen[v]: + ....: raise ValueError + ....: seen[v] = 1 + ....: assert fg._vl[tree[v]] == v + ....: v = fg._vl[fg.edge_flip(tree[v])] + ....: return True + sage: def cotree_to_root(fg, cotree, f): + ....: seen = [0] * fg._nf + ....: while cotree[f] != -1: + ....: if seen[f]: + ....: raise ValueError + ....: seen[f] = 1 + ....: assert fg._fl[cotree[f]] == f + ....: f = fg._fl[fg.edge_flip(cotree[f])] + ....: return True + sage: fg = FatGraph('(0,6,5,2,7,10,3,4)(1,8,11)(9)', '(0,11,7)(1,4,6,2,10,8,9)(3,5)') + sage: tree, cotree, remaining_edges = fg.tree_cotree_decomposition() + sage: assert all(tree_to_root(fg, tree, v) for v in range(fg.num_vertices())) + sage: assert all(cotree_to_root(fg, cotree, f) for f in range(fg.num_faces())) + """ + vp = self._vp[:self._n] # copy (that will be modified) + fp = self._fp[:self._n] # copy (that will be modified) + + vertices, vdegs = perm_dense_cycles(vp, self._n) + faces, fdegs = perm_dense_cycles(fp, self._n) + nv = len(vdegs) + nf = len(fdegs) + v_reps = [-1] * nv # half-edge representatives for vertices + f_reps = [-1] * nf # half-edge representatives for faces + for i in range(self._n): + vi = vertices[i] + if v_reps[vi] == -1: + v_reps[vi] = i + fi = faces[i] + if f_reps[fi] == -1: + f_reps[fi] = i + + seen_vertices = [0] * nv + seen_faces = [0] * nf + tree = [-1] * nv # half-edge to follow toward the root + cotree = [-1] * nf # half-edge to follow toward the root + used_darts = [0] * self._n + + todo = [vertex_root] + seen_vertices[vertex_root] = 1 + while todo: + v = todo.pop() + for j in perm_orbit(vp, v_reps[v]): + jj = j ^ 1 + vv = vertices[jj] + if not seen_vertices[vv]: + tree[vv] = jj + seen_vertices[vv] = 1 + used_darts[j] = used_darts[jj] = 1 + todo.append(vv) + continue + assert sum(i != -1 for i in tree) == nv - 1 + + todo = [face_root] + seen_faces[face_root] = 1 + while todo: + f = todo.pop() + for j in perm_orbit(fp, f_reps[f]): + if used_darts[j]: + continue + jj = j ^ 1 + ff = faces[jj] + if not seen_faces[ff]: + cotree[ff] = jj + seen_faces[ff] = 1 + todo.append(ff) + used_darts[j] = used_darts[jj] = 1 + continue + assert sum(i != -1 for i in cotree) == nf - 1 + + return tuple(tree), tuple(cotree), tuple(i for i in range(0, self._n, 2) if not used_darts[i]) + + def is_path(self, path, check=True): + if check: + if not isinstance(path, (tuple, list)): + raise TypeError + path = [self._check_dart(e) for e in path] + for i in range(len(path) - 1): + current_end = self._vl[path[i] ^ 1] + next_start = self._vl[path[i + 1]] + if current_end != next_start: + return False + return True + + def is_closed_path(self, path, check=True): + if check: + if not isinstance(path, (tuple, list)): + raise TypeError + path = [self._check_dart(e) for e in path] + for i in range(len(path)): + current_end = self._vl[path[i] ^ 1] + next_start = self._vl[path[(i + 1) % len(path)]] + if current_end != next_start: + return False + return True + + + def homology(self, base_ring=None, tree_cotree_decomposition=None): + r""" + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.basis() + ((-1, 0, 1, 0, 0, 1, 0, 0), + (1, 0, 0, 1, 0, -1, 0, 0), + (0, 0, 0, 0, 1, 0, 0, 0), + (1, 0, 0, 0, 0, 0, 1, 0)) + """ + from .homology import FatGraphAbsoluteHomology + if base_ring is None: + from sage.rings.integer_ring import ZZ + base_ring = ZZ + if tree_cotree_decomposition is None: + tree_cotree_decomposition = self.tree_cotree_decomposition() + return FatGraphAbsoluteHomology(self, base_ring, tree_cotree_decomposition) + + def cycle_basis(self, intersection=False, tree_cotree_decomposition=None): + r""" + Return a basis of cycles of the fundamental group on the graph. + + Each element of the basis is given as a sequence of half-edges. All basis + element is a simple path on the underlying graph (ie not passing + twice through the same vertex). + + INPUT: + + - ``intersection`` -- optional boolean (default: ``False``) -- if set + to ``True`` also return the algebraic intersection pairing on this + basis. + + - ``tree_cotree_decomposition`` -- optional tree-cotree decomposition (default: + ``None``) - if provided, use the basis obtained from this tree-cotree + decomposition + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + + A genus 0, genus 1 and genus 2 examples with 3 vertices and 3 faces:: + + sage: fg = FatGraph('(0,6,3,2,1,4)(5)(7)', '(0,2,6,7)(1,4,5)(3)') + sage: fg.cycle_basis() + [] + sage: fg.cycle_basis(True) + ([], []) + + sage: fg = FatGraph('(0,6,5,2,7,10,3,4)(1,8,11)(9)', '(0,11,7)(1,4,6,2,10,8,9)(3,5)') + sage: fg.cycle_basis() + [[2], [6]] + sage: fg.cycle_basis(True) + ( + [ 0 -1] + [[2], [6]], [ 1 0] + ) + + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.cycle_basis() + [[10, 4, 1], [0, 6, 11], [8], [0, 12]] + sage: fg.cycle_basis(True) + ( + [ 0 1 1 0] + [-1 0 0 0] + [-1 0 0 1] + [[10, 4, 1], [0, 6, 11], [8], [0, 12]], [ 0 0 -1 0] + ) + sage: fg.cycle_basis(True)[1].det() # must be 1 or -1 + 1 + """ + if tree_cotree_decomposition is None: + tree_cotree_decomposition = self.tree_cotree_decomposition() + + tree, cotree, remaining_edges = tree_cotree_decomposition + d = len(remaining_edges) + + vp = self._vp + vertices, vdegs = perm_dense_cycles(vp, self._n) + nv = len(vdegs) + basis = [] + for e in remaining_edges: + u = vertices[e] + v = vertices[e ^ 1] + + # compute path from u toward the root + pu = [] + while tree[u] != -1: + pu.append(tree[u]) + assert vertices[tree[u]] == u + u = vertices[tree[u] ^ 1] + + # compute path from v toward the root + pv = [] + while tree[v] != -1: + pv.append(tree[v]) + assert vertices[tree[v]] == v + v = vertices[tree[v] ^ 1] + + # add pu^-1 e pv to our basis + while pu and pv and pu[-1] == pv[-1]: + pu.pop() + pv.pop() + path = [i ^ 1 for i in reversed(pu)] + [e] + pv + basis.append(path) + + I = None + if intersection: + # After contraction of the tree/cotree we get a bouquet of circles + # made of remaining_edges. We compute the associated ordering on + # them. + vp = self._vp + ordering = [-1] * self._n + # ordering encodes for each half-edge + # * -1 for remaining half-edge (will be modified to 0, 1, 2, ...) + # * -2 for half-edge in tree + # * -3 for half-edge in cotree + for i in tree: + if i != -1: + ordering[i] = ordering[i ^ 1] = -2 + for i in cotree: + if i != - 1: + ordering[i] = ordering[i ^ 1] = -3 + if d: + i = i0 = remaining_edges[0] + assert ordering[i] == -1, (ordering, i) + ordering[i] = 0 + r = 1 + i = vp[i] + while i != i0: + if ordering[i] == -2: + # in tree, edges are contracted + i = vp[i ^ 1] + continue + elif ordering[i] == -3: + # in cotree, edges are removed + i = vp[i] + continue + else: + # a remaining edge + assert ordering[i] == -1, ordering + ordering[i] = r + i = vp[i] + r += 1 + + from sage.matrix.constructor import matrix + I = matrix(ZZ, d) + for i in range(1, d): + ei = remaining_edges[i] + p_in = ordering[ei] + p_out = (ordering[ei ^ 1] - p_in) % (2 * d) + for j in range(i): + ej = remaining_edges[j] + q_in = (ordering[ej] - p_in) % (2 * d) + q_out = (ordering[ej ^ 1] - p_in) % (2 * d) + if q_in < p_out and p_out < q_out: + I[i,j] = 1 + I[j,i] = -1 + elif q_out < p_out and p_out < q_in: + I[i,j] = -1 + I[j,i] = 1 + + return (basis, I) if intersection else basis + + def angles(self, verticals): + r""" + Return the angles (as multiple of pi). + + EXAMPLES:: + + sage: from surface_dynamics import AbelianStratum + sage: p = AbelianStratum(5, 1).unique_component().permutation_representative() + sage: fg, verticals = p.fat_graph(verticals=True) + sage: fg.angles(verticals) + [4, 12] + """ + corners = [0] * self._n + for e in verticals: + corners[e] = 1 + res = [sum(corners[e] for e in v) for v in self.vertices()] + res.sort() + return res + + def spin_parity(self, verticals, check=True): + r""" + Return the spin parity of the given ``verticals`` + + The argument ``verticals`` must be a list of half-edges that allows to make sense + of the winding number on the graph. More precisely, if the underlying graph is + made of saddle connections on a translation or half-translation surface, the + verticals are the half-edges whose associated corner contains a vertical germ. + + EXAMPLES:: + + sage: from surface_dynamics import AbelianStratum + + sage: p = AbelianStratum(4, 2).odd_component().permutation_representative() + sage: fg, verticals = p.fat_graph(verticals=True) + sage: fg.spin_parity(verticals) + 1 + + sage: p = AbelianStratum(4, 2).even_component().permutation_representative() + sage: fg, verticals = p.fat_graph(verticals=True) + sage: fg.spin_parity(verticals) + 0 + """ + if check: + if any(a == 0 or a % 4 != 2 for a in self.angles(verticals)): + raise ValueError('invalid verticals') + verticals = [self._check_dart(e) for e in verticals] + + H = self.homology() + corners = [0] * self._n + for e in verticals: + corners[e] = 1 + + # the 2pi winding modulo 2 + # * fat_graph: underlying fat graph + # * corners: the position of verticals + # * path: the path on the fat graph of which we want to compute the winding + def path_winding(fat_graph, corners, path): + vp = fat_graph._vp + res = 0 + for i in range(len(path)): + e1 = path[i] ^ 1 + e2 = path[(i + 1) % len(path)] + + # edge reversion + res += 1 + + # counter-clockwise walk + while e1 != e2: + res -= corners[e1] + e1 = vp[e1] + + # NOTE: we want the 2pi winding modulo 2, hence the division by 2 + assert res % 2 == 0 + return (res // 2) % 2 + + # the actual quadratic form in homology + # * winding: the winding number of the cycle basis + # * I: the intersection matrix mod 2 (in the cycle basis) + # * v: the vector at which we want to evaluate the quadratic form + def quadratic_form(winding, I, v): + v = v.vector() + indices = [i for i, coeff in enumerate(v) if coeff] + # sum of connected components + t = sum(winding[i] + 1 for i in indices) + for j1 in range(len(indices)): + for j2 in range(j1 + 1, len(indices)): + t += I[indices[j1], indices[j2]] + return t % 2 + + cycles, I = H._cycle_basis() + B = H.symplectic_basis() + g = len(B) // 2 + winding = tuple(path_winding(self, corners, cycle) for cycle in cycles) + return sum(quadratic_form(winding, I, B[i]) * quadratic_form(winding, I, B[i + g]) for i in range(g)) % 2 + def dual(self): r""" - Return the dual fat graph. + Change this fat graph to becomes the dual fat graph. + + Be aware that this operation is not an involution but order 4. Taking + twice the dual, we obtain a fat graph in which all edges have been + swapped. + + The dual graph is oriented such that the half-edge `e` in the primal + intersects the half-edge `e` in the dual with a positive sign (ie + we perform a counter-clockwise quarter turn to each edge). EXAMPLES:: sage: from surface_dynamics.topology.fat_graph import FatGraph - sage: F = FatGraph(fp='(0)(1)') + sage: F = FatGraph(fp='(0)(1)', mutable=True) sage: F.dual() sage: F FatGraph('(0)(1)', '(0,1)') - sage: F._check() + sage: s = '20_i31027546b98jchedfag_23146758ab9igdhfejc0' - sage: F = FatGraph.from_string(s) + sage: F = FatGraph.from_string(s, mutable=True) + sage: F + FatGraph('(0,18,10,9,11,8,6,5,7,4,2,1,3)(12,19,16,13)(14,17,15)', '(0,2,1,3,4,6,5,7,8,10,9,11,18,12,16,14,17,19)(13)(15)') + sage: F.dual() + sage: F + FatGraph('(0,2,5,7,4,6,9,11,8,10,19,13,17,15,16,18,1,3)(12)(14)', '(0,18,10,9,11,8,6,5,7,4,2,1,3)(12,19,16,13)(14,17,15)') + + sage: F.dual() sage: F.dual() - sage: F._check() sage: F.dual() - sage: F._check() sage: F == FatGraph.from_string(s) True """ - # TODO: invert in place !!!! - self._vp, self._fp = perm_invert(self._fp, self._n), perm_invert(self._vp, self._n) + if not self._mutable: + raise ValueError('immutable graph; use a copy instead') + + fp = self._fp + fl = self._fl + self._fp = self._vp[:] + self._fl = self._vl[:] + + self._vp = [fp[e ^ 1] ^ 1 for e in range(self._n)] + self._vl = [fl[e ^ 1] for e in range(self._n)] + self._nv, self._nf = self._nf, self._nv - self._vl, self._fl = self._fl, self._vl - self._vd, self._fd = self._fd, self._vd + self._vd, self._fd = self._fd[:], self._vd[:] + self._check() def edge_lengths_polytope(self, b, min_length=0): r""" @@ -2614,3 +3045,49 @@ def relabel(self, r): perm_conjugate_inplace(self._fp, r, n) perm_on_list_inplace(r, self._vl, n) perm_on_list_inplace(r, self._fl, n) + + def lengths_generating_series(self, variable_name='b', symmetrization=True): + r""" + Return the generating series of face perimeters for all possible + positive integral lengths on the edges. + + EXAMPLES:: + + sage: from surface_dynamics import FatGraph, FatGraphs + sage: fg = FatGraph('(0,5,4)(1,2,3)', '(0,3,1,4)(2)(5)') + sage: fg.lengths_generating_series() + (b0*b1*b2^4)/((1 - b2^2)*(1 - b1*b2)*(1 - b0*b2)) + (b0*b1^4*b2)/((1 - b1*b2)*(1 - b1^2)*(1 - b0*b1)) + (b0^4*b1*b2)/((1 - b0*b2)*(1 - b0*b1)*(1 - b0^2)) + sage: fg.lengths_generating_series(variable_name='x', symmetrization=False) + (1/2*x0^4*x1*x2)/((1 - x0*x2)*(1 - x0*x1)*(1 - x0^2)) + + sage: fgs = FatGraphs(g=0, nf=3, vertex_min_degree=3).list() + sage: sum(fg.lengths_generating_series() for fg in fgs).factor() + (b0^2*b1^2*b2^2 + b0^2*b1*b2 + b0*b1^2*b2 + b0*b1*b2^2)/((1 - b2^2)*(1 - b1^2)*(1 - b0^2)) + """ + from surface_dynamics.misc.multiplicative_multivariate_generating_series import MultiplicativeMultivariateGeneratingSeriesRing + + nf = self.num_faces() + M = MultiplicativeMultivariateGeneratingSeriesRing(nf, variable_name) + V = M.free_module() + P = M.polynomial_ring() + x = P.gens() + + den = {} + num = P.one() + for e in range(self._n // 2): + l = [0] * nf + f1 = self._fl[2 * e] + f2 = self._fl[2 * e + 1] + l[f1] += 1 + l[f2] += 1 + v = V(l) + v.set_immutable() + if v in den: + den[v] += 1 + else: + den[v] = 1 + num *= x[f1] * x[f2] + + aut_size = self.automorphism_group().group_cardinality() + ans = M.term(num / aut_size, list(den.items())) + return ans.symmetrization() if symmetrization else ans diff --git a/surface_dynamics/topology/homology.py b/surface_dynamics/topology/homology.py new file mode 100644 index 00000000..e4693107 --- /dev/null +++ b/surface_dynamics/topology/homology.py @@ -0,0 +1,454 @@ +r""" +Fat graph homology. +""" +# **************************************************************************** +# Copyright (C) 2023 Vincent Delecroix <20100.delecroix@gmail.com> +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +from sage.misc.cachefunc import cached_method + +from sage.modules.module import Module +from sage.structure.element import ModuleElement, parent +from sage.structure.unique_representation import UniqueRepresentation + +from sage.categories.modules import Modules + +from sage.modules.free_module import FreeModule +from sage.matrix.constructor import matrix +from sage.arith.misc import gcd +from sage.rings.integer_ring import ZZ + +from surface_dynamics.misc.permutation import perm_orbit + + +def path_to_edge_coefficients(fat_graph, path, check=True): + if check and not fat_graph.is_path(path): + raise ValueError('not a valid path') + coeffs = [0] * fat_graph.num_edges() + for e in path: + if e % 2: + coeffs[e // 2] -= 1 + else: + coeffs[e // 2] += 1 + return coeffs + + +class FatGraphHomologyElement(ModuleElement): + def __init__(self, parent, v): + ModuleElement.__init__(self, parent) + self._v = parent._module(v) + self._v.set_immutable() + + def _richcmp_(self, other, op): + return self._v._richcmp_(other._v, op) + + def monomial_coefficients(self): + return self._v.monomial_coefficients() + + def _add_(self, other): + P = self.parent() + return P.element_class(P, self._v + other._v) + + def vector(self): + return self._v + + def _repr_(self): + if not self._v: + return '0' + + parent = self.parent() + fg = parent.fat_graph() + edge_coeffs= [parent.base_ring().zero()] * fg.num_edges() + cycles = self.parent()._cycle_basis()[0] + for coeff, cycle in zip(self._v, cycles): + if coeff: + for e in cycle: + if e % 2: + edge_coeffs[e // 2] -= coeff + else: + edge_coeffs[e // 2] += coeff + + return '(' + str(edge_coeffs)[1:-1] + ')' + + def intersection(self, other): + r""" + Return the algebraic intersection with ``other``. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: e1 = H.element_from_path([0, 12]) + sage: e2 = H.element_from_path([0, 3, 11]) + sage: e3 = H.element_from_path([13, 14, 11]) + sage: e1.intersection(e2) + 0 + sage: e1.intersection(e3) + 1 + sage: e2.intersection(e3) + 1 + """ + if parent(self) != parent(other): + raise ValueError + P = self.parent() + u = self.vector() + v = other.vector() + I = self.parent()._cycle_basis()[1] + return u * I * v + + +class FatGraphAbsoluteHomology(UniqueRepresentation, Module): + r""" + Absolute homology of a fat graph. + + A canonical basis of homology (in general not symplectic) is provided by a + tree cotree decomposition of the underlying fat graph. Namely, each + homology class has a unique representative that excludes edges of the + cotree. + + TESTS:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: TestSuite(H).run() + """ + Element = FatGraphHomologyElement + + def __init__(self, fat_graph, base_ring, tree_cotree_decomposition): + self._fat_graph = fat_graph.copy(mutable=False) + self._tree, self._cotree, self._complementary_edges = tree_cotree_decomposition + nv = fat_graph.num_vertices() + ne = fat_graph.num_edges() + nf = fat_graph.num_faces() + d = self.dimension() + self._module = FreeModule(base_ring, d) + self._edge_module = FreeModule(base_ring, ne) + self._vertex_module = FreeModule(base_ring, nv) + self._face_module = FreeModule(base_ring, nf) + Module.__init__(self, base_ring, category=Modules(base_ring).FiniteDimensional().WithBasis()) + + def _repr_(self): + r""" + TESTS:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.homology() + Homology(FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)'); Integer Ring) + """ + return 'Homology({}; {})'.format(self.fat_graph(), self.base_ring()) + + def _cotree_dfs(self): + r""" + Return a dfs ordering of the edges in the cotree. + + TESTS:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.homology()._cotree_dfs() + [3, 15] + """ + cotree = self._cotree + fl = self._fat_graph._fl + nf = self._fat_graph._nf + children = [[] for _ in range(nf)] + for f, e in enumerate(cotree): + if e == -1: + root = f + else: + assert fl[e] == f + children[fl[e ^ 1]].append(e) + i = 1 + res = children[root] + while i < len(res): + res.extend(children[fl[res[i]]]) + i += 1 + return res + + def base_ring(self): + r""" + Return the base ring. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.homology().base_ring() + Integer Ring + + sage: fg.homology(Zmod(2)).base_ring() + Ring of integers modulo 2 + """ + return self._module.base_ring() + + def change_ring(self, base_ring): + r""" + Change the underlying ring. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: fg.homology().change_ring(Zmod(2)).base_ring() + Ring of integers modulo 2 + """ + if base_ring == self.base_ring(): + return self + return FatGraphAbsoluteHomology(self._fat_graph, base_ring, (self._tree, self._cotree, self._complementary_edges)) + + def _cycle_basis(self): + return self._fat_graph.cycle_basis(intersection=True, tree_cotree_decomposition=(self._tree, self._cotree, self._complementary_edges)) + + def fat_graph(self): + r""" + Return the underlying graph. + """ + return self._fat_graph + + def an_element(self): + r""" + Return an element in this homology group. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.an_element() + (-1, 0, 1, 0, 0, 1, 0, 0) + """ + return self.element_class(self, self._module.an_element()) + + def random_element(self, *args, **kwds): + r""" + Return a random element in this homology group. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.random_element() # random + (-2, 0, -1, -1, -12, 0, -2, 0) + """ + return self.element_class(self, self._module.random_element(*args, **kwds)) + + def __iter__(self): + return (self.element_class(self, v) for v in self._module) + + def dimension(self): + r""" + Return the dimension which is twice the genus of the underlying graph. + """ + return 2 * self._fat_graph.genus() + + def cycle_basis(self): + r""" + Return the basis as simple paths in the underlying fat graph. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.cycle_basis() + [[10, 4, 1], [0, 6, 11], [8], [0, 12]] + """ + return self._cycle_basis()[0] + + def intersection_matrix(self): + r""" + Return the intersection matrix on the canonical basis. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.intersection_matrix() + [ 0 1 1 0] + [-1 0 0 0] + [-1 0 0 1] + [ 0 0 -1 0] + """ + return self._cycle_basis()[1] + + def basis(self): + r""" + Return the canonical basis built from a tree-cotree decomposition of the underlying fat graph. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: B = H.basis() + sage: B + ((-1, 0, 1, 0, 0, 1, 0, 0), + (1, 0, 0, 1, 0, -1, 0, 0), + (0, 0, 0, 0, 1, 0, 0, 0), + (1, 0, 0, 0, 0, 0, 1, 0)) + sage: matrix(ZZ, 4, [B[i].intersection(B[j]) for j in range(4) for i in range(4)]) + [ 0 -1 -1 0] + [ 1 0 0 0] + [ 1 0 0 -1] + [ 0 0 1 0] + """ + return tuple(self.element_class(self, b) for b in self._module.basis()) + + def symplectic_basis(self): + r""" + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: B = H.symplectic_basis() + sage: B + ((-1, 0, 1, 0, 0, 1, 0, 0), + (-1, 0, 0, -1, 1, 1, 0, 0), + (1, 0, 0, 1, 0, -1, 0, 0), + (1, 0, 0, 0, 0, 0, 1, 0)) + sage: matrix(ZZ, 4, [B[i].intersection(B[j]) for j in range(4) for i in range(4)]) + [ 0 0 -1 0] + [ 0 0 0 -1] + [ 1 0 0 0] + [ 0 1 0 0] + """ + # the rows of C form a symplectic basis + # F = C * self * C.transpose() + F, C = self._cycle_basis()[1].symplectic_form() + return tuple(self.element_class(self, b) for b in C.rows()) + + def element_from_path(self, path): + r""" + Return an homology element from a path. + + The ``path`` should be given as a sequence of consecurive half edges. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg1 = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H1 = fg1.homology() + sage: H1.element_from_path([10, 4, 1]) + (-1, 0, 1, 0, 0, 1, 0, 0) + + sage: fg2 = FatGraph('(0,13,11,9,8,14,2,1,3)(4,15,6,5,12,7,10)', '(0,2,1,3,14,4,6,12)(5,10,13)(7,15,8,11)(9)') + sage: H2 = fg2.homology() + sage: H2.element_from_path([10, 13]) + (0, 0, 0, 0, 0, 1, -1, 0) + sage: H2.element_from_path([0, 11, 4, 15]) + (1, 0, 0, 1, 0, 1, -1, 0) + + Faces are mapped to zero:: + + sage: assert all(H1.element_from_path(face).is_zero() for face in fg1.faces()) + sage: assert all(H2.element_from_path(face).is_zero() for face in fg2.faces()) + """ + coeffs = path_to_edge_coefficients(self._fat_graph, path) + return self.element_from_edge_coefficients(coeffs) + + def element_from_half_edge_coefficients(self, coeffs): + r""" + Return an element from half edge coefficients. + + The argument ``coeffs`` must be a list or a vector whose length is the + number of half edges in the underlying fat graph. The entries are the + coefficients for each half edge. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.element_from_half_edge_coefficients((1, 1, 3, 0, 1, 3, 0, 0, -2, 0, 1, 0, 1, 0, 0, 0)) + (0, 0, -2, 0, -2, -2, -2, 0) + """ + if len(coeffs) != 2 * self._fat_graph.num_edges(): + raise ValueError('invalid coefficients') + edge_coeffs = [self.base_ring().zero()] * self._fat_graph.num_edges() + for e in range(self._fat_graph.num_edges()): + edge_coeffs[e] += coeffs[2 * e] - coeffs[2 * e + 1] + return self.element_from_edge_coefficients(edge_coeffs) + + def boundary(self, coeffs): + coeffs = self._edge_module(coeffs) + bdry = [0] * self._fat_graph._nv + vl = self._fat_graph._vl + for e, coeff in enumerate(coeffs): + if coeff: + bdry[vl[2 * e]] += coeff + bdry[vl[2 * e + 1]] -= coeff + return self._vertex_module(bdry) + + def element_from_edge_coefficients(self, coeffs): + r""" + Return an element from edge coefficients. + + The argument ``coeffs`` must be a list or a vector whose length is the + number of edges in the underlying fat graph. The entries are the + coefficients for each edge. + + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: H.element_from_edge_coefficients((1, 1, 0, 1, 0, 0, 1, 0)) + (1, 0, 0, 1, 0, -1, 0, 0) + sage: H.element_from_edge_coefficients((1, 0, 0, 1, 0, -1, 0, 0)) + (1, 0, 0, 1, 0, -1, 0, 0) + """ + if len(coeffs) != self._fat_graph.num_edges(): + raise ValueError('invalid coefficients') + coeffs = self._edge_module(coeffs) + if self.boundary(coeffs): + raise ValueError('not a cycle') + + # step one: rewrite edge in the cotree using faces + for e in self._cotree_dfs(): + c = coeffs[e // 2] + if c: + coeffs[e // 2] = 0 + face = perm_orbit(self._fat_graph._fp, e) + assert face[0] == e + for i in range(1, len(face)): + ee = face[i] + if ee % 2 == e % 2: + coeffs[ee // 2] -= c + else: + coeffs[ee // 2] += c + + assert all(coeffs[e // 2] == 0 for e in self._cotree if e != -1), (coeffs, self._cotree, self._cotree_dfs()) + assert self.boundary(coeffs) == 0 + + # step two: the remaining part is a sum of cycles and one + # obtain the coefficients by reading the complementary edges + v = [coeffs[e // 2] for e in self._complementary_edges] + return self.element_class(self, v) + + def element_from_vector(self, v): + r""" + EXAMPLES:: + + sage: from surface_dynamics.topology.fat_graph import FatGraph + sage: fg = FatGraph('(0,13,10)(1,6,8,5,3,12,9,14)(2,15,7,4,11)', '(0,14,2,5,7,1,10,4,8,12)(3,11,13)(6,15,9)') + sage: H = fg.homology() + sage: h = H.element_from_vector([0, 0, 0, 1]) + sage: h + (1, 0, 0, 0, 0, 0, 1, 0) + sage: h.vector() + (0, 0, 0, 1) + """ + return self.element_class(self, v) + + From 1bc0e041521a62764472c63ebe70c939a67364d5 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 09:23:57 +0200 Subject: [PATCH 04/17] use fat graph holonomy to compute spin parity --- surface_dynamics/flat_surfaces/homology.py | 689 ++++-------------- .../flat_surfaces/origamis/origami_dense.pyx | 65 +- .../flat_surfaces/separatrix_diagram.py | 78 +- surface_dynamics/flat_surfaces/strata.py | 4 +- .../interval_exchanges/template.py | 103 ++- 5 files changed, 342 insertions(+), 597 deletions(-) diff --git a/surface_dynamics/flat_surfaces/homology.py b/surface_dynamics/flat_surfaces/homology.py index 937124ee..954bd5f4 100644 --- a/surface_dynamics/flat_surfaces/homology.py +++ b/surface_dynamics/flat_surfaces/homology.py @@ -1,50 +1,21 @@ r""" -Simplicial complex, homology of surfaces and translation surfaces +Deprecated module. -In this module are implemented simple homology computation for translation -surfaces. There are three main classes: - -- :class:`RibbonGraph`: decomposition of a surface into polygons. The - combinatorics is stored as a triple of permutations `v` (vertices), `e` - (edges), `f` (faces) so that the product `vef` is the identity. The domain of - the permutations correspond to the half edges or *darts*. The permutation `e` - is an involution so that `e(i)` is the other half of the edge starting at - `i`. The fixed points of `e` corresponds to edge glued to themselves. The - permutation `v` is obtained by turning around a vertex, while `f` turning - around a face. - -- :class:`RibbonGraphWithAngles`: a ribbon graph with an additional angle - structure. - -- :class:`RibbonGraphWithHolonomies`: a ribbon graph with an additional holonomy - structure on its edges. - -EXAMPLES:: +TESTS:: sage: from surface_dynamics import * - -To create a ribbon graph you just need to fix two of the permutations `v`, `e`, -`f`:: - sage: R = RibbonGraph(vertices='(0,1,4,3)(5,2)',edges='(0,3)(1,2)(4,5)') + doctest:warning + ... + DeprecationWarning: RibbonGraph is deprecated; use surface_dynamics.fat_graph.FatGraph instead sage: R Ribbon graph with 2 vertices, 3 edges and 3 faces - -The vertices, edges and faces are by definition the cycles of the permutation. -Calling the method :meth:`~RibbonGraph.vertices`, :meth:`~RibbonGraph.edges` or -:meth:`~RibbonGraph.faces` gives you access to these cycles:: - sage: R.vertices() [[0, 1, 4, 3], [2, 5]] sage: R.edges() [[0, 3], [1, 2], [4, 5]] sage: R.faces() - [[0, 4, 2], [1, 5], [3]] - -Given a half edge (i.e. a dart), you can get the index of the vertex, edge or -face it belongs with the methods :meth:`~RibbonGraph.dart_to_vertex`, -:meth:`~RibbonGraph.dart_to_edge` and :meth:`~RibbonGraph.dart_to_edge`:: - + [[0, 4, 2], [3], [1, 5]] sage: R.dart_to_vertex(1) 0 sage: 1 in R.vertices()[0] @@ -62,28 +33,19 @@ sage: R.dart_to_face(4) 0 sage: R.dart_to_face(3) - 2 - -To initialize a ribbon graph with angles, you have to input the standard data to -initialize a ribbon graph plus a list of positive rational numbers which -corresponds to the angles between darts (more precisely, the number at position -i is the angle between i and v(i)):: - + 1 sage: e = '(0,1)(2,3)' sage: f = '(0,2,1,3)' sage: a = [1/2,1/2,1/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) + doctest:warning + ... + DeprecationWarning: RibbonGraphWithAngles is deprecated; use surface_dynamics.fat_graph.FatGraph instead sage: r.spin_parity() 1 - -TODO: - -- Oriented ribbon graphs are *exactly* separatrix diagrams. An orientation on a Ribbon - graphs is a choice of orientation for each vertex so that each face has all of its - edges oriented in the same direction as we go along the boundaries. """ #***************************************************************************** -# Copyright (C) 2019 Vincent Delecroix <20100.delecroix@gmail.com> +# Copyright (C) 2019-2023 Vincent Delecroix <20100.delecroix@gmail.com> # # Distributed under the terms of the GNU General Public License (GPL) # as published by the Free Software Foundation; either version 2 of @@ -107,61 +69,17 @@ from sage.rings.rational_field import QQ -# TODO: introduce an oriented ribbon graph class -# an oriented ribbon graph is a ribbon graph such that one can choose -# coherently an orientation of each edge so that each face gets -# all its edges oriented the same way -# (the dual corresponds in having all vertices being either sources or sinks) -# we encode this by choosing by labeling for each edge with the smaller edge index - class RibbonGraph(SageObject): r""" - Generic class for Ribbon graph. - - A Ribbon graph (or fat graph or combinatorial map) is a graph embedded in a - surface. This class uses representation as a triple ``(v,e,f)`` of - permutations such that `vef = 1` and the action of the group generated by - `v,e,f` acts transitively in the domain. The cycles of ``v`` are - considered as vertices, the ones of ``e`` are considered as edges and the - ones of ``f`` as the faces. Each element of the domain is a half-edge which - is called a *dart*. A dart is also associated to an oriented edge. - - The domain of the permutations must be a subset of [0, ..., N-1] for some N. - The edges are always considered to be (i, ~i) where i is the bit complement - of the integer i (~0 = -1, ~-3 = 2). So that an edge always has a canonical - representative number given by the non-negative version. - - A dense ribbon graph has the following attributes - - - total_darts - non negative integer - the total number darts - - num_darts - non negative integer - the number of active darts - - active_darts - bitset - list of lengths _total_darts with True or - False. The position i is True if i is an active dart. - - - vertices, vertices_inv - list - partial permutations of [0,N] which are - inverse of each other - - vertex_cycles - the cycles of the partial permutation vertices - - dart_to_vertex_index + Deprecated class. - - edges, edges_inv - list - partial permutations of [0,N] which are - inverse of each other - - edge_cycles - the cycles of the partial permutation edge - - dart_to_edge_index - - - faces, faces_inv - list - partial permutations of [0,N] which are - inverse of each other - - face_cycles - the cycles of the partial permutation faces - - dart_to_face_index - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: RibbonGraph([],[],[]) Ribbon graph with 1 vertex, 0 edge and 1 face sage: RibbonGraph('()','(0,1)','(0,1)') Ribbon graph with 2 vertices, 1 edge and 1 face - sage: G = RibbonGraph('(0,3)(1,2)','(0,1)(2,3)','(0,2)(1,3)') sage: G Ribbon graph with 2 vertices, 2 edges and 2 faces @@ -169,7 +87,6 @@ class RibbonGraph(SageObject): [0, 1, 2, 3] sage: G.genus() 0 - sage: G = RibbonGraph(edges='(0,2)(1,3)(4,6)(5,7)',faces='(0,1,2,3,4,5,6,7)') sage: G Ribbon graph with 1 vertex, 4 edges and 1 face @@ -177,7 +94,6 @@ class RibbonGraph(SageObject): [0, 1, 2, 3, 4, 5, 6, 7] sage: G.genus() 2 - sage: G = RibbonGraph(vertices='(0,2,3,6)(1,4,5,7)') sage: G Ribbon graph with 2 vertices, 4 edges and 4 faces @@ -200,8 +116,35 @@ def __init__(self, vertices=None, edges=None, faces=None, connected=True, check= sage: RibbonGraph(vertices='(1)(5)', edges='(1,5)', faces='(1,5)') Ribbon graph with 2 vertices, 1 edge and 1 face """ + from warnings import warn + warn('RibbonGraph is deprecated; use surface_dynamics.fat_graph.FatGraph instead', DeprecationWarning) vertices, edges, faces = constellation_init(vertices, edges, faces, check=check) + n = len(vertices) + vp = [-1] * n + fp = [-1] * n + mapping = [-1] * n + inv_mapping = [-1] * n + num_darts = 0 + for i in range(n): + ii = edges[i] + if ii != -1 and mapping[i] == -1: + mapping[i] = num_darts + mapping[ii] = num_darts + 1 + inv_mapping[num_darts] = i + inv_mapping[num_darts + 1] = ii + num_darts += 2 + for i in range(n): + if mapping[i] != -1: + vp[mapping[i]] = mapping[vertices[i]] + fp[mapping[i]] = mapping[faces[i]] + vp = vp[:num_darts] + fp = fp[:num_darts] + self._fat_graph_map = mapping + self._fat_graph_inv_map = inv_mapping + from surface_dynamics.topology.fat_graph import FatGraph + self._fat_graph = FatGraph(vp=vp, fp=fp, max_num_dart=n, mutable=True, check=True) + self._total_darts = total_darts = len(vertices) num_darts = 0 self._active_darts = [True] * total_darts @@ -253,8 +196,6 @@ def __init__(self, vertices=None, edges=None, faces=None, connected=True, check= def _repr_(self): r""" - String representation. - TESTS:: sage: from surface_dynamics import * @@ -266,17 +207,17 @@ def _repr_(self): sage: RibbonGraph(edges='(0,1)(2,3)',faces='(0,2)(1,3)')._repr_() 'Ribbon graph with 2 vertices, 2 edges and 2 faces' """ - n = self.num_vertices() + n = self._fat_graph.num_vertices() if n <= 1: vert_str = "%d vertex" %n else: vert_str = "%d vertices" %n - n = self.num_edges() + n = self._fat_graph.num_edges() if n <= 1: edge_str = "%d edge" %n else: edge_str = "%d edges" %n - n = self.num_faces() + n = self._fat_graph.num_faces() if n <= 1: face_str = "%d face" %n else: @@ -286,49 +227,39 @@ def _repr_(self): @cached_method def _symmetric_group(self): from sage.groups.perm_gps.permgroup_named import SymmetricGroup - return SymmetricGroup([i for i,j in enumerate(self._active_darts) if j]) + return SymmetricGroup([i for i, j in enumerate(self._fat_graph_map) if j != -1]) def monodromy_group(self): r""" - Return the group generated by the three defining permutations. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import RibbonGraph - sage: r1 = RibbonGraph(vertices='(0)(2)(1,3,4,5)', edges='(0,1)(2,3)(4,5)') sage: G1 = r1.monodromy_group() sage: G1 Subgroup ... sage: G1.is_isomorphic(SymmetricGroup(5)) True - sage: r2 = RibbonGraph(vertices='(0)(2)(1,3,4)(5,6,7)', edges='(0,1)(2,3)(4,5)(6,7)') sage: G2 = r2.monodromy_group() sage: G2 Subgroup ... sage: G2.is_isomorphic(PSL(2,7)) True - sage: r3 = RibbonGraph(vertices='(1)(5)', edges='(1,5)', faces='(1,5)') sage: r3.monodromy_group() Subgroup ... """ S = self._symmetric_group() - v = S([i for i in self._vertices if i != -1]) - e = S([i for i in self._edges if i != -1]) - f = S([i for i in self._faces if i != -1]) - assert (v*e*f).is_one() - return S.subgroup([v,e,f]) + v = S([self._fat_graph_inv_map[j] for j in self._fat_graph._vp[:self._fat_graph._n]]) + e = S([(self._fat_graph_inv_map[j], self._fat_graph_inv_map[j + 1]) for j in range(0, self._fat_graph._n, 2)]) + f = S([self._fat_graph_inv_map[j] for j in self._fat_graph._fp[:self._fat_graph._n]]) + assert (v * e * f).is_one() + return S.subgroup([v, e, f]) def automorphism_group(self, fix_vertices=False, fix_edges=False, fix_faces=False): r""" - Return the automorphism group of this ribbon graph. - - The automorphism is simply the intersection of the centralizers - of the defining permutations. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * sage: r = RibbonGraph('(1,6,5)(2,3,4)', '(1,2)(3,4)(5,6)', '(1,4,2,5)(3)(6)') @@ -336,13 +267,9 @@ def automorphism_group(self, fix_vertices=False, fix_edges=False, fix_faces=Fals 2 sage: r.automorphism_group(fix_faces=True).cardinality() 1 - sage: r = RibbonGraph('(1,4,5)(2,6,3)', '(1,2)(3,4)(5,6)', '(1,3)(2,5)(4,6)') sage: r.automorphism_group().cardinality() 6 - - Examples in genus 1:: - sage: r = RibbonGraph('(1,5,4)(6,3,2)', '(1,2)(3,4)(5,6)', '(1,3,5,2,4,6)') sage: A = r.automorphism_group() sage: A @@ -351,7 +278,6 @@ def automorphism_group(self, fix_vertices=False, fix_edges=False, fix_faces=Fals 6 sage: r.automorphism_group(fix_faces=True) == A True - sage: r = RibbonGraph('(1,3,2,4)', '(1,2)(3,4)', '(1,3,2,4)') sage: r.automorphism_group().cardinality() 4 @@ -384,37 +310,9 @@ def automorphism_group(self, fix_vertices=False, fix_edges=False, fix_faces=Fals # TODO: perform the expansion as quasi-polynomial # TODO: see how the recursion formula translates on this generating series def length_rational_fraction(self, var='b'): - r""" - Return the generating series for the number of lengths with the given boundaries - """ - from sage.symbolic.ring import SR - - F = SR.one() - - for dart in range(self._total_darts): - if not self._active_darts[dart]: - continue - i = self._dart_to_edge_index[dart] - j1, j2 = self._edge_cycles[i] - if j1 == dart: - continue - else: - assert j2 == dart - - f1 = self._dart_to_face_index[j1] - f2 = self._dart_to_face_index[j2] - - b1 = SR.var('%s%d' %(var, f1)) - b2 = SR.var('%s%d' %(var, f2)) - - F *= b1*b2 / (1 - b1*b2) - - return F + return self._fat_graph.lengths_generating_series(variable_name=var) def add_extra_darts(self, n): - r""" - Add extra darts to support a total of ``2 n`` darts. - """ m = self._total_darts if 2*n > m: self._total_darts = int(2*n) @@ -426,9 +324,6 @@ def add_extra_darts(self, n): self._check() def _check(self): - r""" - Check that the data of the Ribbon graph is coherent - """ if len(self._active_darts) != self._total_darts: raise ValueError("the length of active darts is not total_darts") if self._active_darts.count(True) != self._num_darts: @@ -461,31 +356,15 @@ def _check(self): if self._faces[i] != -1 or self._faces_inv[i] is not None: raise ValueError("dart %d is not active but has a face" %i) - if self._connected and not self.is_connected(force_computation=True): + if self._connected and not self.is_connected(): raise ValueError("the graph is not connected") def is_connected(self, force_computation=False): - if not force_computation and self._connected: - return True - - from sage.graphs.graph import Graph - G = Graph(loops=True, multiedges=True) - for i in range(self._total_darts): - if self._active_darts[i]: - G.add_edge(i,self._vertices[i]) - G.add_edge(i,self._edges[i]) - G.add_edge(i,self._faces[i]) - return G.is_connected() + return self._fat_graph.is_connected() def relabel(self, perm=None): - r""" - perm is a of range(0,N) - - If ``perm`` is None, relabel the darts on 0,2M keeping the relative - order of the darts. - """ if perm is None: - perm=[None]*self.num_darts() + perm = [None] * self.num_darts() k = 0 for i in range(self.num_darts()): if self._active_darts[i]: @@ -501,156 +380,82 @@ def relabel(self, perm=None): edges[perm[i]] = perm[self._edges[i]] faces[perm[i]] = perm[self._faces[i]] - return RibbonGraph(vertices,edges,faces) - - # - # Darts - # + return RibbonGraph(vertices, edges, faces) def num_darts(self): - r""" - Returns the number of darts. - """ return self._num_darts def darts(self): - r""" - Return the list of darts - """ - return [i for i in range(self._total_darts) if self._active_darts[i]] + return [i for i in range(self._total_darts) if self._fat_graph_map[i] != -1] def num_vertices(self): - r""" - Returns the number of vertices. - """ - return max(1,len(self._vertex_cycles)) + return self._fat_graph.num_vertices() - def vertex_perm(self): - r""" - Returns the permutation that define the vertices. - """ + def vertex_perm(self, copy=True): return self._vertices def vertex_orbit(self, i): - r""" - Return the orbit of ``i`` under the permutation that define the - vertices. - """ - if self._active_darts[i]: - return perm_orbit(self._vertices,i) + j = self._fat_graph_map[i] + if j != -1: + return [self._fat_graph_inv_map[x] for x in perm_orbit(self._fat_graph._vp, j)] return None def vertices(self): - r""" - Return the list of vertices as cycles decomposition of the vertex - permutation. - """ - return self._vertex_cycles + return [[self._fat_graph_inv_map[x] for x in cycle] for cycle in self._fat_graph.vertices()] def dart_to_vertex(self,i): - r""" - Return the vertex on which the dart ``i`` is attached. - """ - if self._active_darts[i]: - return self._dart_to_vertex_index[i] + j = self._fat_graph_map[i] + if j != -1: + return self._fat_graph._vl[j] raise ValueError("dart %d is not active" %i) - # - # Edges - # - def num_edges(self): - r""" - Returns the number of edges. - """ - return len(self._edge_cycles) + return self._fat_graph.num_edges() def edge_perm(self): - r""" - Return the permutation that define the edges. - """ return self._edges def edge_orbit(self, i): - r""" - Return the orbit of the dart ``i`` under the permutation that defines - the edges. - """ - if self._active_darts[i]: - return perm_orbit(self._edges,i) + j = self._fat_gramp_map[i] + if j != -1: + return [[self._fat_graph_inv_map[x], self._fat_graph_inv_map[x + 1]] for x in range(self._fat_graph._n)] return None def edges(self): - r""" - Return the set of edges. - """ - return self._edge_cycles + return [[self._fat_graph_inv_map[x] for x in cycle] for cycle in self._fat_graph.edges()] def dart_to_edge(self, i, orientation=False): - r""" - Returns the edge the darts ``i`` belongs to. - - If orientation is set to ``True`` then the output is a `2`-tuple - ``(e,o)`` where ``e`` is the index of the edge and ``o`` is its - orientation as ``+1`` or ``-1``. - """ - if self._active_darts[i]: - if not orientation: - return self._dart_to_edge_index[i] - j = self._dart_to_edge_index[i] - if i == self._edge_cycles[j][0]: - return (j,1) - elif i == self._edge_cycles[j][1]: - return (j,-1) - else: - raise ValueError("this should not happen!") - raise ValueError("dart %d is not active" %i) - - # - # Faces - # + j = self._fat_graph_map[i] + if j == -1: + raise ValueError("dart %d is not active" %i) + return (j // 2, j % 2) if orientation else j // 2 def num_faces(self): - r""" - Return the number of faces. - """ - return max(1,len(self._face_cycles)) + return self._fat_graph.num_faces() def face_perm(self): - r""" - Return the permutation that defines the face. - """ return self._faces def face_orbit(self, i): - r""" - Return the orbit of ``i`` under the permutation associated to faces. - """ - if self._active_darts[i]: - return perm_orbit(self._faces,i) + j = self._fat_graph_map[i] + if j != -1: + return [self._fat_graph_inv_map[x] for x in perm_orbit(self._fat_graph._fp, j)] return None def faces(self): - r""" - Return the list of faces. - """ - return self._face_cycles + return [[self._fat_graph_inv_map[x] for x in cycle] for cycle in self._fat_graph.faces()] def dart_to_face(self, i): - if self._active_darts[i]: - return self._dart_to_face_index[i] - raise ValueError("dart {} is not active".format(i)) + j = self._fat_graph_map[i] + if j == -1: + raise ValueError("dart {} is not active".format(i)) + return self._fat_graph._fl[j] def dual(self): r""" - Returns the dual Ribbon graph. - - The *dual* ribbon graph of `(v,e,f)` is `(f^{-1}, e, v^{-1})`. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph(edges='(0,1)',faces='(0)(1)'); r Ribbon graph with 1 vertex, 1 edge and 2 faces sage: r.dual() @@ -661,150 +466,101 @@ def dual(self): edges=self._edges, faces=perm_invert(self._vertices)) - # - # euler characteristic - # - def euler_characteristic(self): r""" - Returns the Euler characteristic of the embedded surface. - - The *Euler characteristic* of a surface complex is `V - E + F`, where - `V` is the number of vertices, `E` the number of edges and `F` the - number of faces. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph(edges='(0,1)(2,3)(4,5)',faces='(0,2,4)(1)(3,5)') sage: r.euler_characteristic() 2 - sage: r = RibbonGraph(edges='(0,1)(2,3)',faces='(0,2,1,3)') sage: r.euler_characteristic() 0 """ - return self.num_vertices() - self.num_edges() + self.num_faces() + return self._fat_graph.euler_characteristic() def is_plane(self): r""" - Returns true if and only if the ribbon graph belongs in a sphere. In - other words if it has genus 0. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph(vertices='(0)(1)',edges='(0,1)') sage: r.is_plane() True - sage: r = RibbonGraph(vertices='(0,1)',edges='(0,1)') sage: r.is_plane() True - sage: r = RibbonGraph(edges='(0,1)(2,3)',faces='(0,2)(1,3)') sage: r.is_plane() True - sage: r = RibbonGraph(edges='(0,1)(2,3)',faces='(0,2,1,3)') sage: r.is_plane() False """ - return self.euler_characteristic() == 2 + return self._fat_graph.euler_characteristic() == 2 def is_plane_tree(self): r""" - Returns True if and only if the ribbon graph is a planar tree. In other - words, it has genus 0 and only one face. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph(vertices='(0)(1)',edges='(0,1)') sage: r.is_plane_tree() True - sage: r = RibbonGraph(vertices='(0)(1,2,4)(3)(5)',edges='(0,1)(2,3)(4,5)') sage: r.is_plane_tree() True - sage: r = RibbonGraph(vertices='(0,1)',edges='(0,1)') sage: r.is_plane_tree() False sage: r.is_plane() True """ - return (self.num_faces() == 1 and self.genus() == 0) + return self.num_faces() == 1 and self.genus() == 0 def is_triangulated(self): r""" - Returns True if the surface is triangulated. In other words, faces - consist only of the product of 3-cycles. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph(edges='(0,1)(2,3)(4,5)',faces='(0,2,4)(1,5,3)') sage: r.is_triangulated() True - sage: r = RibbonGraph(edges='(0,1)(2,3)',faces='(0,2,1,3)') sage: r.is_triangulated() False """ - return all(len(c) == 3 for c in self.faces()) + return self._fat_graph.is_triangulation() def genus(self): r""" - Return the genus of the surface associated to this Ribbon graph. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: R = RibbonGraph(vertices='(1)(2)',edges='(1,2)') sage: R.genus() 0 - sage: e='(1,3)(2,4)' sage: f='(1,2,3,4)' sage: RibbonGraph(edges=e,faces=f).genus() 1 - sage: e='(1,3)(2,4)(5,7)(6,8)' sage: f='(1,2,3,4,5,6,7,8)' sage: RibbonGraph(edges=e,faces=f).genus() 2 - sage: e='(1,3)(2,4)(5,7)(6,8)(9,11)(10,12)' sage: f='(1,2,3,4,5,6,7,8,9,10,11,12)' sage: RibbonGraph(edges=e,faces=f).genus() 3 """ - return 1 - self.euler_characteristic()//2 - - # - # cycles and fundamental group - # + return self._fat_graph.genus() def spanning_tree(self): r""" - Return a spanning tree - - OUTPUT: - - - spanning tree as a DiGraph - - - remaining edges as 2-tuples ``(i,e[i])`` - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: R = RibbonGraph('(1,2,3)','(1,2)(3,4)') sage: R Ribbon graph with 2 vertices, 2 edges and 2 faces @@ -815,7 +571,6 @@ def spanning_tree(self): [(0, 1, (3, 4))] sage: o [(1, 2)] - sage: R = RibbonGraph('(1,2,3)(4,5,6)','(1,2)(3,4)(5,6)') sage: R Ribbon graph with 2 vertices, 3 edges and 3 faces @@ -826,7 +581,6 @@ def spanning_tree(self): [(0, 1, (3, 4))] sage: o [(1, 2), (5, 6)] - sage: e = '(1,3)(5,7)(2,4)(6,8)' sage: f = '(1,2,3,4,5,6,7,8)' sage: R = RibbonGraph(edges=e, faces=f) @@ -879,16 +633,9 @@ def spanning_tree(self): def collapse(self, spanning_tree=None): r""" - Return a ribbon graph callapsed along a spanning tree. - - The resulting graph is on the same surface as the preceding but has only - one vertex. It could be used twice to provide a polygonal representation - with one vertex and one face. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: R = RibbonGraph(vertices='(0,1,2,5)(3,7)(4,10,9)(6,11,12)(8,13)') sage: R.genus() 1 @@ -906,7 +653,6 @@ def collapse(self, spanning_tree=None): sage: R3 = R2.dual().collapse().dual() sage: R3 Ribbon graph with 1 vertex, 2 edges and 1 face - """ from copy import deepcopy @@ -933,18 +679,12 @@ def collapse(self, spanning_tree=None): def boundaries(self): r""" - Return the list of cycles which are boundaries. - - A cycle is a *boundary* if it bounds a face. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: r = RibbonGraph('(1,2,3)(4,5,6)','(1,2)(3,4)(5,6)') sage: r.boundaries() [[(1, 2)], [(2, 1), (3, 4), (6, 5), (4, 3)], [(5, 6)]] - sage: r = RibbonGraph('(1,2,3)(4,5)(6,7,8)',edges='(1,2)(3,4)(5,6)(7,8)') sage: r.boundaries() [[(1, 2)], [(2, 1), (3, 4), (5, 6), (8, 7), (6, 5), (4, 3)], [(7, 8)]] @@ -954,143 +694,60 @@ def boundaries(self): def cycle_basis(self, intersection=False, verbose=False): r""" - Returns a base of oriented cycles of the Ribbon graph modulo boundaries. - - If ``intersection`` is set to True then the method also returns the - intersection matrix of the cycles. - - EXAMPLES:: - - sage: from surface_dynamics import * + TESTS:: + sage: from surface_dynamics import RibbonGraph sage: r = RibbonGraph('(1,2,3)(4,5,6)','(1,2)(3,4)(5,6)') sage: r.cycle_basis() [] - sage: r = RibbonGraph('(1,2,3)(4,5)(6,7,8)',edges='(1,2)(3,4)(5,6)(7,8)') sage: r.cycle_basis() [] - sage: r = RibbonGraph('(1,4,5)(2,3)(6,7,8)',edges='(1,2)(3,4)(5,6)(7,8)') sage: r.cycle_basis() [] - sage: e = '(1,3)(2,4)(5,7)(6,8)' sage: f = '(1,2,3,4,5,6,7,8)' sage: r = RibbonGraph(edges=e,faces=f) sage: r.cycle_basis() - [[[1, 3]], [[2, 4]], [[5, 7]], [[6, 8]]] - + [[(1, 3)], [(2, 4)], [(5, 7)], [(6, 8)]] + sage: r.dual().cycle_basis() + [[(1, 3)], [(2, 4)], [(5, 7)], [(6, 8)]] + sage: r.cycle_basis(intersection=True) + ( + [ 0 1 0 0] + [-1 0 0 0] + [ 0 0 0 1] + [[(1, 3)], [(2, 4)], [(5, 7)], [(6, 8)]], [ 0 0 -1 0] + ) + sage: r.dual().cycle_basis(intersection=True) + ( + [ 0 -1 0 0] + [ 1 0 0 0] + [ 0 0 0 -1] + [[(1, 3)], [(2, 4)], [(5, 7)], [(6, 8)]], [ 0 0 1 0] + ) sage: f = '(0,10,13)(6,17,11)(2,14,7)(15,12,3)(16,20,19)(18,1,9)(4,22,21)(23,8,5)' sage: e = tuple((i,i+1) for i in range(0,24,2)) sage: r = RibbonGraph(edges=e,faces=f); r Ribbon graph with 2 vertices, 12 edges and 8 faces sage: c,m = r.cycle_basis(intersection=True) sage: c - [[(0, 1), [4, 5]], [[8, 9]], [[12, 13]], [[14, 15], (1, 0)]] + [[(0, 1), (4, 5)], [(6, 7)], [(14, 15), (1, 0)], [(22, 23), (1, 0)]] sage: m - [ 0 1 0 0] - [-1 0 0 0] [ 0 0 0 1] - [ 0 0 -1 0] + [ 0 0 1 0] + [ 0 -1 0 0] + [-1 0 0 0] """ - T,o = self.spanning_tree() - - # build a Ribbon graph with one vertex and one face - r = self.collapse(T).dual().collapse().dual() - if T is None: - return r.edges() - if intersection: - c = r.vertices()[0] - M = len(c) - I = [] - - cycles = [] # the cycles - for e in r.edges(): - if verbose: - print("build cycle from edge %s between vertex v0=%d and v1=%d" %(str(e),self.dart_to_vertex(e[0]),self.dart_to_vertex(e[1]))) - - # build the branch to the root from v0 - v0 = self.dart_to_vertex(e[0]) - if verbose: - print(" build branch from v0=%d" % v0) - p0 = [] - while v0 != 0: - v0,_,e0 = T.incoming_edges(v0)[0] # (v_in,v_out,label) - p0.append(e0) - if verbose: - print(" add %d" % v0) - if verbose: - print(" branch is %s" % str(p0)) - # build the branch to the root from v1 - - v1 = self.dart_to_vertex(e[1]) - if verbose: - print(" build branch from v1=%d" % v1) - p1 = [] - while v1 != 0: - v1,_,e1 = T.incoming_edges(v1)[0] - p1.append(e1) - if verbose: - print(" add %d" % v1) - if verbose: - print(" branch is %s" % str(p1)) - # clean the branches by removing common part - while p0 and p1 and p0[-1] == p1[-1]: - if verbose: - print("find common element", p0[-1]) - p0.pop(-1) - p1.pop(-1) - - # add the cycle to the list - cycles.append((p0,e,p1)) - - # compute algebraic intersection with preceding cycles - if intersection: - i = [] - for _,ee,_ in cycles: - if verbose: - print("compute intersection") - p_in = c.index(e[1]) - p_out = (c.index(e[0]) - p_in) % M - q_in = (c.index(ee[1]) - p_in) % M - q_out = (c.index(ee[0]) - p_in) % M - if verbose: - print(" after reduction: p_out = %d, q_in = %d, q_out = %d" % (p_out, q_in, q_out)) - - # compute intersection - # p_in = 0 and the others 3 are positive - if q_in < p_out and p_out < q_out: - i.append(1) - elif q_out < p_out and p_out < q_in: - i.append(-1) - else: - i.append(0) - - I.append(i) - - # make cycle as list - cycles = [p0[::-1]+[e]+[c[::-1] for c in p1] for p0,e,p1 in cycles] - - if intersection: - m = matrix(len(cycles)) - for j in range(len(I)): - for jj in range(len(I[j])): - m[j,jj] = I[j][jj] - m[jj,j] = -I[j][jj] - - return cycles, m - return cycles - - def is_cycle(self,c): - r""" - Test whether ``c`` is a cycle. + cycles, intersection_matrix = self._fat_graph.cycle_basis(True) + else: + cycles = self._fat_graph.cycle_basis(False) + cycles = [[(self._fat_graph_inv_map[x], self._fat_graph_inv_map[x ^ 1]) for x in cycle] for cycle in cycles] + return (cycles, intersection_matrix) if intersection else cycles - A *path* is a sequence of oriented edges such that each edge starts - where the preceding one ends. A *cycle* is a path which starts where it - ends. - """ + def is_cycle(self, c): for i in range(len(c)-1): if self.dart_to_vertex(c[i][1]) != self.dart_to_vertex(c[i+1][0]): return False @@ -1098,20 +755,17 @@ def is_cycle(self,c): return False return True + class RibbonGraphWithAngles(RibbonGraph): r""" - A Ribbon graph with angles between edges - - Currently angles can only be *rational* multiples of pi. - - TODO: - - - allows any kind of angles by providing a sum for the total and considering - each angle as a (projective) portion of the total angle. + Deprecated class. """ def __init__(self, vertices=None, edges=None, faces=None, angles=None): + from warnings import warn + warn('RibbonGraphWithAngles is deprecated; use surface_dynamics.fat_graph.FatGraph instead', DeprecationWarning) + r = RibbonGraph(vertices,edges,faces) - RibbonGraph.__init__(self,r.vertex_perm(),r.edge_perm(),r.face_perm()) + RibbonGraph.__init__(self, r.vertex_perm(), r.edge_perm(), r.face_perm()) if len(angles) != self.num_darts(): raise ValueError("there are %d angles and %d darts" %(len(angles),self.num_darts())) @@ -1132,9 +786,6 @@ def __init__(self, vertices=None, edges=None, faces=None, angles=None): raise ValueError("the angle of a face should be (nb_edges - 2) x pi") def angle_between_darts(self, d1, d2): - r""" - Return the angle between the darts ``d1`` and ``d2`` - """ v = self.vertex_orbit(d1) if d2 not in v: raise ValueError("d1=%s and d2=%s are not at the same vertex" %(str(d1),str(d2))) @@ -1147,24 +798,12 @@ def angle_between_darts(self, d1, d2): return a def angle_at_vertex(self,v): - r""" - Angle at a vertex (coefficient of pi) - """ return self._total_angle[v] def angle_at_vertices(self): - r""" - Return the list of angles at a vertex. - """ return self._total_angle def winding(self, c): - r""" - Return winding number along the cycle ``c``. - - This is NOT well defined because it depends on the way we choose to pass - on the left or on the right at singularity. - """ a = 0 for i in range(len(c)-1): d1 = c[i][1] @@ -1182,40 +821,27 @@ def winding(self, c): def holonomy_representation(self): r""" - Return the holonomy representation in `SO(2)` as two lists. - - The first list correspond to cycles around vertices, while the second - correspond to a cycle basis that generate homology. - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: e = '(0,1)(2,3)' sage: f = '(0,2,1,3)' sage: a = [1/2,1/2,1/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.holonomy_representation() ([0], [0, 0]) - - The standard cube:: - sage: e = tuple((i,i+1) for i in range(0,24,2)) sage: f = '(0,20,7,10)(16,22,19,21)(2,9,5,23)(14,3,17,1)(12,8,15,11)(18,4,13,6)' sage: a = [1/2]*24 sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.holonomy_representation() ([3/2, 3/2, 3/2, 3/2, 3/2, 3/2, 3/2, 3/2], []) - - Two copies of a triangle:: - sage: e = '(0,1)(2,3)(4,5)' sage: f = '(0,2,4)(1,5,3)' sage: a = [1/2,1/6,1/3,1/3,1/6,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.holonomy_representation() ([1, 1/2, 1/2], []) - sage: a = [1/3,7/15,1/5,1/5,7/15,1/3] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.holonomy_representation() @@ -1237,19 +863,15 @@ def holonomy_representation(self): def has_trivial_holonomy(self): r""" - Test whether self has trivial holonomy representation - - EXAMPLES:: + TESTS:: sage: from surface_dynamics import * - sage: e = '(0,1)(2,3)' sage: f = '(0,2,1,3)' sage: a = [1/2,1/2,1/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.has_trivial_holonomy() True - sage: e = '(0,1)(2,3)(4,5)' sage: f = '(0,2,4)(1,5,3)' sage: a = [1/3,7/15,1/5,1/5,7/15,1/3] @@ -1260,51 +882,35 @@ def has_trivial_holonomy(self): l1,l2 = self.holonomy_representation() return all(i==0 for i in l1) and all(i==0 for i in l2) - def spin_parity(self,check=True,verbose=False): + def spin_parity(self, check=True, verbose=False): r""" - Return the spin parity of the Ribbon graph with angles. - - The surface should be holonomy free and with odd multiple of 2 pi - angles. Implements the formula of [Joh80]_. - - EXAMPLES: + TESTS:: sage: from surface_dynamics import * - - We first consider the case of the torus:: - sage: e = '(0,1)(2,3)' sage: f = '(0,2,1,3)' sage: a = [1/2,1/2,1/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.spin_parity() 1 - - Then the case of genus 2 surface (with an angle of 6pi):: - sage: e = '(0,1)(2,3)(4,5)(6,7)' sage: f = '(0,2,4,3,6,1,7,5)' sage: a = [1/2,1/2,1,1/2,1/2,1,3/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.spin_parity() 1 - sage: e = '(0,1)(2,3)(4,5)(6,7)' sage: f = '(0,2,4,6,1,3,5,7)' sage: a = [1/2,1/2,1,1,1,1,1/2,1/2] sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.spin_parity() 1 - sage: e = '(0,1)(2,3)(4,5)(6,7)' sage: f = '(0,2,4,6,1,3,5,7)' sage: a = [3/4]*8 sage: r = RibbonGraphWithAngles(edges=e,faces=f,angles=a) sage: r.spin_parity() 1 - - In genus 3 two spin parities occur for one conical angle 10pi:: - sage: e = '(0,1)(2,3)(4,5)(6,7)(8,9)(10,11)' sage: f1 = '(0,4,6,8,10,2,1,9,11,5,7,3)' sage: f2 = '(0,4,6,8,10,2,1,5,7,9,11,3)' @@ -1326,7 +932,7 @@ def spin_parity(self,check=True,verbose=False): GF2 = GF(2) - c,M = self.cycle_basis(intersection=True) + c, M = self.cycle_basis(intersection=True) winding = [] for cc in c: @@ -1381,6 +987,7 @@ def spin_parity(self,check=True,verbose=False): return s + def angle(v): r""" Return the argument of the vector ``v``. @@ -1399,21 +1006,3 @@ def angle(v): return acos(x / r) / pi else: return -acos(x / r) / pi - -class RibbonGraphWithHolonomies(RibbonGraph): - r""" - A Ribbon graph with holonomies. - - For now - """ - def __init__(self, vertices=None, edges=None, faces=None, holonomies=None): - r = RibbonGraph(vertices,edges,faces) - RibbonGraph.__init__(self,r.vertex_perm(),r.edge_perm(),r.face_perm()) - - if len(holonomies) != self.num_darts(): - raise ValueError("there are %d angles and %d darts" %(len(angles),self.num_darts())) - - V = FreeModule(ZZ, 2) - self._holonomies = list(map(V, holonomies)) - - #self._angles = map(angle, self._holonomies) diff --git a/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx b/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx index b349327a..aa2c70d9 100644 --- a/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx +++ b/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx @@ -1466,8 +1466,8 @@ cdef class Origami_dense_pyx: True """ cdef int i - cdef int *rr = malloc(2*self._n*sizeof(int)) - cdef int *uu = rr + self._n + cdef int * rr = malloc(2 * self._n * sizeof(int)) + cdef int * uu = rr + self._n if cylinder is not None: @@ -1971,6 +1971,45 @@ cdef class Origami_dense_pyx: return res[:nb_vectors] + def fat_graph(self, verticals=False): + r""" + EXAMPLES:: + + sage: from surface_dynamics.all import Origami + sage: o = Origami('(1,2)', '(1,3)') + sage: o.fat_graph() + FatGraph('(0,3,5,2,8,11,9,6,4,7,1,10)', '(0,7,9,2)(1,10,8,11)(3,5,6,4)') + sage: o = Origami('(1,2,3,4,5)', '(1,6)(2,7)') + sage: o.fat_graph() + FatGraph('(0,3,17,2,20,23,21,6,24,27,25,10,8,11,5,26,4,7,1,22)(9,14,12,15)(13,18,16,19)', '(0,7,21,2)(1,22,20,23)(3,17,18,16)(4,11,25,6)(5,26,24,27)(8,15,9,10)(12,19,13,14)') + """ + # for the square number i we label edges as + # + # x---------------x + # | 4u(i)+1 | + # |4i+2 | + # | | + # | | + # | 4r(i) | + # | 4i +3 | + # x---------------x + fp = [-1] * (4 * self._n) + for i in range(self._n): + down = 4 * i + right = 4 * self._r[i] + 3 + up = 4 * self._u[i] + 1 + left = 4 * i + 2 + fp[down] = right + fp[right] = up + fp[up] = left + fp[left] = down + from surface_dynamics.topology.fat_graph import FatGraph + fat_graph = FatGraph(fp=fp) + if verticals: + return fat_graph, [4 * i for i in range(self._n)] + [4 * i + 1 for i in range(self._n)] + else: + return fat_graph + def as_graph(self): r""" Return the graph associated to self @@ -2087,6 +2126,28 @@ cdef class Origami_dense_pyx: # Component of stratum # + def spin_parity(self): + r""" + Return the spin parity of this origami. + + TESTS:: + + sage: from surface_dynamics import AbelianStratum + sage: for z in [[6], [4, 2], [2, 2, 2]]: + ....: A = AbelianStratum(*z) + ....: o0 = A.even_component().one_origami() + ....: o1 = A.odd_component().one_origami() + ....: for _ in range(20): + ....: assert o0.spin_parity() == 0 + ....: assert o1.spin_parity() == 1 + ....: o0 = o0.vertical_twist(cylinder=randrange(o0.nb_squares())) + ....: o0 = o0.horizontal_twist(cylinder=randrange(o0.nb_squares())) + ....: o1 = o1.vertical_twist(cylinder=randrange(o1.nb_squares())) + ....: o1 = o1.horizontal_twist(cylinder=randrange(o1.nb_squares())) + """ + fg, verticals = self.fat_graph(verticals=True) + return fg.arf_invariant(verticals) + def stratum_component(self, fake_zeros=False, verbose=False): r""" Return the component of stratum this origami belongs to. diff --git a/surface_dynamics/flat_surfaces/separatrix_diagram.py b/surface_dynamics/flat_surfaces/separatrix_diagram.py index 9176b926..ecdd08c2 100644 --- a/surface_dynamics/flat_surfaces/separatrix_diagram.py +++ b/surface_dynamics/flat_surfaces/separatrix_diagram.py @@ -3423,6 +3423,67 @@ def smallest_integer_lengths(self): # homology # + def fat_graph(self, verticals=False): + r""" + Return a fat graph which is obtained by adding a transverse edge in each cylinder. + + If ``verticals`` is set to ``True``, also return a list of half-edges that are + after a vertical direction. This data can be used to compute the stratum or the + spin structure (see examples below). + + EXAMPLES:: + + sage: from surface_dynamics import CylinderDiagram + sage: cd = CylinderDiagram([((0,1),(0,2)),((2,),(1,))]) + sage: fg = cd.fat_graph() + sage: fg + FatGraph('(0,7,3,8,2,1,6,4,9,5)', '(0,2,7,1,5,6)(3,8,4,9)') + sage: fg.genus() + 2 + + sage: cd = CylinderDiagram('(0,1)-(0,3,6) (2,4)-(5) (3)-(2) (5,6)-(1,4)') + sage: fg, verticals = cd.fat_graph(verticals=True) + sage: fg.angles(verticals) # in multiple of pi + [14] + sage: fg.spin_parity(verticals) + 1 + + TESTS:: + + sage: from surface_dynamics import AbelianStratum + sage: for cd in AbelianStratum(2, 2).odd_component().cylinder_diagrams(): + ....: fg, verticals = cd.fat_graph(verticals=True) + ....: assert fg.num_faces() == cd.ncyls() + ....: assert fg.angles(verticals) == [6, 6] + ....: assert fg.spin_parity(verticals) == 1 + sage: for cd in AbelianStratum(6).even_component().cylinder_diagrams(): + ....: fg, verticals = cd.fat_graph(verticals=True) + ....: assert fg.num_faces() == cd.ncyls() + ....: assert fg.angles(verticals) == [14] + ....: assert fg.spin_parity(verticals) == 0 + """ + # separatrix i in bottom -> 2i + # separatrix i in top -> 2i+1 + n = self.nseps() + m = self.ncyls() + fp = [-1] * (2 * (n + m)) + for j, (b, t) in enumerate(self.cylinders()): + fp [2 * (n + j)] = 2 * b[0] + for i in range(len(b) - 1): + fp[2 * b[i]] = 2 * b[i + 1] + fp[2 * b[-1]] = 2 * (n + j) + 1 + fp[2 * (n + j) + 1] = 2 * t[0] + 1 + for i in range(len(t) - 1): + fp[2 * t[i] + 1] = 2 * t[i + 1] + 1 + fp[2 * t[-1] + 1] = 2 * (n + j) + + from surface_dynamics.topology.fat_graph import FatGraph + fat_graph = FatGraph(fp=fp) + if verticals: + return fat_graph, list(range(2 * n)) + else: + return fat_graph + def to_ribbon_graph(self): r""" Return a ribbon graph @@ -3443,7 +3504,13 @@ def to_ribbon_graph(self): sage: C = CylinderDiagram([((0,1),(0,2)),((2,),(1,))]) sage: C.stratum() H_2(2) - sage: R = C.to_ribbon_graph(); R + sage: R = C.to_ribbon_graph() + doctest:warning + ... + DeprecationWarning: RibbonGraphWithAngles is deprecated; use surface_dynamics.fat_graph.FatGraph instead + ... + DeprecationWarning: RibbonGraph is deprecated; use surface_dynamics.fat_graph.FatGraph instead + sage: R Ribbon graph with 1 vertex, 5 edges and 2 faces sage: l,m = R.cycle_basis(intersection=True) sage: m.rank() == 2 * C.genus() @@ -3500,8 +3567,6 @@ def to_ribbon_graph_with_holonomies(self, lengths, heights, twists): return RibbonGraphWithHolonomies(edges=edges,faces=faces,holonomies=holonomies) - - def spin_parity(self): r""" Return the spin parity of any surface that is built from this cylinder @@ -3509,7 +3574,7 @@ def spin_parity(self): EXAMPLES:: - sage: from surface_dynamics import * + sage: from surface_dynamics import CylinderDiagram sage: c = CylinderDiagram('(0,1,2,3,4)-(0,1,2,3,4)') sage: c.spin_parity() @@ -3525,9 +3590,8 @@ def spin_parity(self): sage: c.spin_parity() 1 """ - if any(z%2 for z in self.stratum().zeros()): - return None - return self.to_ribbon_graph().spin_parity() + fat_graph, verticals = self.fat_graph(verticals=True) + return fat_graph.spin_parity(verticals) # def circumferences_of_cylinders(self,ring=None): # r""" diff --git a/surface_dynamics/flat_surfaces/strata.py b/surface_dynamics/flat_surfaces/strata.py index 235921ea..be086fb6 100644 --- a/surface_dynamics/flat_surfaces/strata.py +++ b/surface_dynamics/flat_surfaces/strata.py @@ -14,9 +14,6 @@ # https://www.gnu.org/licenses/ #***************************************************************************** -from __future__ import print_function, absolute_import, division -from six.moves import range, map, filter, zip - from functools import total_ordering from sage.structure.unique_representation import UniqueRepresentation @@ -478,6 +475,7 @@ def masur_veech_volume(self, rational=False, method=None): from .masur_veech_volumes import masur_veech_volume return masur_veech_volume(self, rational, method) + class StratumComponent(SageObject): r""" Generic class for connected component of a stratum of flat surfaces. diff --git a/surface_dynamics/interval_exchanges/template.py b/surface_dynamics/interval_exchanges/template.py index 7c50ac8a..d54ce053 100644 --- a/surface_dynamics/interval_exchanges/template.py +++ b/surface_dynamics/interval_exchanges/template.py @@ -4027,7 +4027,52 @@ def genus(self) : p = self.profile() return Integer((sum(p)-len(p))//2+1) - def arf_invariant(self): + def fat_graph(self, verticals=False, mutable=False, check=True): + r""" + Return the unicellular fat graph corresponding to this permutation. + + The bottom edges are numbered 0, 2, 4, ... while the top edges are numbered 1, 3, ... + + EXAMPLES:: + + sage: from surface_dynamics import iet + sage: p = iet.Permutation('0 1 2 3 4 5 6 7', '6 5 4 3 2 0 7 1') + sage: p.fat_graph() + FatGraph('(0,5,6,9,10,13,14,1,2,15,3,4,7,8,11,12)', '(0,14,2,15,13,11,9,7,5,3,1,12,10,8,6,4)') + + sage: p = iet.Permutation('0 1 2 3 4 5 6 7', '4 3 6 5 2 0 7 1') + sage: p.fat_graph() + FatGraph('(0,5,6,9,10,13,14,1,2,15,3,4,11,12,7,8)', '(0,14,2,15,13,11,9,7,5,3,1,8,6,12,10,4)') + """ + if self._labels is not None: + top, bot = self._labels + else: + top = list(range(len(self._twin[0]))) + bot = self._twin[1] + n = len(top) + fp = [0] * 2 * n + for i in range(n - 1): + e = bot[i] + f = bot[i + 1] + fp[2 * e] = 2 * f + + e = top[i + 1] + f = top[i] + fp[2 * e + 1] = 2 * f + 1 + + fp[2 * top[0] + 1] = 2 * bot[0] + fp[2 * bot[-1]] = 2 * top[-1] + 1 + + + from surface_dynamics.topology.fat_graph import FatGraph + fat_graph = FatGraph(fp=fp, mutable=mutable, check=check) + + if verticals: + return fat_graph, [2 * i for i in bot[1:]] + [2 * i + 1 for i in top[:-1]] + else: + return fat_graph + + def spin_parity(self): r""" Returns the Arf invariant of the permutation. @@ -4064,10 +4109,10 @@ def arf_invariant(self): sage: b1 = [3,2,4,6,5,7,9,8,1,0] sage: b0 = [6,5,4,3,2,7,9,8,1,0] sage: p1 = iet.Permutation(a,b1) - sage: p1.arf_invariant() + sage: p1.spin_parity() 1 sage: p0 = iet.Permutation(a,b0) - sage: p0.arf_invariant() + sage: p0.spin_parity() 0 Permutations from the odd and even component of H(4,4):: @@ -4076,44 +4121,32 @@ def arf_invariant(self): sage: b1 = [3,2,5,4,6,8,7,10,9,1,0] sage: b0 = [5,4,3,2,6,8,7,10,9,1,0] sage: p1 = iet.Permutation(a,b1) - sage: p1.arf_invariant() + sage: p1.spin_parity() 1 sage: p0 = iet.Permutation(a,b0) - sage: p0.arf_invariant() + sage: p0.spin_parity() 0 - """ - if any((z+1)%2 for z in self.profile()): - return None - - from sage.rings.finite_rings.finite_field_constructor import GF - GF2 = GF(2) - - M = self.intersection_matrix(GF2) - F, C = M.symplectic_form() - g = F.rank() // 2 - n = F.ncols() - - s = GF2(0) - for i in range(g): - a = C.row(i) - - a_indices = [k for k in range(n) if a[k]] - t_a = GF2(len(a_indices)) - for j1 in range(len(a_indices)): - for j2 in range(j1+1,len(a_indices)): - t_a += M[a_indices[j1], a_indices[j2]] - - b = C.row(g+i) - b_indices = [k for k in range(n) if b[k]] - t_b = GF2(len(b_indices)) - for j1 in range(len(b_indices)): - for j2 in range(j1+1,len(b_indices)): - t_b += M[b_indices[j1],b_indices[j2]] + TESTS:: - s += t_a * t_b + sage: from surface_dynamics import AbelianStratum + sage: for z in [[6], [4, 2], [2, 2, 2]]: + ....: A = AbelianStratum(*z) + ....: p0 = A.even_component().permutation_representative() + ....: p1 = A.odd_component().permutation_representative() + ....: for _ in range(20): + ....: assert p0.spin_parity() == 0 + ....: assert p1.spin_parity() == 1 + ....: p0 = p0.rauzy_move(randrange(2)) + ....: p1 = p1.rauzy_move(randrange(2)) + """ + if any((z + 1) % 2 for z in self.profile()): + return None + fg, verticals = self.fat_graph(verticals=True) + return fg.spin_parity(verticals) - return s + # TODO: deprecate + arf_invariant = spin_parity def stratum_component(self): r""" From 17da29abc0f2c58ff3b132ad351a18c1d6fb08e1 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 09:27:14 +0200 Subject: [PATCH 05/17] news entry --- doc/news/homology.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/news/homology.rst diff --git a/doc/news/homology.rst b/doc/news/homology.rst new file mode 100644 index 00000000..40f71790 --- /dev/null +++ b/doc/news/homology.rst @@ -0,0 +1,7 @@ +**Added:** + +* Homology of fat graph and generic spin parity computation + +**Deprecated:** + +* RibbonGraph are deprecated in favor of FatGraph From 053a3500004c932102287cecd2b483cd38cdcb3b Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 18:09:04 +0200 Subject: [PATCH 06/17] linting --- surface_dynamics/misc/factored_denominator.py | 1 - surface_dynamics/topology/homology.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/surface_dynamics/misc/factored_denominator.py b/surface_dynamics/misc/factored_denominator.py index 3d592d56..1c8f7a70 100644 --- a/surface_dynamics/misc/factored_denominator.py +++ b/surface_dynamics/misc/factored_denominator.py @@ -1129,4 +1129,3 @@ def _element_constructor_(self, arg): """ num = self._polynomial_ring(arg) return self.element_class(self, [([], num)], self.free_module()) - diff --git a/surface_dynamics/topology/homology.py b/surface_dynamics/topology/homology.py index e4693107..244a98a9 100644 --- a/surface_dynamics/topology/homology.py +++ b/surface_dynamics/topology/homology.py @@ -450,5 +450,3 @@ def element_from_vector(self, v): (0, 0, 0, 1) """ return self.element_class(self, v) - - From 6a7ac5f803a5b0e1b019156033dfa3faec7928e9 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 19:15:35 +0200 Subject: [PATCH 07/17] fix iscc doctests --- surface_dynamics/misc/iscc.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/surface_dynamics/misc/iscc.py b/surface_dynamics/misc/iscc.py index bfc84fe4..f4e1ef6b 100644 --- a/surface_dynamics/misc/iscc.py +++ b/surface_dynamics/misc/iscc.py @@ -1,5 +1,5 @@ # **************************************************************************** -# Copyright (C) 2019 Vincent Delecroix <20100.delecroix@gmail.com> +# Copyright (C) 2019-2023 Vincent Delecroix <20100.delecroix@gmail.com> # # Distributed under the terms of the GNU General Public License (GPL) # as published by the Free Software Foundation; either version 2 of @@ -9,6 +9,33 @@ from sage.cpython.string import bytes_to_str, str_to_bytes +def iscc_fg_string(fg): + r""" + EXAMPLES:: + + sage: from surface_dynamics import FatGraph + sage: from surface_dynamics.misc.iscc import iscc_fg_string + sage: r = FatGraph(fp='(0,2,4,6,7)(8,9,5,3,1)') + sage: iscc_fg_string(r) + '[b0, b1] -> { [l0, l1, l2, l3, l4] : l0 >= 0 and l1 >= 0 and l2 >= 0 and l3 >= 0 and l4 >= 0 and b0 = l0 + l1 + l2 + l3 + l3 and b1 = l0 + l4 + l4 + l2 + l1 }' + """ + boundary_vars = ', '.join('b%d'%i for i in range(fg.num_faces())) + lengths_vars = ', '.join('l%d'%i for i in range(fg.num_edges())) + + ieqs = ' and '.join('l%d >= 0' % i for i in range(fg.num_edges())) + + eqns = [] + for i, face in enumerate(fg.faces()): + eqns.append('b%d = %s' % (i, ' + '.join('l%d' % (j // 2) for j in face))) + eqns = ' and '.join(eqns) + + return "[{boundary_vars}] -> {{ [{lengths_vars}] : {ieqs} and {eqns} }}".format( + boundary_vars=boundary_vars, + lengths_vars=lengths_vars, + ieqs=ieqs, + eqns=eqns) + + def iscc_rg_string(r): r""" EXAMPLES:: @@ -16,6 +43,9 @@ def iscc_rg_string(r): sage: from surface_dynamics import * sage: from surface_dynamics.misc.iscc import iscc_rg_string sage: r = RibbonGraph(faces='(0,2,4,6,7)(8,9,5,3,1)', edges='(0,1)(2,3)(4,5)(6,7)(8,9)') + doctest:warning + ... + DeprecationWarning: RibbonGraph is deprecated; use surface_dynamics.fat_graph.FatGraph instead sage: iscc_rg_string(r) '[b0, b1] -> { [l0, l1, l2, l3, l4, l5, l6, l7, l8, l9] : l0 >= 0 and l1 >= 0 and l2 >= 0 and l3 >= 0 and l4 >= 0 and l5 >= 0 and l6 >= 0 and l7 >= 0 and l8 >= 0 and l9 >= 0 and l0 = l1 and l2 = l3 and l4 = l5 and l6 = l7 and l8 = l9 and b0 = l0 + l2 + l4 + l6 + l7 and b1 = l1 + l8 + l9 + l5 + l3 }' """ @@ -94,6 +124,8 @@ def iscc_card(arg): poly = iscc_cd_string(arg) elif isinstance(arg, RibbonGraph): poly = iscc_rg_string(arg) + elif isinstance(arg, FatGraph): + poly = iscc_fg_string(arg) from subprocess import Popen, PIPE From 6a6689589fe4c66c6f350d75c5d2edc9f14600a2 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 19:47:02 +0200 Subject: [PATCH 08/17] restore RibbonGraphWithHolonomies --- surface_dynamics/flat_surfaces/homology.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/surface_dynamics/flat_surfaces/homology.py b/surface_dynamics/flat_surfaces/homology.py index 954bd5f4..bd982a6a 100644 --- a/surface_dynamics/flat_surfaces/homology.py +++ b/surface_dynamics/flat_surfaces/homology.py @@ -1006,3 +1006,23 @@ def angle(v): return acos(x / r) / pi else: return -acos(x / r) / pi + + +class RibbonGraphWithHolonomies(RibbonGraph): + r""" + A Ribbon graph with holonomies. + + For now + """ + def __init__(self, vertices=None, edges=None, faces=None, holonomies=None): + from warnings import warn + warn('RibbonGraphWithHolonomies is deprecated; use surface_dynamics.fat_graph.FatGraph instead', DeprecationWarning) + + r = RibbonGraph(vertices,edges,faces) + RibbonGraph.__init__(self,r.vertex_perm(),r.edge_perm(),r.face_perm()) + + if len(holonomies) != self.num_darts(): + raise ValueError("there are %d angles and %d darts" %(len(angles),self.num_darts())) + + V = FreeModule(ZZ, 2) + self._holonomies = list(map(V, holonomies)) From 1d01b0eb0b13e1c94990000982312af9204e54be Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 19:56:07 +0200 Subject: [PATCH 09/17] rewrite lattice_of_periods for origamis --- .../flat_surfaces/origamis/origami_dense.pyx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx b/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx index aa2c70d9..db82ca39 100644 --- a/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx +++ b/surface_dynamics/flat_surfaces/origamis/origami_dense.pyx @@ -2146,7 +2146,7 @@ cdef class Origami_dense_pyx: ....: o1 = o1.horizontal_twist(cylinder=randrange(o1.nb_squares())) """ fg, verticals = self.fat_graph(verticals=True) - return fg.arf_invariant(verticals) + return fg.spin_parity(verticals) def stratum_component(self, fake_zeros=False, verbose=False): r""" @@ -3394,15 +3394,24 @@ cdef class Origami_dense_pyx: sage: o = Origami('(1,2,3,4)(5,6)', '(1,5)(2,6)') sage: o.absolute_period_generators() - [(2, 0), (2, 0), (0, 1), (0, 1)] + [(2, 0), (0, 1), (0, 1), (2, 0)] """ - cyl, lengths, heights, twists = self.cylinder_diagram(data=True) - r = cyl.to_ribbon_graph_with_holonomies(lengths, heights, twists) + fg = self.fat_graph() periods = [] - for c in r.cycle_basis(): - s = sum(r._holonomies[e[0]] for e in c) - if s: - periods.append(s) + for c in fg.cycle_basis(): + s = [0, 0] + for e in c: + # NOTE: the edge labelling tells us about the holonomy + if e % 4 == 0: + s[0] += 1 + elif e % 4 == 1: + s[0] -= 1 + elif e % 4 == 2: + s[1] += 1 + else: + s[1] -= 1 + if s[0] or s[1]: + periods.append(tuple(s)) return periods def stratum(self, fake_zeros=False): From 47984530e2b1f9248d1140a7460e531051ac10e5 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Mon, 23 Oct 2023 21:46:29 +0200 Subject: [PATCH 10/17] fix initialization of Reduced vs Labelled permutations --- .../flat_surfaces/abelian_strata.py | 20 ++++++++++++++++++- .../interval_exchanges/labelled.py | 20 +++++++++++++++---- .../interval_exchanges/reduced.py | 18 ++++++++++++++--- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/surface_dynamics/flat_surfaces/abelian_strata.py b/surface_dynamics/flat_surfaces/abelian_strata.py index 1f717bdf..bbd069b0 100644 --- a/surface_dynamics/flat_surfaces/abelian_strata.py +++ b/surface_dynamics/flat_surfaces/abelian_strata.py @@ -2192,6 +2192,23 @@ def permutation_representative(self, left_degree=None, reduced=True, alphabet=No 1 sage: p.rauzy_diagram() Rauzy diagram with 20 permutations + + TESTS:: + + sage: from surface_dynamics import AbelianStratum + sage: assert AbelianStratum(6).hyperelliptic_component().permutation_representative(reduced=True)._labels is None + sage: assert AbelianStratum(6).odd_component().permutation_representative(reduced=True)._labels is None + sage: assert AbelianStratum(6).even_component().permutation_representative(reduced=True)._labels is None + sage: assert AbelianStratum(3, 3).hyperelliptic_component().permutation_representative(reduced=True)._labels is None + sage: assert AbelianStratum(3, 3).non_hyperelliptic_component().permutation_representative(reduced=True)._labels is None + sage: assert AbelianStratum(1, 1, 1, 1).unique_component().permutation_representative(reduced=True)._labels is None + + sage: assert AbelianStratum(6).hyperelliptic_component().permutation_representative(reduced=False)._labels is not None + sage: assert AbelianStratum(6).odd_component().permutation_representative(reduced=False)._labels is not None + sage: assert AbelianStratum(6).even_component().permutation_representative(reduced=False)._labels is not None + sage: assert AbelianStratum(3, 3).hyperelliptic_component().permutation_representative(reduced=False)._labels is not None + sage: assert AbelianStratum(3, 3).non_hyperelliptic_component().permutation_representative(reduced=False)._labels is not None + sage: assert AbelianStratum(1, 1, 1, 1).unique_component().permutation_representative(reduced=False)._labels is not None """ g = self._stratum.genus() n = self._stratum.nb_fake_zeros() @@ -2239,14 +2256,15 @@ def permutation_representative(self, left_degree=None, reduced=True, alphabet=No if reduced: from surface_dynamics.interval_exchanges.reduced import ReducedPermutationIET p = ReducedPermutationIET([l0, l1]) - else: from surface_dynamics.interval_exchanges.labelled import LabelledPermutationIET p = LabelledPermutationIET([l0, l1]) + if alphabet is not None: p.alphabet(alphabet) elif relabel: p.alphabet(range(len(p))) + return p def rauzy_class_cardinality(self, left_degree=None, reduced=True): diff --git a/surface_dynamics/interval_exchanges/labelled.py b/surface_dynamics/interval_exchanges/labelled.py index 3e4b5c69..ddb79304 100644 --- a/surface_dynamics/interval_exchanges/labelled.py +++ b/surface_dynamics/interval_exchanges/labelled.py @@ -109,10 +109,10 @@ from sage.matrix.constructor import identity_matrix from sage.rings.integer import Integer -from .template import (OrientablePermutationIET, OrientablePermutationLI, - FlippedPermutationIET, FlippedPermutationLI, - RauzyDiagram, FlippedRauzyDiagram, - interval_conversion, side_conversion) +from .template import (Permutation, OrientablePermutationIET, + OrientablePermutationLI, FlippedPermutationIET, FlippedPermutationLI, + RauzyDiagram, FlippedRauzyDiagram, interval_conversion, + side_conversion) class LabelledPermutation(SageObject): @@ -533,6 +533,9 @@ class LabelledPermutationIET(LabelledPermutation, OrientablePermutationIET): sage: p in d True """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, False, flips) + def reduced(self): r""" Returns the associated reduced abelian permutation. @@ -743,6 +746,9 @@ class LabelledPermutationLI(LabelledPermutation, OrientablePermutationLI): sage: p in r True """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, False, flips) + def has_right_rauzy_move(self, winner): r""" Test of Rauzy movability with a specified winner) @@ -1196,6 +1202,9 @@ class FlippedLabelledPermutationIET(FlippedPermutationIET, LabelledPermutationIE sage: d = iet.RauzyDiagram('a b c d','d a b c',flips='a') """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, False, flips) + def reduced(self): r""" The associated reduced permutation. @@ -1262,6 +1271,9 @@ class FlippedLabelledPermutationLI(FlippedPermutationLI, LabelledPermutationLI): sage: p.has_rauzy_move(1) True """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, False, flips) + def reduced(self): r""" The associated reduced permutation. diff --git a/surface_dynamics/interval_exchanges/reduced.py b/surface_dynamics/interval_exchanges/reduced.py index 09680150..d7371dbf 100644 --- a/surface_dynamics/interval_exchanges/reduced.py +++ b/surface_dynamics/interval_exchanges/reduced.py @@ -64,9 +64,9 @@ from sage.combinat.words.alphabet import Alphabet from sage.rings.integer import Integer -from .template import OrientablePermutationIET, OrientablePermutationLI # permutations -from .template import FlippedPermutationIET, FlippedPermutationLI # flipped permutations -from .template import RauzyDiagram, FlippedRauzyDiagram +from .template import (Permutation, OrientablePermutationIET, + OrientablePermutationLI, FlippedPermutationIET, FlippedPermutationLI, + RauzyDiagram, FlippedRauzyDiagram) from .template import interval_conversion, side_conversion @@ -200,6 +200,9 @@ class ReducedPermutationIET(ReducedPermutation, OrientablePermutationIET): sage: d_red.cardinality() 6 """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, True, flips) + def list(self): r""" Returns a list of two list that represents the permutation. @@ -485,6 +488,9 @@ class ReducedPermutationLI(ReducedPermutation, OrientablePermutationLI): sage: d_red.cardinality() 4 """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, True, flips) + def list(self): r""" The permutations as a list of two lists. @@ -589,6 +595,9 @@ class FlippedReducedPermutationIET( c -b a ******** """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, True, flips) + def list(self, flips=False): r""" Returns a list representation of self. @@ -658,6 +667,9 @@ class FlippedReducedPermutationLI( sage: p = iet.GeneralizedPermutation('a a b', 'b c c', reduced=True, flips='a') """ + def __init__(self, intervals=None, alphabet=None, reduced=False, flips=None): + Permutation.__init__(self, intervals, alphabet, True, flips) + def list(self, flips=False): r""" Returns a list representation of self. From 463fa550d654f2a5d249594850270ab53cf63c00 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 15:23:10 +0200 Subject: [PATCH 11/17] remove construction of PillowCaseCover from QuadraticCylinderDiagram --- .../flat_surfaces/separatrix_diagram.py | 662 ------------------ 1 file changed, 662 deletions(-) diff --git a/surface_dynamics/flat_surfaces/separatrix_diagram.py b/surface_dynamics/flat_surfaces/separatrix_diagram.py index ecdd08c2..7f5255b7 100644 --- a/surface_dynamics/flat_surfaces/separatrix_diagram.py +++ b/surface_dynamics/flat_surfaces/separatrix_diagram.py @@ -4642,665 +4642,3 @@ def widths_generating_series(self, var='w'): ans += f1 return ans - - def _lengths_to_dart_x_coords(self, lengths): - r""" - EXAMPLES:: - - sage: from surface_dynamics.flat_surfaces.separatrix_diagram import QuadraticCylinderDiagram - sage: from surface_dynamics import * - sage: r = RibbonGraph(vertices='(0,3,1,2)(4,7,5,6)', - ....: edges='(0,1)(2,3)(4,5)(6,7)', - ....: faces='(0,3,1,2)(4,7,5,6)', connected=False) - sage: q = QuadraticCylinderDiagram(r, [1,0]) - sage: q._lengths_to_dart_x_coords(lengths=[2,2,2,2]) - [0, 4, 6, 2, 0, 4, 6, 2] - sage: q._lengths_to_dart_x_coords(lengths=[1,2,3,4]) - [0, 3, 4, 1, 0, 7, 10, 3] - """ - G = self._g - dart_x_coords = [None] * G.num_darts() - for f in G.faces(): - dart_x_coords[f[0]] = 0 - w = lengths[G._dart_to_edge_index[f[0]]] - for i in range(1, len(f)): - dart_x_coords[f[i]] = w - w += lengths[G._dart_to_edge_index[f[i]]] - return dart_x_coords - - def _cylcoord_dart_to_corner(self, dart_x_coords, heights, twists, verbose=False): - r""" - TESTS:: - - sage: from surface_dynamics.flat_surfaces.separatrix_diagram import QuadraticCylinderDiagram - sage: from surface_dynamics import * - - The pillow:: - - sage: q = QuadraticCylinderDiagram('(0,0)-(1,1)') - sage: x = q._lengths_to_dart_x_coords([1, 1]) - sage: q._cylcoord_dart_to_corner(x, [1], [0]) - [0, 1, 3, 2] - sage: q._cylcoord_dart_to_corner(x, [1], [1]) - [0, 1, 2, 3] - sage: x = q._lengths_to_dart_x_coords([2, 2]) - sage: q._cylcoord_dart_to_corner(x, [1], [0]) - [0, 0, 3, 3] - sage: q._cylcoord_dart_to_corner(x, [1], [1]) - [0, 0, 2, 2] - - An example in `Q(2^2)` (everything is authorized):: - - sage: q = QuadraticCylinderDiagram('(0,1,0,1)-(2,3,2,3)') - sage: x = q._lengths_to_dart_x_coords([2,2,2,2]) - sage: q._cylcoord_dart_to_corner(x, [1], [0]) - [0, 0, 0, 0, 3, 3, 3, 3] - sage: q._cylcoord_dart_to_corner(x, [1], [1]) - [0, 0, 0, 0, 2, 2, 2, 2] - sage: q._cylcoord_dart_to_corner(x, [2], [0]) - [0, 0, 0, 0, 0, 0, 0, 0] - sage: q._cylcoord_dart_to_corner(x, [2], [1]) - [0, 0, 0, 0, 1, 1, 1, 1] - - Another example in `Q(2^2)`:: - - sage: q = QuadraticCylinderDiagram('(0,2,0,3)-(1,2,1,3)') - sage: x = q._lengths_to_dart_x_coords([1,2,2,1]) - sage: q._cylcoord_dart_to_corner(x, [2], [0]) - [0, 1, 1, 0, 0, 1, 1, 0] - - An example in `Q(3, -1^3)`:: - - sage: q = QuadraticCylinderDiagram('(0,1,1)-(0,2,2,3,3)') - sage: x = q._lengths_to_dart_x_coords([2, 1, 1, 1]) - sage: q._cylcoord_dart_to_corner(x, [2], [0]) - [0, 0, 1, 0, 0, 1, 0, 1] - sage: q._cylcoord_dart_to_corner(x, [1], [0]) - Traceback (most recent call last): - ... - ValueError: invalid lengths/heights/twists - sage: q._cylcoord_dart_to_corner(x, [1], [1]) - Traceback (most recent call last): - ... - ValueError: invalid lengths/heights/twists - - An example in `Q(4, 2, -1^2)`:: - - sage: q = QuadraticCylinderDiagram('(0,1,0,2)-(1,3,4,3) (2,4)-(5,5)') - sage: x = q._lengths_to_dart_x_coords([1, 2, 2, 1, 2, 2]) - sage: q._cylcoord_dart_to_corner(x, [2, 1], [1, 0]) - [0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 3, 3] - sage: q._cylcoord_dart_to_corner(x, [2, 1], [1, 1]) - [0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 2, 2] - sage: q._cylcoord_dart_to_corner(x, [2, 1], [0, 0]) - Traceback (most recent call last): - ... - ValueError: invalid lengths/heights/twists - """ - G = self._g - # pillowcase vertices - dart_to_corner = [None] * G.num_darts() # which vertex of the pillowcase the attached - dart_to_corner[0] = 0 - todo = set([0]) # dart from which we should complete vertices and faces - cross = set([0]) # dart from which we should cross cylinders - while todo or cross: - - # 1. completing vertices and faces - while todo: - i = todo.pop() - if verbose: - print('** new loop **') - print(' i = {}'.format(i)) - print(' dart_to_corner = {}'.format(dart_to_corner)) - - v = dart_to_corner[i] - assert v is not None - - for j in G.vertex_orbit(i): - if dart_to_corner[j] is None: - dart_to_corner[j] = v - todo.add(j) - elif dart_to_corner[j] != dart_to_corner[i]: - if verbose: - print('i = {} j = {} dart_to_corner[{}] = {} dart_to_corner[{}] = {}'.format( - i, j, i, dart_to_corner[i], j, dart_to_corner[j])) - raise ValueError('invalid lengths/heights/twists') - if verbose: - print('done completing vertex') - print(' dart_to_corner = {}'.format(dart_to_corner)) - - f = G.face_orbit(i) - m = dart_x_coords[i] % 2 - for j in f: - if dart_x_coords[j] % 2 == m: - if dart_to_corner[j] is None: - dart_to_corner[j] = v - todo.add(j) - elif dart_to_corner[j] != dart_to_corner[i]: - if verbose: - print('i = {} j = {} dart_to_corner[{}] = {} dart_to_corner[{}] = {}'.format( - i, j, i, dart_to_corner[i], j, dart_to_corner[j])) - raise ValueError('invalid lengths/heights/twists') - else: - # 0 <-> 1 and 2 <-> 3 - vv = v + 1 - 2 * (v % 2) - if dart_to_corner[j] is None: - dart_to_corner[j] = vv - todo.add(j) - elif dart_to_corner[j] != vv: - if verbose: - print('j = {} dart_to_corner[{}] = {}'.format(j,j,dart_to_corner[j])) - raise ValueError('invalid lengths/heights/twists') - - cross.add(min(f)) - - if verbose: - print('done completing face') - print(' dart_to_corner = {}'.format(dart_to_corner)) - - # 2. crossing cylinders - while cross: - # i: dart index - # j: face index - i = cross.pop() - v = dart_to_corner[i] - j = G._dart_to_face_index[i] - k = self._face_to_cylinder_index[j] - jj = self._p[j] - ii = G._face_cycles[jj][0] - - vv = v - if heights[k] % 2: - # 0 <-> 3 and 1 <-> 2 - vv = 3 - vv - if twists[k] % 2: - # 0 <-> 1 and 2 <-> 3 - vv = vv + 1 - 2 * (vv % 2) - - if verbose: - print('crossing cylinder from i={} to ii={}'.format(i, ii)) - print(' dart {} is above pillow vertex {}'.format(i, v)) - print(' dart {} is above pillow vertex {}'.format(ii, vv)) - - if dart_to_corner[ii] is None: - dart_to_corner[ii] = vv - todo.add(ii) - elif dart_to_corner[ii] != vv: - if verbose: - print('ii = {} dart_to_corner[{}] = {}'.format(ii, ii, dart_to_corner[ii])) - raise ValueError('invalid lengths/heights/twists') - - if verbose: - print('done crossing cylinder {} from dart {}'.format(j, i)) - print(' dart_to_corner = {}'.format(dart_to_corner)) - - if verbose: - print('dart_to_corner: {}'.format(dart_to_corner)) - - return dart_to_corner - - # figure out whether this awfull method is really useful... - # square tiled quadratic surface would be much more simple to handle - def cylcoord_to_pillowcase_cover(self, lengths, heights, twists=None, verbose=False): - r""" - Convert coordinates of the cylinders into a pillowcase cover. - - The pillow is considered as made of two 1 x 1 squares in order to avoid denominators - in the input ``lengths``, ``heights`` and ``twists``. - - INPUT: - - - ``lengths`` - positive integers - lengths of the separatrices - - - ``heights`` - positive integers - heights of the cylinders - - - ``twists`` - (optional) non-negative integers - twists. The twist - is measured as the difference in horizontal coordinates between - the smallest element in bottom and the smallest in element in top. - - OUTPUT: a pillowcase cover - - EXAMPLES:: - - sage: from surface_dynamics import * - - Some pillows in `Q(-1^4)`:: - - sage: q = QuadraticCylinderDiagram('(0,0)-(1,1)') - sage: q.cylcoord_to_pillowcase_cover([1,1],[1],[0]) - g0 = (1) - g1 = (1) - g2 = (1) - g3 = (1) - sage: q.cylcoord_to_pillowcase_cover([1,1],[1],[1]) - g0 = (1) - g1 = (1) - g2 = (1) - g3 = (1) - sage: q.cylcoord_to_pillowcase_cover([2,2],[1],[0]) - g0 = (1)(2) - g1 = (1,2) - g2 = (1,2) - g3 = (1)(2) - sage: q.cylcoord_to_pillowcase_cover([2,2],[1],[1]) - g0 = (1)(2) - g1 = (1,2) - g2 = (1)(2) - g3 = (1,2) - sage: q.cylcoord_to_pillowcase_cover([2,2],[1],[2]) == q.cylcoord_to_pillowcase_cover([2,2],[1],[2]) - True - sage: q.cylcoord_to_pillowcase_cover([3,3],[1],[0]) - g0 = (1)(2,3) - g1 = (1,3)(2) - g2 = (1,3)(2) - g3 = (1)(2,3) - sage: q.cylcoord_to_pillowcase_cover([3,3],[1],[1]) - g0 = (1)(2,3) - g1 = (1,3)(2) - g2 = (1)(2,3) - g3 = (1,2)(3) - - sage: q.cylcoord_to_pillowcase_cover([1,1],[2],[0]) - g0 = (1)(2) - g1 = (1)(2) - g2 = (1,2) - g3 = (1,2) - sage: q.cylcoord_to_pillowcase_cover([1,1],[2],[1]) == q.cylcoord_to_pillowcase_cover([1,1],[2],[0]) - True - - sage: q.cylcoord_to_pillowcase_cover([2,2],[2],[1]) - g0 = (1)(2)(3,4) - g1 = (1,2)(3)(4) - g2 = (1,3)(2,4) - g3 = (1,4)(2,3) - - Two one cylinder examples in `Q(2^2)`:: - - sage: q1 = QuadraticCylinderDiagram('(0,1,0,1)-(2,3,2,3)') - sage: q2 = QuadraticCylinderDiagram('(0,1,0,2)-(3,1,3,2)') - - sage: q1.cylcoord_to_pillowcase_cover([2,2,2,2],[1],[0]) - g0 = (1,2,3,4) - g1 = (1,3)(2,4) - g2 = (1,3)(2,4) - g3 = (1,4,3,2) - sage: q1.cylcoord_to_pillowcase_cover([2,2,2,2],[1],[1]) - g0 = (1,2,3,4) - g1 = (1,3)(2,4) - g2 = (1,4,3,2) - g3 = (1,3)(2,4) - sage: p = q1.cylcoord_to_pillowcase_cover([2,6,4,4],[3],[1]) - sage: p.stratum() - Q_2(2^2) - sage: p.nb_pillows() - 24 - - One two cylinders example in `Q(2^2)`:: - - sage: q = QuadraticCylinderDiagram('(0,1)-(2,3) (0,3)-(1,2)') - sage: p = q.cylcoord_to_pillowcase_cover([1,1,1,1], [2,2], [0,1]) - sage: p - g0 = (1,4,2,3) - g1 = (1,3,2,4) - g2 = (1,2)(3,4) - g3 = (1,2)(3,4) - sage: p.stratum() - Q_2(2^2) - sage: q.cylcoord_to_pillowcase_cover([1,3,1,3], [2,2], [0,1]).stratum() - Q_2(2^2) - - TESTS:: - - sage: from surface_dynamics import * - sage: q = QuadraticCylinderDiagram('(0,0)-(1,1)') - sage: q.cylcoord_to_pillowcase_cover([1,2],[1]) - Traceback (most recent call last): - ... - ValueError: sum of lengths on top and bottom differ - """ - from sage.rings.integer_ring import ZZ - - G = self._g - - if len(lengths) != G.num_edges(): - raise ValueError("'lengths' has wrong length") - if len(heights) != self.num_cylinders(): - raise ValueError("'heights' has wrong length") - - lengths = [ZZ.coerce(l) for l in lengths] - heights = [ZZ.coerce(h) for h in heights] - ZZ_zero = ZZ.zero() - for l in lengths: - if l <= ZZ_zero: - raise ValueError("each length should be positive") - for h in heights: - if h <= ZZ_zero: - raise ValueError("each height should be positive") - - widths = [] - for bot, top in self.cylinders(dart=False): - w1 = sum(lengths[i] for i in bot) - w2 = sum(lengths[i] for i in top) - if w1 != w2: - raise ValueError('sum of lengths on top and bottom differ') - widths.append(w1) - - areas = [heights[i] * widths[i] for i in range(self.ncyls())] - N = sum(areas) # number of squares - if N % 2: - raise ValueError('got an odd area') - N = N // 2 - if verbose: - print('area = {}'.format(N)) - - if twists is None: - twists = [ZZ_zero] * self.num_cylinders() - elif len(twists) != len(widths): - raise ValueError("the 'twists' vector has wrong length") - else: - twists = [ZZ.coerce(t) % w for (t,w) in zip(twists, widths)] - - # now glue the boundaries - # (distance with respect to the minimum dart in the face) - x = self._lengths_to_dart_x_coords(lengths) - dart_to_corner = self._cylcoord_dart_to_corner(x, heights, twists, verbose) - - # building dart_to_pillow - if verbose: - print('Constructing dart_to_pillow') - dart_to_pillow = [None] * G.num_darts() - n = 0 - for w,h,t,(bot,top) in zip(widths, heights, twists, self.cylinders(True)): - w = w // 2 - - if verbose: - print('new cylinder ({},{}) w={} h={} t={}'.format(bot,top,w,h,t)) - print('pillows in [n, n + h*w[ = [{}, {}['.format(n, n+h*w)) - # pillows in the bottom - dart_to_pillow[bot[0]] = n - nn = n - for i in range(len(bot) - 1): - j = bot[i] - l = lengths[G._dart_to_edge_index[j]] - if l % 2: - # changing vertex nature (0 <-> 1, 2 <-> 3) - v = dart_to_corner[j] - if verbose: - print(' odd length {} for dart {} at ({},{})'.format(l,j,nn,v)) - if v == 0 or v == 2: - nn += (l - 1) // 2 - else: - nn += (l + 1) // 2 - else: - # keeping vertex nature - if verbose: - print(' even length {} for dart {}'.format(l,j)) - nn += l // 2 - - if nn >= n + w: - nn -= w - if verbose: - print(' n={} w={} nn = {}'.format(n,w,nn)) - assert n <= nn < n + w - dart_to_pillow[bot[i+1]] = nn - - if verbose: - print(' dart to pillow done on bot') - print(' dart_to_pillow {}'.format(dart_to_pillow)) - - # pillow of top[0] (depends on twist) - if t % 2 == 0: - tadj = t // 2 - else: - v = dart_to_corner[bot[0]] - if v % 2: - tadj = (t+1) // 2 - else: - tadj = (t-1) // 2 - - nn = dart_to_pillow[bot[0]] + (h-1)*w + tadj - if nn >= n + h * w: - nn -= w - assert n + (h-1)*w <= nn < n + h * w - dart_to_pillow[top[0]] = nn - - if verbose: - print(' crossing cylinder: dart {} at ({},{})'.format( - top[0], dart_to_pillow[top[0]], dart_to_corner[top[0]])) - - # other pillows on top - for i in range(len(top) - 1): - j = top[i] - l = lengths[G._dart_to_edge_index[j]] - if l % 2: - # changing vertex nature (0 <-> 1, 2 <-> 3) - v = dart_to_corner[j] - if verbose: - print(' odd length {} for dart {} at ({},{})'.format(l,j,nn,v)) - if v == 0 or v == 2: - nn -= (l - 1) // 2 - else: - nn -= (l + 1) // 2 - if verbose: - print(' ends at {}'.format(nn)) - else: - # keeping vertex nature - if verbose: - print(' even length {} for dart {}'.format(l,j)) - nn -= l // 2 - - if nn < n + (h-1)*w: - nn += w - - assert n <= nn < n + h * w - dart_to_pillow[top[i+1]] = nn - - n += h * w - - if verbose: - print(' dart to pillow done on top') - print(' dart_to_pillow: {}'.format(dart_to_pillow)) - # safety check - assert all(i is None or 0 <= i < N for i in dart_to_pillow) - - # fill the cylinders and record the number at a dart - # (g01 and g23 correspond to horizontal displacements) - g0 = [None] * N - g1 = [None] * N - g2 = [None] * N - g3 = [None] * N - g = [g0, g1, g2, g3] - g01 = [None] * N # redundant information (will be used to glue edges) - g23 = [None] * N # redundant information (will be used to glue edges) - n = 0 - if verbose: - print('Filling permutations inside cylinders') - for w,h,t,(bot,top) in zip(widths, heights, twists, self.cylinders(True)): - w = w // 2 - if verbose: - print('** new cylinder **') - print(' w = {} h = {} t = {} n = {}'.format(w,h,t,n)) - - u = dart_to_corner[bot[0]] - if u < 2: - h0 = g0 - h1 = g1 - h2 = g2 - h3 = g3 - h01 = g01 - h23 = g23 - else: - h0 = g2 - h1 = g3 - h2 = g0 - h3 = g1 - h01 = g23 - h23 = g01 - - - # filling all rows but the top one - for k in range(h - 1): - if verbose: - print(' filling row k={} from n={}'.format(k, n)) - for i in range(n, n + w): - h2[i] = i + w - h2[i + w] = i - h3[i] = i + w - 1 - h3[i + w - 1] = i - h23[i] = i + 1 - h01[i + 1] = i - - # adjustments at the gluings (left and right of the cylinders) - h3[n] = n + 2*w - 1 - h3[n + 2*w - 1] = n - h23[n + w - 1] = n - h01[n] = n + w - 1 - - # 3 2 -> 1 0 - # 0 1 2 3 - h0, h1, h2, h3 = h2, h3, h0, h1 - h01, h23 = h23, h01 - n += w - if verbose: - print(' done') - print(' g0 = {}'.format(g0)) - print(' g1 = {}'.format(g1)) - print(' g2 = {}'.format(g2)) - print(' g3 = {}'.format(g3)) - print(' g01 = {}'.format(g01)) - print(' g23 = {}'.format(g23)) - - # top row - if verbose: - print(' filling top row from n={}'.format(n)) - for i in range(n, n + w - 1): - h23[i] = i + 1 - h01[i + 1] = i - h23[n + w - 1] = n - h01[n] = n + w - 1 - n += w - if verbose: - print(' done') - print(' g0 = {}'.format(g0)) - print(' g1 = {}'.format(g1)) - print(' g2 = {}'.format(g2)) - print(' g3 = {}'.format(g3)) - print(' g01 = {}'.format(g01)) - print(' g23 = {}'.format(g23)) - - if verbose: - print('done filling cylinders:') - print(' g0 : {}'.format(g0)) - print(' g1 : {}'.format(g1)) - print(' g2 : {}'.format(g2)) - print(' g3 : {}'.format(g3)) - print(' g01: {}'.format(g01)) - print(' g23: {}'.format(g23)) - - # safety check - assert all(i is None or 0 <= i < N for i in g0) - assert all(i is None or 0 <= i < N for i in g1) - assert all(i is None or 0 <= i < N for i in g2) - assert all(i is None or 0 <= i < N for i in g3) - assert all(i is not None and 0 <= i < N for i in g01) - assert all(i is not None and 0 <= i < N for i in g23) - assert all(g0.count(i) < 2 for i in range(N)) - assert all(g1.count(i) < 2 for i in range(N)) - assert all(g2.count(i) < 2 for i in range(N)) - assert all(g3.count(i) < 2 for i in range(N)) - - # gluing corners - for c in G._vertex_cycles: - i = dart_to_pillow[c[0]] - u = dart_to_corner[c[0]] - if verbose: - print('gluing vertex c = {}'.format(c)) - print(' c[0] = {} at ({},{})'.format(c[0], i, u)) - - for k in range(len(c)-1): - j = dart_to_pillow[c[k+1]] - v = dart_to_corner[c[k+1]] - if verbose: - print(' c[{}] = {} at ({},{})'.format(k+1, c[k+1], j, v)) - assert u == v - # safety check - g[u][i] = j - i = j - j = dart_to_pillow[c[0]] - v = dart_to_corner[c[0]] - g[u][i] = j - - if verbose: - print('done gluing corners') - print(' g0: {}'.format(g0)) - print(' g1: {}'.format(g1)) - print(' g2: {}'.format(g2)) - print(' g3: {}'.format(g3)) - - # safety check - assert all(i is None or 0 <= i < N for i in g0) - assert all(i is None or 0 <= i < N for i in g1) - assert all(i is None or 0 <= i < N for i in g2) - assert all(i is None or 0 <= i < N for i in g3) - assert all(i is not None and 0 <= i < N for i in g01) - assert all(i is not None and 0 <= i < N for i in g23) - assert all(g0.count(i) < 2 for i in range(N)) - assert all(g1.count(i) < 2 for i in range(N)) - assert all(g2.count(i) < 2 for i in range(N)) - assert all(g3.count(i) < 2 for i in range(N)) - - # gluing edges - for s1, s2 in G._edge_cycles: - t1 = G._faces[s1] # endpoint of first edge - t2 = G._faces[s2] # endpoint of second edge - i1 = dart_to_pillow[s1] - u1 = dart_to_corner[s1] - j1 = dart_to_pillow[t1] - v1 = dart_to_corner[t1] - i2 = dart_to_pillow[s2] - u2 = dart_to_corner[s2] - j2 = dart_to_pillow[t2] - v2 = dart_to_corner[t2] - if verbose: - print('gluing edge {} ({},{}) - {} ({},{}) to {} ({},{}) - {} ({},{})'.format( - s1,i1,u1, - t1,j1,v1, - s2,i2,u2, - t2,j2,v2)) - - # safety check - assert u1 == v2 and u2 == v1 - - # gluing the central part - j2, v2 = move_backward(j2, v2, g01, g23) - if verbose: - print('MOVE BACKWARD ({},{})'.format(j2, v2)) - i1, u1 = move_forward(i1, u1, g01, g23) - if verbose: - print('MOVE FORWARD ({},{})'.format(i1, u1)) - while (j2,v2) != (i2,u2): - if verbose: - print(' glue (i1,u1) = ({},{}) and (j2,v2) = ({},{})'.format(i1, u1, j2, v2)) - # safety check - assert u1 == v2 - g[v2][j2] = i1 - g[v2][i1] = j2 - j2, v2 = move_backward(j2, v2, g01, g23) - i1, u1 = move_forward(i1, u1, g01, g23) - - if verbose: - print('done gluing edges') - print(' (i1,u1) = ({},{})'.format(i1,u1)) - print(' (j1,v1) = ({},{})'.format(j1,v1)) - print(' (i2,u2) = ({},{})'.format(i2,u2)) - print(' (j2,v2) = ({},{})'.format(j2,v2)) - print(' g0: {}'.format(g0)) - print(' g1: {}'.format(g1)) - print(' g2: {}'.format(g2)) - print(' g3: {}'.format(g3)) - - # safety check - assert (i1,u1) == (j1,v1) - - from surface_dynamics.flat_surfaces.origamis.pillowcase_cover import PillowcaseCover - return PillowcaseCover(g0, g1, g2, g3, as_tuple=True) From 7aaff170d2738030e914933960559e7aeb467755 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 15:24:17 +0200 Subject: [PATCH 12/17] return embedding with connected components --- surface_dynamics/topology/fat_graph.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/surface_dynamics/topology/fat_graph.py b/surface_dynamics/topology/fat_graph.py index c5a895f8..728ea5ff 100644 --- a/surface_dynamics/topology/fat_graph.py +++ b/surface_dynamics/topology/fat_graph.py @@ -300,19 +300,20 @@ def is_connected(self): def connected_components(self): r""" - Return the list of connected components. + Return the list of connected components as pairs ``(fat_graph, embedding)``. EXAMPLES:: sage: from surface_dynamics.topology.fat_graph import FatGraph sage: FatGraph(vp='(0,2)(1,3)').connected_components() - [FatGraph('(0,2)(1,3)', '(0,3)(1,2)')] + [(FatGraph('(0,2)(1,3)', '(0,3)(1,2)'), (0, 1, 2, 3))] sage: FatGraph(vp='(0,1)(2,3)').connected_components() - [FatGraph('(0,1)', '(0)(1)'), FatGraph('(0,1)', '(0)(1)')] + [(FatGraph('(0,1)', '(0)(1)'), (0, 1)), + (FatGraph('(0,1)', '(0)(1)'), (2, 3))] """ ccs = perms_transitive_components([self._vp, self._fp], self._n) if len(ccs) == 1 and not self._mutable: - return [self] + return [(self, ccs[0])] # build a FatGraph for each connected component connected_graphs = [] @@ -323,7 +324,7 @@ def connected_components(self): for j in cc: vp[relabel[j]] = relabel[self._vp[j]] fp[relabel[j]] = relabel[self._fp[j]] - connected_graphs.append(FatGraph(vp, fp)) + connected_graphs.append((FatGraph(vp, fp), cc)) return connected_graphs @@ -341,9 +342,9 @@ def disjoint_union(self, *args, mutable=False): sage: fg FatGraph('(0,4,2,5)(1,6)(3,7)(8,11,13)(9,10,12)(14,15)', '(0,6,3,4,2,7,1,5)(8,12,11,9,13,10)(14)(15)') sage: fg.connected_components() - [FatGraph('(0,4,2,5)(1,6)(3,7)', '(0,6,3,4,2,7,1,5)'), - FatGraph('(0,3,5)(1,2,4)', '(0,4,3,1,5,2)'), - FatGraph('(0,1)', '(0)(1)')] + [(FatGraph('(0,4,2,5)(1,6)(3,7)', '(0,6,3,4,2,7,1,5)'), (0, 1, 2, 3, 4, 5, 6, 7)), + (FatGraph('(0,3,5)(1,2,4)', '(0,4,3,1,5,2)'), (8, 9, 10, 11, 12, 13)), + (FatGraph('(0,1)', '(0)(1)'), (14, 15))] """ if not args: if not self._mutable and not mutable: From 6baa445e700cf071bf85e208bbc6779cd06b1798 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 15:23:22 +0200 Subject: [PATCH 13/17] Rewrite QuadraticCylinderDiagram --- .../flat_surfaces/quadratic_strata.py | 6 +- .../flat_surfaces/separatrix_diagram.py | 242 ++++++++++++------ 2 files changed, 161 insertions(+), 87 deletions(-) diff --git a/surface_dynamics/flat_surfaces/quadratic_strata.py b/surface_dynamics/flat_surfaces/quadratic_strata.py index 5e4d3280..e10fe946 100644 --- a/surface_dynamics/flat_surfaces/quadratic_strata.py +++ b/surface_dynamics/flat_surfaces/quadratic_strata.py @@ -809,21 +809,21 @@ def one_cylinder_diagram(self): sage: Q = QuadraticStratum({1:1,-1:5}) sage: c = Q.unique_component().one_cylinder_diagram() sage: c - (0,0,1,1,2,2)-(3,3) + (0,0)-(1,1,3,3,2,2) sage: c.stratum() == Q True sage: Q = QuadraticStratum(5,-1) sage: c = Q.unique_component().one_cylinder_diagram() sage: c - (0,1,1,2)-(3,0,3,2) + (0,1,0,2)-(1,3,3,2) sage: c.stratum() == Q True sage: QuadraticStratum({-1:4}).unique_component().one_cylinder_diagram() (0,0)-(1,1) sage: QuadraticStratum({-1:4,0:1}).unique_component().one_cylinder_diagram() - (0,0)-(1,1,2,2) + (0,0,1,1)-(2,2) """ from surface_dynamics.flat_surfaces.separatrix_diagram import QuadraticCylinderDiagram p = self.permutation_representative(reduced=True).to_cylindric() diff --git a/surface_dynamics/flat_surfaces/separatrix_diagram.py b/surface_dynamics/flat_surfaces/separatrix_diagram.py index 7f5255b7..81027779 100644 --- a/surface_dynamics/flat_surfaces/separatrix_diagram.py +++ b/surface_dynamics/flat_surfaces/separatrix_diagram.py @@ -99,7 +99,7 @@ from surface_dynamics.misc.permutation import (perm_check, equalize_perms, perm_init, perm_cycles, perm_cycle_type, perm_compose_i, - perm_invert, perms_canonical_labels, + perm_invert, perms_canonical_labels, perm_orbit, perms_transitive_components, canonical_perm, canonical_perm_i, argmin) from surface_dynamics.misc.linalg import cone_triangulate @@ -1551,7 +1551,7 @@ def saddle_connections_graph(self, mutable=False): sage: H11 = AbelianStratum(1,1).unique_component() sage: for cd in H11.cylinder_diagrams(): ....: fg = cd.saddle_connections_graph() - ....: print(cd.ncyls(), [comp.genus() for comp in fg.connected_components()]) + ....: print(cd.ncyls(), [comp.genus() for comp, _ in fg.connected_components()]) 1 [1] 2 [0] 2 [0] @@ -4253,6 +4253,7 @@ def move_backward(i, v, g01, g23): return i,v + def simplex_count(rays): r""" EXAMPLES:: @@ -4275,97 +4276,96 @@ def simplex_count(rays): d = len(rays[0]) return Polyhedron([[0]*d] + list(rays)).integral_points_count() - len(rays) + class QuadraticCylinderDiagram(SageObject): r""" - Cylinder diagram for quadratic differentials + Cylinder diagram for quadratic differentials. - Cylinder diagrams are encoded as a Ribbon graph together with a pairing of - faces (in particular the number of faces must be even). + A cylinder diagram is encoded as a fat graph together with a perfect + matching of its faces. The faces of the fat graph are the cylinder + boundaries and two faces that are matched correspond to the two sides of a + cylinder. EXAMPLES:: sage: from surface_dynamics import * - If you start with strings, the cylinders are preserved but the names of - saddle connections are changed:: + Note that cylinders could be reordered:: sage: QuadraticCylinderDiagram('(4,4,5)-(6,6,1) (2,3,2,0)-(1,0,5,3)') - (0,0,1)-(2,2,3) (4,5,4,6)-(3,6,1,5) + (0,2,3,2)-(0,5,3,1) (1,6,6)-(4,4,5) """ def __init__(self, arg1, arg2=None): r""" INPUT: there are two input formats - - with a unique argument - - - with two arguments + - with a unique string argument - - ``g`` -- a ribbon graph - - - ``pairing`` -- the pairing of faces of g (= permutation) + - with two arguments made of a fat graph and a pairing """ from .homology import RibbonGraph + from surface_dynamics.topology.fat_graph import FatGraph if arg2 is None: if isinstance(arg1, str): - data = [(string_to_cycle(b),string_to_cycle(t)) for b,t in (w.split('-') for w in arg1.split(' '))] + data = [(string_to_cycle(b), string_to_cycle(t)) for b, t in (w.split('-') for w in arg1.split(' '))] else: data = arg1 N = 0 - for i,pair in enumerate(data): + for i, pair in enumerate(data): if not isinstance(pair, (tuple, list)) or len(pair) != 2: raise TypeError('input must be a list of pairs') data[i] = [[int(x) for x in pair[0]], [int(x) for x in pair[1]]] N += len(pair[0]) + len(pair[1]) - if N%2: + if N % 2: raise ValueError('each symbol must appear exactly twice') - n = N//2 - for b,t in data: + n = N // 2 + for b, t in data: if any(i < 0 or i >= n for i in b) or \ any(i < 0 or i >= n for i in t): raise ValueError('symbol out of range') + # shift half-edges k = 0 - faces = [] - edges = [None] * N - seen = [None] * n - p = [] - for i, bt in enumerate(data): - # constructing p (= face matching) - p.append(2*i+1) - p.append(2*i) - - # constructing edges and faces - for f in bt: - faces.append(tuple(range(k, k+len(f)))) - - for j in range(len(f)): - e = f[j] - if seen[e] is not None: - if seen[e] == -1: - raise ValueError('number %d appears more than twice' % e) - kk = seen[e] - edges[kk] = k + j - edges[k + j] = kk - seen[e] = -1 + seen = [0] * n + for bt in data: + for cyc in bt: + for i, e in enumerate(cyc): + if seen[e] > 1: + raise RuntimeError + elif not seen[e]: + cyc[i] = 2 * e + seen[e] = 1 + k += 1 else: - seen[e] = k + j - - k += len(f) - - g = RibbonGraph(edges=edges, faces=faces, connected=False) + cyc[i] = 2 * e + 1 + seen[e] = 2 + + # constructing p (= face matching) + fp = perm_init([bot for bot, _ in data] + [top for _, top in data]) + + g = FatGraph(fp=fp) + assert g.num_faces() == 2 * len(data) + p = [None] * g.num_faces() + for bt in data: + bot_face = g._fl[bt[0][0]] + top_face = g._fl[bt[1][0]] + p[bot_face] = top_face + p[top_face] = bot_face + assert all(x is not None for x in p), p else: g = arg1 p = arg2 - if not isinstance(g, RibbonGraph): + if isinstance(g, RibbonGraph): + g = g._fat_graph + if not isinstance(g, FatGraph): raise ValueError - p = perm_init(p) - self._g = g - self._p = p + self._g = g # fat graph + self._p = p # perfect matching on faces of g self._check() @@ -4383,32 +4383,98 @@ def _check(self): f = self._g.num_faces() if f % 2: raise ValueError('the number of faces of the fatgraph must be even') - if len(self._p) != f: - raise ValueError('the pairing has wrong length') - for i,j in enumerate(self._p): + if not perm_check(self._p, f): + raise ValueError('invalid pairing') + for i, j in enumerate(self._p): if i == j or self._p[j] != i: raise ValueError('the pairing is not an involution') - from sage.graphs.graph import Graph - G = Graph(loops=True, multiedges=True) - for i in range(self._g._total_darts): - if self._g._active_darts[i]: - G.add_edge(i,self._g._vertices[i]) - G.add_edge(i,self._g._edges[i]) - G.add_edge(i,self._g._faces[i]) - for i,j in enumerate(self._p): - k1 = self._g._face_cycles[i][0] - k2 = self._g._face_cycles[j][0] - G.add_edge(k1, k2) - - if not G.is_connected(): + if not self.stable_graph()[1].is_connected(): raise ValueError("the graph is not connected") + def is_abelian(self): + r""" + Return whether this diagram corresponds to Abelian differentials. + + EXAMPLES:: + + sage: from surface_dynamics import QuadraticCylinderDiagram + sage: QuadraticCylinderDiagram('(0,1,4)-(2,3,4) (2,3)-(0,1)').is_abelian() + True + sage: QuadraticCylinderDiagram('(0,1)-(2,3,4) (2,3)-(0,1,4)').is_abelian() + False + """ + faces = self._g.faces() + faces.sort(key = lambda f: self._g._fl[f[0]]) + face_colors = [-1] * self._g._nf + face_colors[0] = 0 + todo = [0] + while todo: + i = todo.pop() + coli = face_colors[i] + assert coli != -1 + adjacent_faces = [self._g._fl[e ^ 1] for e in faces[i]] + [self._p[i]] + for j in adjacent_faces: + colj = face_colors[j] + if colj == -1: + face_colors[j] = coli ^ 1 + todo.append(j) + elif colj != coli ^ 1: + return False + assert all(x != -1 for x in face_colors) + return True + + def stable_graph(self): + r""" + Return the stable graph associated to this cylinder diagram. + + This is this the graph whose vertices are the connected components of + the underlying fat graph and there is an edge for each cylinder. + + EXAMPLES:: + + sage: from surface_dynamics import QuadraticCylinderDiagram + sage: qcd = QuadraticCylinderDiagram('(0,0)-(3) (1,1)-(4) (2,2)-(3,4)') + sage: qcd.stable_graph() + ([0, 0, 0, 0], Looped graph on 4 vertices) + """ + from collections import defaultdict + from sage.graphs.graph import Graph + ccs = self._g.connected_components() + face_label_to_cc = [-1] * self._g._nf + for i, (h, embedding) in enumerate(ccs): + for f in h.faces(): + face_label = self._g._fl[embedding[f[0]]] + face_label_to_cc[face_label] = i + assert not any(x == -1 for x in face_label_to_cc) + + genera = [h.genus() for h, _ in ccs] + edges = {} # (u, v) -> multiplicity + for i in range(len(self._p)): + j = self._p[i] + if j < i: + continue + cci = face_label_to_cc[i] + ccj = face_label_to_cc[j] + if ccj < cci: + edge = (ccj, cci) + else: + edge = (cci, ccj) + if edge in edges: + edges[edge] += 1 + else: + edges[edge] = 1 + sg = Graph(len(ccs), loops=True, multiedges=False) + for (u, v), mult in edges.items(): + sg.add_edge(u, v, mult) + + return (genera, sg) + def num_darts(self): r""" Number of darts """ - return self._g.num_darts() + return 2 * self._g.num_edges() def num_edges(self): r""" @@ -4422,13 +4488,13 @@ def edges(self): r""" The set of edges. """ - return self._g.edges() + return [[2 * e, 2 * e + 1] for e in range(self._g.num_edges())] def num_cylinders(self): r""" Number of cylinders. """ - return len(self._p) // 2 + return self._g.num_faces() // 2 ncyls = num_cylinders @@ -4442,20 +4508,26 @@ def stratum(self): sage: QuadraticCylinderDiagram('(0,0)-(1,1)').stratum() Q_0(-1^4) - + sage: QuadraticCylinderDiagram('(0)-(0)').stratum() + H_1(0) sage: QuadraticCylinderDiagram('(0,0)-(1,1,2,2,3,3)').stratum() Q_0(1, -1^5) - + sage: QuadraticCylinderDiagram('(0,2)-(1,2) (0)-(1)').stratum() + H_2(2) sage: QuadraticCylinderDiagram('(0,2,3,2)-(1,0,1,3)').stratum() Q_2(2^2) - sage: QuadraticCylinderDiagram('(0,1)-(2,3) (0)-(4,4) (1)-(5,5) (2)-(6,6) (3)-(7,7)').stratum() Q_0(2^2, -1^8) """ - from surface_dynamics.flat_surfaces.quadratic_strata import QuadraticStratum - return QuadraticStratum([len(t)-2 for t in self._g._vertex_cycles]) + from surface_dynamics.misc.permutation import perm_cycle_type + if self.is_abelian(): + from surface_dynamics.flat_surfaces.abelian_strata import AbelianStratum + return AbelianStratum([(l - 2) // 2 for l in perm_cycle_type(self._g._vp)]) + else: + from surface_dynamics.flat_surfaces.quadratic_strata import QuadraticStratum + return QuadraticStratum([l - 2 for l in perm_cycle_type(self._g._vp)]) - def cylinders(self, dart=False): + def cylinders(self, half_edges=False): r""" Cylinders of self @@ -4464,10 +4536,10 @@ def cylinders(self, dart=False): EXAMPLES:: - sage: from surface_dynamics import * + sage: from surface_dynamics import FatGraph sage: from surface_dynamics.flat_surfaces.separatrix_diagram import QuadraticCylinderDiagram - sage: rg = RibbonGraph(edges='(0,1)(2,3)(4,5)(6,7)', faces='(0,1)(2,4,5)(3)(6,7)', connected=False) - sage: q = QuadraticCylinderDiagram(rg, '(0,1)(2,3)') + sage: fg = FatGraph(fp='(0,1)(2,4,5)(3)(6,7)') + sage: q = QuadraticCylinderDiagram(fg, '(0,1)(2,3)') sage: q.cylinders() [((0, 0), (1, 2, 2)), ((1,), (3, 3))] sage: q.cylinders(True) @@ -4475,17 +4547,19 @@ def cylinders(self, dart=False): """ ans = [] g = self._g - for i,j in enumerate(self._p): + faces = self._g.faces() + faces.sort(key = lambda f: self._g._fl[f[0]]) + for i, j in enumerate(self._p): if i > j: continue - bot = g._face_cycles[i] - top = g._face_cycles[j] - if dart: + bot = faces[i] + top = faces[j] + if half_edges: bot = tuple(bot) top = tuple(top) else: - bot = tuple(g._dart_to_edge_index[x] for x in bot) - top = tuple(g._dart_to_edge_index[x] for x in top) + bot = tuple(x // 2 for x in bot) + top = tuple(x // 2 for x in top) ans.append((bot,top)) return ans From 0b14dd0a6f91449e7f0e3528e2da15ac4b1ac78e Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 15:59:02 +0200 Subject: [PATCH 14/17] remove an example using removed construction --- .../flat_surfaces/origamis/pillowcase_cover.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/surface_dynamics/flat_surfaces/origamis/pillowcase_cover.py b/surface_dynamics/flat_surfaces/origamis/pillowcase_cover.py index f74a8679..30a11c5f 100644 --- a/surface_dynamics/flat_surfaces/origamis/pillowcase_cover.py +++ b/surface_dynamics/flat_surfaces/origamis/pillowcase_cover.py @@ -257,13 +257,6 @@ def orientation_cover(self): (1,10,5,12)(2,9)(3,8)(4,7,6,11) sage: o.stratum() H_2(2) - - A last example in Q(2^2):: - - sage: q = QuadraticCylinderDiagram('(0,1)-(2,3) (0,3)-(1,2)') - sage: pc = q.cylcoord_to_pillowcase_cover([1,1,1,1], [2,2], [0,1]) - sage: pc.orientation_cover().stratum() - H_3(1^4) """ from surface_dynamics.misc.permutation import perm_invert from surface_dynamics.flat_surfaces.origamis.origami import Origami From 38d062c012fb782f9b814a4650a43765bf0eafc0 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 16:00:26 +0200 Subject: [PATCH 15/17] removal in the news --- doc/news/homology.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/news/homology.rst b/doc/news/homology.rst index 40f71790..a393de3f 100644 --- a/doc/news/homology.rst +++ b/doc/news/homology.rst @@ -5,3 +5,8 @@ **Deprecated:** * RibbonGraph are deprecated in favor of FatGraph + +**Removed:** + +* it is not possible anymore to build a pillowcase cover from + a QuadraticCylinderDiagram and (length, height, twist) coordinates From 9cc8c299b720da6a6fcffa98afce2eefde9cbb31 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 16:13:30 +0200 Subject: [PATCH 16/17] fix CarrellChapuy callback --- tests/test_fat_graphs_enumeration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fat_graphs_enumeration.py b/tests/test_fat_graphs_enumeration.py index b7583655..a448e832 100644 --- a/tests/test_fat_graphs_enumeration.py +++ b/tests/test_fat_graphs_enumeration.py @@ -25,6 +25,7 @@ def __init__(self, ne): def __call__(self, cm, aut): # NOTE: in order to count rooted maps, we multiply by 2 ne / |Aut| self.counter += 2 * self.ne // (1 if aut is None else aut.group_cardinality()) * self.x ** cm.num_faces() + return True @cached_function def carrell_chapuy_polynomial(g, n): From b75fdec35d7c6f348e8505a16f41334fa837e100 Mon Sep 17 00:00:00 2001 From: Vincent Delecroix Date: Tue, 24 Oct 2023 16:23:44 +0200 Subject: [PATCH 17/17] cite Joh80 in spin_parity --- surface_dynamics/topology/fat_graph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/surface_dynamics/topology/fat_graph.py b/surface_dynamics/topology/fat_graph.py index 728ea5ff..124fad2b 100644 --- a/surface_dynamics/topology/fat_graph.py +++ b/surface_dynamics/topology/fat_graph.py @@ -1189,13 +1189,15 @@ def angles(self, verticals): def spin_parity(self, verticals, check=True): r""" - Return the spin parity of the given ``verticals`` + Return the spin parity of the winding given by ``verticals``. The argument ``verticals`` must be a list of half-edges that allows to make sense of the winding number on the graph. More precisely, if the underlying graph is made of saddle connections on a translation or half-translation surface, the verticals are the half-edges whose associated corner contains a vertical germ. + The spin structure for surfaces is well explained in [Joh80]_. + EXAMPLES:: sage: from surface_dynamics import AbelianStratum