Source code for comch.cubical.cubical

from ..free_module import FreeModuleElement
from ..symmetric import SymmetricRingElement
from ..simplicial import Simplex, SimplicialElement
from ..utils import pairwise
from itertools import combinations, product


[docs]class Cube(tuple): r"""A cube :math:`(I_1, \dots, I_n)`. A cube is a finite tuple of elements in :math:`\{0,1,2\}`, where we think of :math:`2` as the interval :math:`[0,1]` and :math:`0,1` as its endpoints. We identify these with faces of the infinite cube :math:`\mathbb I^\infty`. """
[docs] def __init__(self, iterable): """Initializes *self*. PARAMETERS ---------- interable : :class:'iterable' Used to create a :class:`tuple` of :class:`int` with values 0, 1 or 2. EXAMPLE ------- >>> print(Cube((1,2,0,2))) (1,2,0,2) """ tuple.__init__(iterable)
def __str__(self): return super.__str__(self).replace(', ', ',') @property def dimension(self): """The dimension of *self*. Defined as the number of values in the tuple that are equal to 2. RETURNS ------- :class:`int` The dimension of *self*. EXAMPLE ------- >>> Cube((1,2,0,2)).dimension 2 """ return self.count(2) @property def intervals(self): """The positions of intervals in *self*. Corresponds to the tuple of indices where *self* contains 2. RETURNS ------- :class:`tuple` The indices of intervals in *self*. EXAMPLE ------- >>> Cube((1,2,0,2)).intervals (1, 3) """ return tuple(idx for idx, x in enumerate(self) if x == 2)
[docs] def face(self, i, epsilon): r"""The i-th :math:`\epsilon` face of *self*. Obtained by replacing the i-th entry of *self* by :math:`\epsilon`. RETURNS ------- :class:`comch.cubical.Cube` The i-th face of *self*. EXAMPLE ------- >>> Cube((1,2,0,2)).face(1, 0) (1, 2, 0, 1) """ idx = self.intervals[i] answer = self[:idx] + ((epsilon + 1) % 2,) + self[idx + 1:] return Cube(answer)
[docs] def cartan_serre_map(self): r"""The image *self* under the Cartan-Serre collapse map. Obtained by sending the cell :math:`x_1 \times \dots \times x_n` to :math:`[q_1-1, \dots, q_m-1, p-1]` where :math:`q_1 < q_2 < \dots` are the positions of the intervals and :math:`q_m < p` with :math:`p = \min\{i \mid x_i = [0]\}` or :math:`p = n+1` if empty. RETURNS ------- :class:`comch.simplicial.Simplex` The image of *self* under the Cartan-Serre collapse map. EXAMPLE ------- >>> Cube((2,1,2)).cartan_serre_map() (0, 2, 3) """ end = len(self) if 0 in self: end = self.index(0) vertices = tuple(i for i in self.intervals if i < end) + (end,) spx = Simplex(vertices) return spx
[docs]class CubicalElement(FreeModuleElement): r"""Elements in an iterated tensor product of the chains on the infinite cube. The chains on the infinite cube :math:`C = C_\bullet(\Delta^\infty; R)` is the differential graded module :math:`C` with degree-:math:`n` part :math:`C_n` freely generated as an :math:`R`-module by cubes of dimension :math:`n`, and differential on these given by the sum of its faces with alternating signs. Explicitly, for $x \in C_n$ we have .. math:: \partial x = \sum_{i = 1}^{n} (-1)^{i-1}(d^0_i x - d^1_i x) The differential graded module :math:`C` is isomorphic to .. math:: \bigoplus_{k \geq 0} C_\bullet^{CW}(I; R)^{\otimes k} where :math:`I` is the interval with its usual cellular structure. The degree-:math:`n` part of the tensor product :math:`C^{\otimes r}` and its differential are recursively defined by .. math:: (C^{\otimes r})_n = \bigoplus_{i+j=n} C_i \otimes (C^{\otimes r})_n and .. math:: \partial(c_1 \otimes c_2 \otimes \cdots \otimes c_r) = (\partial c_1) \otimes c_2 \otimes \cdots \otimes c_r + (-1)^{|c_1|} c_1 \otimes \partial (c_2 \otimes \cdots \otimes c_r). ATTRIBUTES ---------- torsion : :class:`int` The non-neg int :math:`n` of the ring :math:`\mathbb Z/n \mathbb Z`. """
[docs] def __init__(self, data=None, torsion=None): """Initializes *self*. PARAMETERS ---------- data : :class:`int` or ``None``, default: ``None`` Dictionary representing a linear cobination of basis elements. Items in the dictionary correspond to `basis element: coefficient` pairs. Each basis element must create a :class:`tuple` of :class:`comch.cubical.Cube` and `coefficient` must be an :class:`int`. torsion : :class:`int` The non-neg int :math:`n` of the ring :math:`\mathbb Z/n \mathbb Z`. EXAMPLE ------- >>> x = CubicalElement({((0,2), (0,1)): 1,\ ((2,1), (1,2)): -1,\ ((1,2), (2,0)): 1}) >>> print(x) ((0,2),(0,1)) - ((2,1),(1,2)) + ((1,2),(2,0)) """ if data: new_data = {} for k, v in data.items(): new_k = tuple(Cube(cube) for cube in k) new_data[new_k] = v data = new_data super(CubicalElement, self).__init__( data=data, torsion=torsion)
def __str__(self): string = super().__str__() return string.replace(', ', ',') def _latex_(self): r"""Representation in LaTex. RETURNS ------- :class:`string` A LaTex friendly representation. EXAMPLE ------- >>> x = CubicalElement({((2,), (1,)): 1, ((0,), (2,)): 1}) >>> print(x._latex_()) [01] \otimes [1] + [0] \otimes [01] """ string = str(self) string = string.replace('1', '[1]').replace('0', '[0]') string = string.replace('2,', '[01],').replace(',2', ',[01]') string = string.replace(',),(', r' \otimes ') string = string.replace('),(', r' \otimes ') string = string.replace(')', '').replace('((', '') string = string.replace(')', '').replace('))', '') string = string.replace(',', '') return string @property def arity(self): """Arity of *self*. Defined as ``None`` if *self* is not homogeneous. The arity of a basis element is defined as the number of tensor factors making it. RETURNS ------- :class:`int` positive or ``None``. The length of the keys of *self* or ``None`` if not well defined. EXAMPLE ------- >>> x = CubicalElement({((0,2), (0,1)): 1}) >>> x.arity 2 """ arities = set(len(multicube) for multicube in self.keys()) if len(arities) != 1: return None return arities.pop() @property def degree(self): """Degree of *self*. Defined as ``None`` if self is not homogeneous. The degree of a basis element agrees with the sum of the dimension of the simplices making it. RETURNS ------- :class:`int` positive or ``None``. The sum of the dimensions of the simplices of every key of *self* or ``None`` if not well defined. EXAMPLE ------- >>> x = CubicalElement({((0,2), (0,1)): 1}) >>> x.degree 1 """ degrees = {sum(cube.dimension for cube in k) for k in self.keys()} if len(degrees) != 1: return None return degrees.pop()
[docs] def boundary(self): """Boundary of *self*. As defined in the class's docstring. RETURNS _______ :class:`comch.cubical.CubicalElement` The boundary of *self* as an element in a tensor product of differential graded modules. EXAMPLE ------- >>> x = CubicalElement({((0, 2), (2, 1)): 1}) >>> print(x.boundary()) ((0,1),(2,1)) - ((0,0),(2,1)) - ((0,2),(1,1)) + ((0,2),(0,1)) """ answer = self.zero() for k, v in self.items(): for idx, cube in enumerate(k): acc_dim = sum((cube_l.dimension for cube_l in k[:idx])) for i in range(cube.dimension): for epsilon in (0, 1): new_cube = cube.face(i, epsilon) new_k = k[:idx] + (new_cube,) + k[idx + 1:] sign_exp = (acc_dim + i + epsilon) % 2 answer += answer.create({new_k: v * (-1) ** sign_exp}) return answer
[docs] def __rmul__(self, other): """Left action: *other* ``*`` *self* Left multiplication by a symmetric group element or an integer. Defined up to signs on basis elements by permuting the tensor factor. PARAMETERS ---------- other : :class:`int` or :class:`comch.symmetric.SymmetricElement`. The symmetric ring element left acting on *self*. RETURNS _______ :class:`comch.cubical.CubicalElement` The product: *other* ``*`` *self* with Koszul's sign convention. EXAMPLE ------- >>> x = CubicalElement({((0, 2), (1, 2)): 1}) >>> t = SymmetricRingElement({(2, 1): 1}) >>> print(t * x) - ((1,2),(0,2)) >>> print(3 * x) 3((0,2),(1,2)) """ def check_input(self, other): """Symmetric ring element with same attributes.""" if not isinstance(other, SymmetricRingElement): raise TypeError(f'__rmul__ by type int or \ SymmetricRingElement not {type(other)}') if self.torsion != other.torsion: raise TypeError('only defined for equal attribute torsion') if self.arity != other.arity: raise TypeError('Unequal arity attribute') def sign(perm, multicube): weights = [cube.dimension % 2 for cube in multicube] sign_exp = 0 for idx, i in enumerate(perm): right = [weights[perm.index(j)] for j in perm[idx + 1:] if i > j] sign_exp += sum(right) * weights[idx] return (-1) ** (sign_exp % 2) if isinstance(other, int): return super().__rmul__(other) check_input(self, other) answer = self.zero() for (k1, v1), (k2, v2) in product(self.items(), other.items()): new_key = [None] * len(k2) for idx, i in enumerate(k2): new_key[i - 1] = k1[idx] new_sign = sign(k2, k1) answer += self.create({tuple(new_key): new_sign * v1 * v2}) return answer
[docs] def iterated_diagonal(self, times=1, coord=1): r"""Iterated Serre diagonal applied at a specific tensor factor. The Serre diagonal is the chain map :math:`\Delta \colon C \to C \otimes C` defined on the chains of the infinite cube by the formula .. math:: \Delta (x_1 \otimes \cdots \otimes x_n) = \sum \pm \left( x_1^{(1)} \otimes \cdots \otimes x_n^{(1)} \right) \otimes \left( x_1^{(2)} \otimes \cdots \otimes x_n^{(2)} \right), where the sign is determined using the Koszul convention, and we are using Sweedler’s notation .. math:: \Delta(x_i) = \sum x_i^{(1)} \otimes x_i^{(2)}. It is coassociative, :math:`(\Delta \otimes \mathrm{id}) \Delta = (\mathrm{id} \otimes \Delta) \Delta`, so it has a well defined iteration :math:`\Delta^k`, and for every :math:`i \in \{1, \dots, r\}`, there is map :math:`C^{\otimes r} \to C^{\otimes k+r}` sending :math:`(x_1 \otimes \cdots \otimes x_n)` to :math:`(x_1 \otimes \cdots \otimes \Delta^k(k_i) \cdots \otimes x_n)`. PARAMETERS ---------- times : :class:`int` The number of times the Serre diagonal is composed with itself. coord : :class:`int` The tensor position on which the iterated diagonal acts. RETURNS _______ :class:`comch.cubical.CubicalElement` The action of the iterated Serre diagonal on *self*. EXAMPLE ------- >>> x = CubicalElement({((2,2), (2,)):1}) >>> print(x.iterated_diagonal(1,2)) ((2,2),(2,),(1,)) + ((2,2),(0,),(2,)) """ def sign(p): """Counts the number of pairs appearing in reversed order.""" to_count = filter(lambda x: x[0] > x[1], combinations(p, 2)) sign_exp = sum(1 for _ in to_count) % 2 return (-1) ** sign_exp def elementary_summand(fixed, i): """Models as a function the element 0,...,0,2,1,...,1 appearing as one of the summands of the iterated diagonal of an interval.""" if i < fixed: return 0 elif i == fixed: return 2 else: return 1 if self.degree is None: raise TypeError(f'only for homogeneous elements') if self.arity < coord: raise TypeError(f'arity = {self.arity} < coord = {coord}') answer = self.zero() for k, v in self.items(): left, cube, right = k[:coord - 1], k[coord - 1], k[coord:] intervals = cube.intervals base = [i for idx, i in enumerate(cube) if idx not in intervals] for p in product(range(times + 1), repeat=cube.count(2)): multibase = [list(base) for _ in range(times + 1)] for idx, fixed in enumerate(p): at = intervals[idx] for i, new_base in enumerate(multibase): to_insert = elementary_summand(fixed, i) new_base.insert(at, to_insert) new_k = tuple(Cube(x) for x in multibase) answer += answer.create({left + new_k + right: v * sign(p)}) return answer
[docs] def join(self): r"""Join of *self*. The join is a map from :math:`C_\bullet^{\otimes 2}` to :math:`C_\bullet` of degree 1. It is define by .. math:: \begin{aligned} (x_1 \otimes \cdots \otimes x_n) \ast (y_1 \otimes \cdots \otimes y_n) = (-1)^{|x|} \sum_{i=1}^n x_{<i} \epsilon(y_{<i}) \otimes x_i \ast y_i \otimes \epsilon(x_{>i})y_{>i}, \end{aligned} where .. math:: \begin{aligned} x_{<i} & = x_1 \otimes \cdots \otimes x_{i-1}, & y_{<i} & = y_1 \otimes \cdots \otimes y_{i-1}, \\ x_{>i} & = x_{i+1} \otimes \cdots \otimes x_n, & y_{>i} & = y_{i+1} \otimes \cdots \otimes y_n, \end{aligned} with the convention .. math:: x_{<1} = y_{<1} = x_{>n} = y_{>n} = 1 \in \mathbb Z, and the only non-zero values of :math:`x_i \ast y_i` are .. math:: \ast([0] \otimes [1]) = [0, 1], \qquad \ast([1] \otimes [0]) = -[0, 1]. PARAMETERS ---------- :class:`comch.cubical.CubicalElement`, of arity 2 The element to take the join of. RETURNS _______ :class:`comch.cubical.CubicalElement` The join of *self*. Examples -------- >>> x = CubicalElement({((0, 0, 1), (1, 0, 0)): 1}) >>> print(x.join()) ((2,0,0),) - ((0,0,2),) """ def is_zero(left, right): """Two conditions need to be satisfied for a triple (cube1, cube2, i) give a nonzero i-join: no intervals in cube1 can have indices greater or equal to i, and no intervals in cube2 can have indices less than or equal to i.""" if left == tuple() or right == tuple(): return False if isinstance(right, int): return right <= max(left) if isinstance(left, int): return left >= min(right) def _join(i, cube1, cube2, sign_exp): """the i-th elementary join keeping track of signs.""" sign_exp += cube1.dimension cube = Cube(cube1[:i] + (2,) + cube2[i + 1:]) p, q = cube1[i], cube2[i] if (p, q) == (0, 1): return cube, sign_exp % 2 elif (p, q) == (1, 0): return cube, (1 + sign_exp) % 2 else: return None, None if not self: return self if self.degree is None: raise TypeError(f'only for homogeneous elements') answer = self.zero() for k, v in self.items(): for indices in combinations(range(len(k[0])), self.arity - 1): skip = False for i, (cube1, cube2) in zip(indices, pairwise(k)): if (is_zero(cube1.intervals, i) or is_zero(i, cube2.intervals)): skip = True break if not skip: non_zero = True sign_exp = 0 cube = k[0] for i, next_cube in zip(indices, k[1:]): cube, sign_exp = _join(i, cube, next_cube, sign_exp) if cube is None: non_zero = False break if non_zero: answer += answer.create({(cube,): (-1) ** sign_exp * v}) return answer
[docs] def cartan_serre_map(self): r"""The image *self* under the Cartan-Serre collapse map. Image of *self* via the chain map induced by the Cartan-Serre map defined for cubes. RETURNS ------- :class:`comch.simplicial.SimplicialElement` The image of *self* under the Cartan-Serre chain map. EXAMPLE ------- >>> CubicalElement({((2,1,2), (2,2,0)): 1}).cartan_serre_map() SimplicialElement({((0, 2, 3), (0, 1, 2)): 1}) """ answer = SimplicialElement(torsion=self.torsion) if self == self.zero(): return answer for k, v in self.items(): old_dim = sum(cube.dimension for cube in k) new_k = tuple(cube.cartan_serre_map() for cube in k) new_dim = sum(spx.dimension for spx in new_k) if old_dim == new_dim: answer += answer.create({new_k: v}) return answer
[docs]class Cubical: """Produces cubical elements of interest."""
[docs] @staticmethod def standard_element(n, times=1, torsion=None): r"""The chain represented by the cube :math:`[0,1]^{n}`. PARAMETERS ---------- n : :class:`int` The dimension of the standard cube considered. times : :class:`int` The number of tensor copies. torsion : :class:`int` The non-neg int :math:`n` of the ring :math:`\mathbb Z/n \mathbb Z`. EXAMPLES -------- >>> print(Cubical.standard_element(3, 2)) ((2,2,2),(2,2,2)) """ key = ((2,) * n,) * times return CubicalElement({key: 1}, torsion=torsion)
[docs] @staticmethod def basis(n, torsion=None): r"""Iterator of all basis elements in the chain complex of an n-cube. PARAMETERS ---------- n : :class:`int` The dimension of the standard cube considered. torsion : :class:`int` The non-neg int :math:`n` of the ring :math:`\mathbb Z/n \mathbb Z`. EXAMPLES -------- >>> print([str(b) for b in Cubical.basis(1)]) ['((0,),)', '((1,),)', '((2,),)'] """ for b in product({0, 1, 2}, repeat=n): yield CubicalElement({(b,): 1}, torsion=torsion)