Math laboratory
Complex tori, the modular group and the j-invariant
Computes the j-invariant of the torus C/(Z + tau Z) from q-expansions of Eisenstein series, checks its invariance under tau -> tau + 1 and tau -> -1/tau, evaluates j(i) = 1728 and j(rho) = 0, and reduces points of the upper half-plane to the fundamental domain of SL(2, Z): tori form a one-complex-parameter family. Numerical evidence (truncated series).
Back to the labNumerical approximation
"""Complex tori and the j-invariant.
Every complex torus is C / (Z + tau Z) with tau in the upper half-plane, and
two tori are biholomorphic exactly when their tau's differ by an element of
SL(2, Z) acting by Mobius transformations. The j-invariant is a complete
invariant. We compute j(tau) = 1728 E4^3 / (E4^3 - E6^2) from the
q-expansions of the Eisenstein series (q = exp(2 pi i tau)), check that
j(tau + 1) = j(-1/tau) = j(tau), compute j(i) = 1728 and j(exp(2 pi i/3)) = 0,
reduce points to the standard fundamental domain, and show that tori of the
same shape but different size have the same j while the square and the
hexagonal tori are different. Numerical evidence from truncated series.
"""
import cmath
import math
def sigma(k, n):
"""Sum of the k-th powers of the divisors of n."""
return sum(d**k for d in range(1, n + 1) if n % d == 0)
def eisenstein(tau, terms=60):
"""(E4, E6) at tau via E4 = 1 + 240 sum s3(n) q^n, E6 = 1 - 504 sum s5(n) q^n."""
q = cmath.exp(2j * math.pi * tau)
e4, e6, qn = 1 + 0j, 1 + 0j, 1 + 0j
for n in range(1, terms + 1):
qn *= q
e4 += 240 * sigma(3, n) * qn
e6 -= 504 * sigma(5, n) * qn
return e4, e6
def j_invariant(tau, terms=60):
"""j(tau) = 1728 E4^3 / (E4^3 - E6^2)."""
e4, e6 = eisenstein(tau, terms)
return 1728 * e4**3 / (e4**3 - e6**2)
def reduce_to_fundamental_domain(tau, max_steps=100):
"""Apply T: tau -> tau + n and S: tau -> -1/tau until |Re| <= 1/2, |tau| >= 1."""
steps = []
for _ in range(max_steps):
n = round(tau.real)
if n:
tau -= n
steps.append(f"T^{-n}")
if abs(tau) < 1 - 1e-12:
tau = -1 / tau
steps.append("S")
else:
return tau, steps
raise RuntimeError("did not converge")
def lattice_tau(w1, w2):
"""The tau of the lattice Z w1 + Z w2, normalised to the upper half-plane."""
tau = w2 / w1
return tau if tau.imag > 0 else -tau
def main():
i = 1j
rho = cmath.exp(2j * math.pi / 3)
print(f"j(i) = {j_invariant(i).real:12.4f} (square torus; exact 1728)")
print(f"j(rho) = {abs(j_invariant(rho + 1)):12.4e} (hexagonal torus; exact 0)")
tau = 0.3 + 1.1j
print(f"j(tau) = {j_invariant(tau):.6f} for tau = 0.3 + 1.1i")
print(f"j(tau + 1) = {j_invariant(tau + 1):.6f}")
print(f"j(-1/tau) = {j_invariant(-1 / tau, terms=120):.6f}")
print()
print("Reducing tau to the fundamental domain |Re tau| <= 1/2, |tau| >= 1:")
for start in [3.7 + 0.2j, -0.45 + 0.05j, 0.1 + 0.6j]:
end, steps = reduce_to_fundamental_domain(start)
moves = " ".join(steps) if steps else "(already reduced)"
x, y = round(end.real, 4) + 0.0, round(end.imag, 4)
print(f" {start}: {moves} -> {x:+.4f}{y:+.4f}i")
print(f" j before {j_invariant(start, 400):.2f}")
print(f" j after {j_invariant(end):.2f}")
print()
t1 = lattice_tau(1 + 0j, 1j)
t2 = lattice_tau(2 + 2j, -2 + 2j)
print("Same shape, different size: lattices Z + iZ and (2+2i)Z + (-2+2i)Z")
print(f" j = {j_invariant(t1).real:.4f} and {j_invariant(t2).real:.4f}")
print()
print("Epistemic status: floating-point evaluation of truncated q-series, so")
print("evidence only. The invariance of j under SL(2, Z) and the fact that j")
print("classifies tori up to biholomorphism are theorems (see the lesson).")
if __name__ == "__main__":
main()Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.