Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
The Künneth formula for products (1923)#
Hermann Künneth showed that the Betti numbers of a product are a convolution: \(\beta_k(X \times Y) = \sum_{i+j=k} \beta_i(X)\,\beta_j(Y)\), the coefficients of the product of the two Poincaré polynomials. The product of two simplicial complexes is triangulated by staircase paths through each product of simplices. Circle times circle is the torus, \((1 + t)^2 = 1 + 2t + t^2\), and circle times sphere gives \((1 + t)(1 + t^2)\).
import matplotlib.pyplot as plt
import numpy as np
from mathematicskit.topology import betti_numbers, circle, simplex, simplicial_product, sphere
def poincare_polynomial(betti):
return " + ".join(f"{b}t^{k}" if k else f"{b}" for k, b in enumerate(betti) if b) or "0"
pairs = {
"circle x circle": (circle(3), circle(4)),
"circle x interval": (circle(4), simplex(1)),
"circle x sphere": (circle(3), sphere(2)),
"sphere x sphere": (sphere(2), sphere(2)),
"torus x circle": (simplicial_product(circle(3), circle(3)), circle(3)),
}
for name, (X, Y) in pairs.items():
product = simplicial_product(X, Y)
predicted = poincare_polynomial(np.convolve(betti_numbers(X), betti_numbers(Y)))
print(f"{name:>17}: predicted {predicted:>22}, computed {poincare_polynomial(betti_numbers(product)):>22}, f = {product.f_vector}")
circle x circle: predicted 1 + 2t^1 + 1t^2, computed 1 + 2t^1 + 1t^2, f = (12, 36, 24)
circle x interval: predicted 1 + 1t^1, computed 1 + 1t^1, f = (8, 16, 8)
circle x sphere: predicted 1 + 1t^1 + 1t^2 + 1t^3, computed 1 + 1t^1 + 1t^2 + 1t^3, f = (12, 48, 72, 36)
sphere x sphere: predicted 1 + 2t^2 + 1t^4, computed 1 + 2t^2 + 1t^4, f = (16, 84, 216, 240, 96)
torus x circle: predicted 1 + 3t^1 + 3t^2 + 1t^3, computed 1 + 3t^1 + 3t^2 + 1t^3, f = (27, 189, 324, 162)
A square times a square, drawn as a flat torus#
Each square of the grid is a product of two edges, cut into two triangles along the staircase diagonal. The squares that wrap around (glued to the opposite side) are left out of the drawing.
T = simplicial_product(circle(4), circle(4))
grid = np.array([(i, j) for i in range(4) for j in range(4)], dtype=float)
fig, ax = plt.subplots(figsize=(5, 5))
for tri in T.simplices(2):
pts = grid[list(tri)]
if np.ptp(pts[:, 0]) <= 1 and np.ptp(pts[:, 1]) <= 1:
ax.add_patch(plt.Polygon(pts, alpha=0.4, ec="k"))
ax.set_xlim(-0.2, 3.2)
ax.set_ylim(-0.2, 3.2)
ax.set_aspect("equal")
ax.set_title(f"$S^1 \\times S^1$: f = {T.f_vector}, β = {betti_numbers(T)}")

Text(0.5, 1.0, '$S^1 \\times S^1$: f = (16, 48, 32), β = (1, 2, 1)')
Total running time of the script: (0 minutes 0.113 seconds)