Created
June 6, 2024 09:18
-
-
Save sweeneyde/ad7a21ebefadf8ce7063f3ffdc62447f to your computer and use it in GitHub Desktop.
Compute Künneth theorem, Universal Coefficients Theorem, or other calculations involving of finitely generated abelian groups.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Künneth Theorem Calculator | |
| and | |
| Universal Coefficient Theorem Calculator | |
| Written by Dennis Sweeney | |
| Calculate elementary divisors, invariant factors, | |
| direct sums, tensor products, tor, ext, and hom modules | |
| of finitely generate abelian groups. | |
| Use this to implement the Künneth Theorem and | |
| Universal Coefficient Theorem. | |
| Example: Compute the homology of RP^6 x S^2 | |
| >>> rp6 = [Z, Zmod(2), trivial, Zmod(2), trivial, Zmod(2), trivial] | |
| >>> s2 = [Z, trivial, Z] | |
| >>> for dim, G in enumerate(product_homology(rp6, s2)): | |
| ... print(f"H_{dim} = {G}") | |
| H_0 = Z | |
| H_1 = C2 | |
| H_2 = Z | |
| H_3 = C2 x C2 | |
| H_4 = trivial | |
| H_5 = C2 x C2 | |
| H_6 = trivial | |
| H_7 = C2 | |
| H_8 = trivial | |
| Example: Compute the cohomology of RP^6 with coefficients | |
| in Z or in Z/6Z: | |
| >>> for dim, G in enumerate(cohomology_from_homology(rp6, Z)): | |
| ... print(f"H^{dim}(rp6; Z) = {G}") | |
| H^0(rp6; Z) = Z | |
| H^1(rp6; Z) = trivial | |
| H^2(rp6; Z) = C2 | |
| H^3(rp6; Z) = trivial | |
| H^4(rp6; Z) = C2 | |
| H^5(rp6; Z) = trivial | |
| H^6(rp6; Z) = C2 | |
| >>> for dim, G in enumerate(cohomology_from_homology(rp6, Zmod(6))): | |
| ... print(f"H^{dim}(rp6; C6) = {G}") | |
| H^0(rp6; C6) = C2 x C3 | |
| H^1(rp6; C6) = C2 | |
| H^2(rp6; C6) = C2 | |
| H^3(rp6; C6) = C2 | |
| H^4(rp6; C6) = C2 | |
| H^5(rp6; C6) = C2 | |
| H^6(rp6; C6) = C2 | |
| Example: Compute the homology with coefficients in Z/Z6: | |
| >>> for dim, G in enumerate(homology_with_coefficients(rp6, Zmod(6))): | |
| ... print(f"H_{dim}(rp6; C6) = {G}") | |
| H_0(rp6; C6) = C2 x C3 | |
| H_1(rp6; C6) = C2 | |
| H_2(rp6; C6) = C2 | |
| H_3(rp6; C6) = C2 | |
| H_4(rp6; C6) = C2 | |
| H_5(rp6; C6) = C2 | |
| H_6(rp6; C6) = C2 | |
| """ | |
| from itertools import zip_longest | |
| from collections import defaultdict | |
| from math import prod | |
| try: | |
| from sympy import factorint | |
| except ImportError: | |
| import warnings | |
| warnings.warn("Sympy not found") | |
| def factorint(n): | |
| result = {} | |
| p = 2 | |
| while p * p <= n: | |
| if n % p == 0: | |
| n //= p | |
| exp = 1 | |
| while n % p == 0: | |
| n //= p | |
| exp += 1 | |
| result[p] = exp | |
| p += 1 | |
| if n > 1: | |
| result[n] = 1 | |
| return result | |
| class FinitelyGeneratedAbelianGroup: | |
| """A Finitely generated abelian groups G | |
| is always a direct sums of cyclic groups: | |
| G = Z/m_1 (+) Z/m_2 (+) ... (+) Z/m_k. | |
| To construct such a group, pass in m_1, ..., m_k as arguments: | |
| >>> print(FinitelyGeneratedAbelianGroup(2, 3, 4)) | |
| C4 x C2 x C3 | |
| Infinite cyclic summands can be included as Z = Z/0: | |
| >>> print(FinitelyGeneratedAbelianGroup(0, 0, 2)) | |
| Z x Z x C2 | |
| By default, FinitelyGeneratedAbelianGroups are listed | |
| by their elementary divisors: we split the group into | |
| as many direct summands as possible (perhaps applying the | |
| Chinese Remainder Theorem). | |
| >>> FinitelyGeneratedAbelianGroup(10) | |
| FinitelyGeneratedAbelianGroup(2, 5) | |
| >>> FinitelyGeneratedAbelianGroup(12, 60) | |
| FinitelyGeneratedAbelianGroup(4, 4, 3, 3, 5) | |
| >>> print(FinitelyGeneratedAbelianGroup(100)) | |
| C4 x C25 | |
| To recombine the summands into as few direct sums as possible, | |
| we can find the invariant factors: | |
| >>> G = FinitelyGeneratedAbelianGroup(0, 0, 2, 4, 8, 3, 9, 5) | |
| >>> G.invariant_factors() | |
| [0, 0, 360, 12, 2] | |
| >>> G.elementary_divisors() | |
| [0, 0, 8, 4, 2, 9, 3, 5] | |
| Direct sums concatenate the direct summands: | |
| >>> Z_C4 = FinitelyGeneratedAbelianGroup(0, 4) | |
| >>> Z_Z_C3 = FinitelyGeneratedAbelianGroup(0, 0, 3) | |
| >>> print(Z_C4.direct_sum(Z_Z_C3)) | |
| Z x Z x Z x C4 x C3 | |
| Other available operations include tensor products, | |
| the abelian group hom(G1, G2) of functions | |
| between two abelian groups under pointwise addition, | |
| along with Tor and Ext functors. | |
| >>> Z_C4 = FinitelyGeneratedAbelianGroup(0, 4) | |
| >>> print(Z_C4.tensor(Z_C4)) | |
| Z x C4 x C4 x C4 | |
| >>> print(Z_C4.tor(Z_C4)) | |
| C4 | |
| >>> print(Z_C4.hom(Z_C4)) | |
| Z x C4 x C4 | |
| >>> print(Z_C4.ext(Z_C4)) | |
| C4 x C4 | |
| The name "Zmod" is an alias for FinitelyGeneratedAbelianGroup. | |
| The name "Z" is an alias for Zmod(0). | |
| The name "trivial" is an alias for Zmod(). | |
| """ | |
| # elementary_divisor_prime_exponents is a dict | |
| # that maps a prime p to a tuple of exponents e | |
| # for which there is an elementary divisor of | |
| # the form Z/p**e, with multiplicity. | |
| # The tuples are in decreasing order. | |
| # free_rank is an integer. | |
| # One might improve performance for huge sets of generators | |
| # by using a collections.Counter instead of a lists/tuples, | |
| # but I haven't bothered. | |
| __slots__ = ("_ed_prime_exponents", "free_rank") | |
| def __new__(cls, *divisors): | |
| ed = defaultdict(list) | |
| rank = 0 | |
| for d in divisors: | |
| d = abs(d) | |
| if d == 0: | |
| rank += 1 | |
| elif d == 1: | |
| pass | |
| else: | |
| # Do all the factorization here. | |
| # After this, work only with primes and exponents. | |
| for p, e in factorint(d).items(): | |
| ed[int(p)].append(int(e)) | |
| return cls._from_data(ed, rank) | |
| @classmethod | |
| def _from_data(cls, ed_prime_exponents, free_rank): | |
| obj = object.__new__(cls) | |
| # Make sure everything is in a predictable order | |
| obj._ed_prime_exponents = { | |
| p: tuple(sorted(e_list, reverse=True)) | |
| for p, e_list in sorted(ed_prime_exponents.items()) | |
| if e_list | |
| } | |
| obj.free_rank = free_rank | |
| return obj | |
| def __eq__(self, other): | |
| """ | |
| >>> Zmod(2, 3) == Zmod(6) | |
| True | |
| >>> Zmod(2, 4) == Zmod(8) | |
| False | |
| """ | |
| if not isinstance(other, __class__): | |
| return NotImplemented | |
| return (self._ed_prime_exponents == other._ed_prime_exponents | |
| and self.free_rank == other.free_rank) | |
| def __hash__(self): | |
| """ | |
| >>> hash(Zmod(2,3)) == hash(Zmod(3,2)) == hash(Zmod(6)) | |
| True | |
| >>> hash(Zmod(2,3,0)) == hash(Zmod(2,3)) | |
| False | |
| """ | |
| return hash((tuple(self._ed_prime_exponents.items()), self.free_rank)) | |
| def elementary_divisors(self): | |
| """ | |
| >>> Zmod(0, 30, 4).elementary_divisors() | |
| [0, 4, 2, 3, 5] | |
| """ | |
| result = [0] * self.free_rank | |
| for p, e_list in self._ed_prime_exponents.items(): | |
| for e in e_list: | |
| result.append(p**e) | |
| return result | |
| def invariant_factors(self): | |
| """ | |
| >>> Zmod(0, 30, 4).invariant_factors() | |
| [0, 60, 2] | |
| """ | |
| result = [0] * self.free_rank | |
| torsion = zip_longest(*[ | |
| [p**e for e in e_list] | |
| for p, e_list in self._ed_prime_exponents.items() | |
| ], fillvalue=1) | |
| for p_component_tup in torsion: | |
| result.append(prod(p_component_tup)) | |
| return result | |
| def __repr__(self): | |
| ed_str = ", ".join(map(str, self.elementary_divisors())) | |
| return f"{type(self).__qualname__}({ed_str})" | |
| def __str__(self): | |
| """ | |
| >>> print(Zmod(0,1,2,3,4)) | |
| Z x C4 x C2 x C3 | |
| """ | |
| factors = [] | |
| for d in self.elementary_divisors(): | |
| factors.append("Z" if d == 0 else f"C{d}") | |
| if not factors: | |
| return "trivial" | |
| return " x ".join(factors) | |
| @classmethod | |
| def from_string(cls, s): | |
| """ | |
| >>> Zmod.from_string("C12 x Z x Z^2 x C40") | |
| FinitelyGeneratedAbelianGroup(0, 0, 0, 8, 4, 3, 5) | |
| """ | |
| divisors = [] | |
| for summand in s.split("x"): | |
| summand = summand.strip() | |
| summand, caret, multiplicity = summand.strip().partition("^") | |
| multiplicity = int(multiplicity) if multiplicity else 1 | |
| if multiplicity < 0: | |
| raise ValueError | |
| summand = summand.strip() | |
| if summand == "Z": | |
| d = 0 | |
| elif summand.startswith("C"): | |
| d = int(summand.removeprefix("C")) | |
| else: | |
| raise ValueError(f"Couldn't parse {summand!r}") | |
| divisors.extend([d] * multiplicity) | |
| return cls(*divisors) | |
| def free_part(self): | |
| """ | |
| >>> print(Zmod(2, 3, 0, 0).free_part()) | |
| Z x Z | |
| """ | |
| return type(self)._from_data({}, self.free_rank) | |
| def torsion_part(self): | |
| """ | |
| >>> print(Zmod(2, 3, 0, 0).torsion_part()) | |
| C2 x C3 | |
| """ | |
| return type(self)._from_data(self._ed_prime_exponents, 0) | |
| def direct_sum(*summands): | |
| """ | |
| >>> Zmod(2).direct_sum(Zmod(15)) == Zmod(30) | |
| True | |
| >>> Zmod(2).direct_sum(Zmod(20)) == Zmod(40) | |
| False | |
| """ | |
| # Just collect together the direct summands of each | |
| primes = set() | |
| for G in summands: | |
| primes |= G._ed_prime_exponents.keys() | |
| new_ed_prime_exponents = {} | |
| for p in primes: | |
| new_e_list = [] | |
| for G in summands: | |
| new_e_list.extend(G._ed_prime_exponents.get(p, ())) | |
| new_ed_prime_exponents[p] = new_e_list | |
| r = sum(G.free_rank for G in summands) | |
| return __class__._from_data(new_ed_prime_exponents, r) | |
| def tor(self, other): | |
| """ | |
| >>> print(Zmod(0, 0, 0, 9).tor(Zmod(0, 0, 0, 27))) | |
| C9 | |
| >>> print(Zmod(30).tor(Zmod(50))) | |
| C2 x C5 | |
| """ | |
| # Since Tor distributes across direct sums and Tor(Z, anything) = 0 | |
| # and Tor(Z/a, Z/b) = Z/gcd(a,b), we can work one prime at a time. | |
| primes = self._ed_prime_exponents.keys() & other._ed_prime_exponents.keys() | |
| new_ed_prime_exponents = {} | |
| for p in primes: | |
| e1_list = self._ed_prime_exponents.get(p, ()) | |
| e2_list = other._ed_prime_exponents.get(p, ()) | |
| # gcd(p**e1, p**e2) == p**min(e1, e2), so | |
| # Tor(Z/p**e1, Z/p**e2) should be Z/p**min(e1, e2) | |
| new_e_list = [min(e1, e2) for e1 in e1_list for e2 in e2_list] | |
| new_ed_prime_exponents[p] = new_e_list | |
| return type(self)._from_data(new_ed_prime_exponents, 0) | |
| def tensor(self, other): | |
| """ | |
| >>> print(Zmod(15).tensor(Zmod(10))) | |
| C5 | |
| >>> print(Zmod(0, 0).tensor(Zmod(0, 0, 0))) | |
| Z x Z x Z x Z x Z x Z | |
| >>> print(Zmod(0, 3).tensor(Zmod(0, 5))) | |
| Z x C3 x C5 | |
| """ | |
| # Since tensor distributes across direct sums | |
| # and tensor(Z/a, Z/b) = Z/gcd(a,b), we can work one prime at a time. | |
| # Unlike Tor, we also need to consider the free part. | |
| primes = self._ed_prime_exponents.keys() | other._ed_prime_exponents.keys() | |
| new_ed_prime_exponents = {} | |
| for p in primes: | |
| e1_list = self._ed_prime_exponents.get(p, ()) | |
| e2_list = other._ed_prime_exponents.get(p, ()) | |
| # gcd(p**e1, p**e2) = p**min(e1, e2), so | |
| # Z/p**e1 tensor Z/p**e2 should be Z/p**min(e1, e2) | |
| new_e_list = [min(e1, e2) for e1 in e1_list for e2 in e2_list] | |
| # Z/p**e1 tensor Z should be Z/p**e1 | |
| new_e_list.extend(e1_list * other.free_rank) | |
| # Z tensor Z/p**e2 should be Z/p**e2 | |
| new_e_list.extend(e2_list * self.free_rank) | |
| new_ed_prime_exponents[p] = new_e_list | |
| r = self.free_rank * other.free_rank | |
| return type(self)._from_data(new_ed_prime_exponents, r) | |
| def ext(self, other): | |
| """ | |
| >>> print(Z.ext(Zmod(2,3,4,5,6,7))) | |
| trivial | |
| >>> print(Zmod(2).ext(Z)) | |
| C2 | |
| >>> print(Zmod(12).ext(Zmod(18))) | |
| C2 x C3 | |
| """ | |
| # Since Ext distributes across (finite!) direct sums | |
| # and Ext(Z/a, Z/b) = Z/gcd(a,b), we can work one prime at a time. | |
| primes = self._ed_prime_exponents.keys() | other._ed_prime_exponents.keys() | |
| new_ed_prime_exponents = {} | |
| for p in primes: | |
| e1_list = self._ed_prime_exponents.get(p, ()) | |
| e2_list = other._ed_prime_exponents.get(p, ()) | |
| # gcd(p**e1, p**e2) = p**min(e1, e2), so | |
| # Ext(Z/p**e1, Z/p**e2) should be Z/p**min(e1, e2) | |
| new_e_list = [min(e1, e2) for e1 in e1_list for e2 in e2_list] | |
| # Ext(Z/p**e1, Z) should be Z/p**e1 | |
| new_e_list.extend(e1_list * other.free_rank) | |
| # note: Ext(Z, anything) is trivial | |
| new_ed_prime_exponents[p] = new_e_list | |
| return type(self)._from_data(new_ed_prime_exponents, 0) | |
| def hom(self, other): | |
| """ | |
| >>> print(Z.hom(Zmod(0, 0, 0, 2, 3, 4))) | |
| Z x Z x Z x C4 x C2 x C3 | |
| >>> print(Zmod(10).hom(Zmod(0, 0, 0))) | |
| trivial | |
| >>> print(Zmod(10).hom(Zmod(5, 0, 0))) | |
| C5 | |
| """ | |
| # Since Hom() distributes across (finite!) direct sums | |
| # and Hom(Z/a, Z/b) = Z/gcd(a, b), we can work one prime at a time. | |
| primes = self._ed_prime_exponents.keys() | other._ed_prime_exponents.keys() | |
| new_ed_prime_exponents = {} | |
| for p in primes: | |
| e1_list = self._ed_prime_exponents.get(p, ()) | |
| e2_list = other._ed_prime_exponents.get(p, ()) | |
| # gcd(p**e1, p**e2) = p**min(e1, e2), so | |
| # Hom(Z/p**e1, Z/p**e2) should be Z/p**min(e1, e2) | |
| new_e_list = [min(e1, e2) for e1 in e1_list for e2 in e2_list] | |
| # note: Hom(Z/p**e1, Z) is trivial | |
| # Hom(Z, Z/p**e2) should be Z/p**e2 | |
| new_e_list.extend(e2_list * self.free_rank) | |
| new_ed_prime_exponents[p] = new_e_list | |
| r = self.free_rank * other.free_rank | |
| return type(self)._from_data(new_ed_prime_exponents, r) | |
| Zmod = FinitelyGeneratedAbelianGroup | |
| Z = FinitelyGeneratedAbelianGroup(0) | |
| trivial = FinitelyGeneratedAbelianGroup() | |
| def product_homology( | |
| homology_list_a : list[FinitelyGeneratedAbelianGroup], | |
| homology_list_b : list[FinitelyGeneratedAbelianGroup] | |
| ) -> list[FinitelyGeneratedAbelianGroup]: | |
| # See Hatcher's Algebraic Topology Book, Theorem 3B.6. | |
| len_a, len_b = len(homology_list_a), len(homology_list_b) | |
| result = [] | |
| for dim in range(len_a + len_b - 1): | |
| summands = [] | |
| for i in range(dim + 1): | |
| j = dim - i | |
| if i < len_a and j < len_b: | |
| ha = homology_list_a[i] | |
| hb = homology_list_b[j] | |
| summands.append(ha.tensor(hb)) | |
| for i in range(dim): | |
| j = dim - 1 - i | |
| if i < len_a and j < len_b: | |
| ha = homology_list_a[i] | |
| hb = homology_list_b[j] | |
| summands.append(ha.tor(hb)) | |
| result.append(FinitelyGeneratedAbelianGroup.direct_sum(*summands)) | |
| return result | |
| def cohomology_from_homology(homology_list, coefficients): | |
| # See Hatcher's Algebraic Topology Book, Theorem 3.2. | |
| G = coefficients | |
| len_list = len(homology_list) | |
| result = [homology_list[0].hom(G)] | |
| for dim in range(1, len_list): | |
| hdim, lower = homology_list[dim], homology_list[dim - 1] | |
| result.append(lower.ext(G).direct_sum(hdim.hom(G))) | |
| return result | |
| def homology_with_coefficients(homology_list, coefficients): | |
| # See Hatcher's Algebraic Topology book, Corollary 3A.4. | |
| G = coefficients | |
| len_list = len(homology_list) | |
| result = [homology_list[0].tensor(G)] | |
| for dim in range(1, len_list): | |
| hdim, lower = homology_list[dim], homology_list[dim - 1] | |
| result.append(hdim.tensor(G).direct_sum(lower.tor(G))) | |
| return result | |
| if __name__ == "__main__": | |
| import doctest | |
| doctest.testmod() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment