Gromov–Witten invariants of a quintic threefold
As mentioned in my blog post on the Gromov–Witten invariants of complex projective space, I am currently writing a thesis on Gromov–Witten invariants. I’ll leave an explanation of what they are to that post; in this post, I want to focus on my numerical calculation of the Gromov–Witten invariants of the quintic and a slight nod to the physical application of Gromov–Witten invariants.
Gromov–Witten invariants in physics
Of course, for the entire story of the relevance of Gromov–Witten in physics, one would have to read my thesis (or any other source). Long story short, the correlators in an A-twisted topological string theory on a manifold \(X\) look like
\begin{equation} \langle\mathcal{O}_1\cdots\mathcal{O}_n\rangle = \sum_{g\geq 0}\sum_{\beta\in H_2(X;\mathbb{Z})}g_s^{2g-2}e^{-\langle\omega,\beta\rangle}GW_{g,n,\beta}^X(\alpha_1\otimes\cdots\otimes\alpha_n), \end{equation}
where the \(\mathcal{O}_i\) are operator insertions that correspond to some cohomology classes \(\alpha_i\), \(\omega\) is the complexified Kähler form and \(g_s\) is the string coupling constant. There also exists another twist, the B-twist, whose (genus zero) correlators are straightforwardly calculated as
\begin{equation} \langle\mathcal{O}_1\cdots\mathcal{O}_n\rangle_{g=0} = g_s^{-2}\int_X \Omega\smile\Omega(\alpha_1\smile\cdots\smile\alpha_n), \end{equation}
where the \(\mathcal{O}_i\) now correspond to \(\Lambda^\bullet TX\)-valed antiholomorhic forms \(\alpha_i\), and \(\Omega\) is a nowhere-vanishing holomorphic top-form on \(X\) (which is therefore required to be Calabi–Yau).
Conjecturally, there is a relationship between an A-twisted theory on one Calabi–Yau threefold and a B-twisted theory on another called mirror symmetry. This relationship allows one to compute Gromov–Witten invariants using B-twisted correlators on the mirror manifold. The most (mathematically) useful way of stating mirror symmetry is an active area of research, and it remains unknown what a minimal requirement on a Calabi–Yau threefold is to assure it admits mirror symmetry. The most prominent “mirror theorems”, i.e., theorem that states whether a manifold admits mirror symmetry, are developed by Givental, for example (Givental, 1997), which was on toric complete intersections. The origin of mirror symmetry, however, lies in (Greene–Plesser, 1990), and the first Gromov–Witten invariant computations were done in (Candelas et al., 1991). The latter considers the quintic threefold (a degree five projective variety in \(\mathbb{CP}^4\)), and conjectures its mirror manifold, from which the Gromov–Witten invariants are computed. This method was proven by Givental in (Givental, 1996).
The quintic
The goal of this blog post is not at all to explore this method in detail (read my thesis for that!), but rather to sketch how I computed the Gromov–Witten invariants numerically. We will start from the following expression,
\begin{equation} q(b) = b\exp\left(5\frac{\sum_{n=1}^\infty \frac{(5n)!}{(n!)^5}\left(\sum_{j=n+1}^{5n}\frac{1}{j}\right)b^n}{1 + \sum_{n=1}^\infty\frac{(5n)!}{(n!)^5}b^n}\right), \end{equation}
which is the mirror map. In our Python script below, this is q_func. It describes a map from the complex moduli space of the quintic mirror (the B-twisted structure) to the Kähler moduli space of the quintic (the A-twisted structure). We now want to invert this to a \(b(q)\), denoted b_func in our script. We’ll be doing a numerical calculation, so we should first express \(q(b)\) as a power series, and then use a smart theorem that allows us to invert a power series into another power series.
First, turning \(q(b)\) into a power series is quite straightforward — one simply uses the Taylor series of the exponential and \(\frac{1}{1+x}\), which are known to most undergrads.
The aforementioned smart theorem is the Lagrange inversion theorem. For \(q(b) = \sum_{n\geq 1}q_nb^n\) as above, we can use \(q(0)=0\) to find its inverse \(b(q) = \sum_{n\geq 1}b_nq^n\) with
\begin{equation} b_n = \lim_{b\to 0}\frac{\mathrm{d}^{n-1}}{\mathrm{d}b^{n-1}}\left(\frac{b}{f(b)}\right)^n. \end{equation}
Wikipedia lists the following variant, citing C.A. Charalambides’s Enumerative Combinatorics (to which I sadly don’t have access),
\begin{equation} b_n = \frac{1}{q_1^n}\sum_{k=1}^{n-1}(-1)^k\frac{(n+k-1)!}{(n-1)!}B_{n-1,k}(\hat{q}_1,\dots,\hat{q}_{n-k}), \end{equation}
for \(B_{n-1,k}\) a Bell polynomial and
\begin{equation} \hat{q}_k = \frac{q_{k+1}}{(k+1)q_1}. \end{equation}
The Bell polynomial can simply be imported from sympy, significantly simplifying the process. This gives us a power series for \(b(q)\) that is precise up to some degree of our choosing.
Next up, we should calculate the correlators. In the B-twist, we can derive that the three-point function is given by
\begin{equation} \langle\mathcal{O}_\theta\mathcal{O}_\theta\mathcal{O}_\theta\rangle = \frac{5/(2\pi i)^3}{(1-5^5b)\left(1 + \sum_{n=1}^\infty\frac{(5n)!}{(n!)^5}b^n\right)^2}. \end{equation}
We then derive that the A-twist three-point function is
\begin{equation} \langle\mathcal{O}_H\mathcal{O}_H\mathcal{O}_H\rangle = \frac{5}{(1-5^5b(q))\left(1 + \sum_{n=1}^\infty\frac{(5n)!}{(n!)^5}b(q)^n\right)^2}\left(\frac{q}{b(q)}\frac{\mathrm{d}b(q)}{\mathrm{d}q}\right)^3. \end{equation}
For this, we use more numerical evaluation of products, fractions and derivatives to construct a power series of \(\langle\mathcal{O}_H\mathcal{O}_H\mathcal{O}_H\rangle\), which is very doable. We then find the following:
\begin{equation} \langle\mathcal{O}_H\mathcal{O}_H\mathcal{O}_H\rangle = 5 + 2{,}875q + 4{,}876{,}875q^2 + 8{,}564{,}575{,}000q^3+\dots. \end{equation}
The final step is to take the power series above and compute the Gromov–Witten invariants \(N_0(d)\) and instanton numbers \(n_0(d)\). We find the following results up to degree five:
| \(\boldsymbol{d}\) | \(\boldsymbol{1}\) | \(\boldsymbol{2}\) | \(\boldsymbol{3}\) | \(\boldsymbol{4}\) | \(\boldsymbol{5}\) |
|---|---|---|---|---|---|
| \(\boldsymbol{N_0(d)}\) | \(2{,}875\) | \(\frac{4{,}876{,}875}{8}\) | \(\frac{8{,}564{,}575{,}000}{27}\) | \(\frac{15{,}517{,}926{,}796{,}875}{64}\) | \(229{,}305{,}888{,}887{,}648\) |
| \(\boldsymbol{n_0(d)}\) | \(2{,}875\) | \(609{,}250\) | \(317{,}206{,}375\) | \(242{,}467{,}530{,}000\) | \(229{,}305{,}888{,}887{,}625\) |
It takes our program very little time to derive these. In fact, up to degree \(15\), the program proves very fast. Beyond this, however, the speed decreases quickly.
Implementation
Below, you can find my implementation. A good first step to improvement is to increase calculation speed is to sequentially calculate each degree, outputting intermediate results and allowing those values to be used for future calculations. In the current form, the entire calculation has to be redone for every degree if you want to find another degree. However, for the purpose of my thesis, finding the numbers up to degree five is plenty.
from sympy import symbols, Symbol, bell, expand
from sympy.polys.polytools import Poly
from math import factorial
from fractions import Fraction
from collections.abc import Callable
class TaylorSeries:
# Creates a Taylor series, allowing for polynomial representations
def __init__(self, nthterm: Callable[[int],Fraction]):
self.nthterm = nthterm
def as_polynomial(self, degree: int, variable: Symbol) -> Poly:
polynomial = 0
for n in range(degree+1):
polynomial += self.nthterm(n)*variable**n
return Poly(polynomial, variable)
def truncate_poly(f: Poly, degree: int) -> Poly:
# Truncates a polynomial to some degree
result = {}
monomials = f.monoms()
coeffs = f.coeffs()
for i in range(len(monomials)):
if all(d <= degree for d in monomials[i]):
result[monomials[i]] = coeffs[i]
return Poly.from_dict(result, *f.gens)
def division(f: Poly, g:Poly, degree: int, variable: Symbol) -> Poly:
# Divides two Taylor series up to some degree
g_const = g.coeff_monomial(1)
g_new = g - g_const
result = 0
for n in range(0,degree+1):
result += (-1)**n*Fraction(1,g_const**n)*g_new**n
return truncate_poly(f*result, degree)
def exponentiation(f: Poly, degree: int) -> Poly:
# Exponentiates some polynomial up to some degree
result = 0
for n in range(0,degree+1):
result += f**n*Fraction(1,factorial(n))
return truncate_poly(result, degree)
def lagrange_bell_invert(f: Poly, old_variable: Symbol, new_variable: Symbol) -> Poly:
# An implementation of the Lagrange inversion theorem using Bell polynomials
deg = f.degree(gen=old_variable)
f_list = [f.as_expr().coeff(old_variable,i)*factorial(i) for i in range(deg+1)]
if deg >= 1 and f_list[1] != 0:
if f_list[0] == 0:
fhat_list = [Fraction(f_list[k],k*f_list[1]) for k in range(1,len(f_list))]
g = new_variable*Fraction(1,f_list[1])
for n in range(2,len(f_list)):
res = 0
for k in range(1,n):
funct_tup = tuple(fhat_list[i] for i in range(1,n-k+1))
res += (-1)**k*Fraction(factorial(n+k-1),factorial(n-1))*bell(n-1,k,funct_tup)
g += res*Fraction(1,f_list[1]**n*factorial(n))*new_variable**n
return g.as_poly()
else:
raise Exception("This function has a nonzero constant term, so the algorithm doesn't apply.")
else:
raise Exception("This function isn't invertible.")
if __name__ == "__main__":
# b/q are the complex/Kähler moduli coordinates
b, q = symbols('b q')
# Max degree to calculate GWs for
degree = 5
# The period
omega0 = TaylorSeries(nthterm = lambda n : Fraction(factorial(5*n),(factorial(n))**5))
# The perturbation of the second period
def sum_of_recips(n: int) -> Fraction:
result = 0
for i in range(n+1,5*n+1):
result += Fraction(1,i)
return result
psi = TaylorSeries(nthterm = lambda n : Fraction(5*factorial(5*n),(factorial(n))**5)*sum_of_recips(n))
# Mirror map q(b)
q_func = b*exponentiation(division(psi.as_polynomial(degree+1,b),omega0.as_polynomial(degree,b),degree,b),degree)
# Mirror map b(q)
b_func = lagrange_bell_invert(q_func,b,q)
# Yukawa <θθθ>(b) (in B-model)
yukawa_B = division(1,(1-5**5*b)*omega0.as_polynomial(degree,b)**2,degree,b)
# Yukawa <HHH>(q) (in A-model)
yukawa_A = truncate_poly(Poly(truncate_poly(truncate_poly(yukawa_B*(q_func/b)**3,degree).compose(b_func),degree),q)*b_func.diff()**3,degree)
# Results
print("q(b) = " + str(q_func.as_expr()))
print("b(q) = " + str(b_func.as_expr()))
print("<θθθ> = " + str(5*yukawa_B.as_expr()))
print("<HHH> = " + str(5*yukawa_A.as_expr()))
# Gromov-Witten invariants
N = [5*yukawa_A.as_expr().coeff(q,0)] + [5*yukawa_A.as_expr().coeff(q,i)/i**3 for i in range(1,degree+1)]
print("N = " + str(N))
# Instanton numbers
N_temp = N.copy()
n = [N[0]]
for i in range(1,degree+1):
n.append(N_temp[i])
for k in range(1,degree//i+1):
N_temp[i*k] = N_temp[i*k] - Fraction(n[i],k**3)
print("n = " + str(n))