Moduli-space limits and custom prepotentials#
What’s in this notebook? A theory + reference walkthrough of the moduli-space limits implemented in JAXVacua — the Large Complex Structure (LCS) limit, the coni-LCS limit near a conifold singularity, the coni-LCS bulk approximation, and the conifold series expansion — followed by the API for feeding custom periods or prepotentials to
FluxEFTwhen the limit you need isn’t shipped natively.In this notebook, you will learn:
The defining inequalities and prepotential structure of LCS, coni-LCS, the coni-LCS bulk approximation, and the conifold series expansion.
Which model fixture each limit naturally uses and how to construct one from raw CYTools polytope data.
How
prepotential_input=Fandperiod_input=Pilet you bypass the catalogue entirely and ship a hand-written analytic prepotential.When the
conjflag matters and how to validate periods via the symplectic pairing.Prerequisites: NB02 — JAXVacua overview, NB05 — Finding flux vacua.
Outline#
Setup#
import warnings, time, math
from functools import partial
import numpy as np
from scipy.optimize import root
import jax
from jax import jit, vmap
import jax.numpy as jnp
jax.config.update("jax_enable_x64", True)
import matplotlib.pyplot as plt
import seaborn as sn
cmap = sn.color_palette("viridis", as_cmap=True)
import jaxvacua as jvc
warnings.filterwarnings("ignore")
Section-specific fixtures. Each limit section uses the model that best illustrates the corresponding feature:
§LCS — the degree-18 hypersurface in \(\mathbb{CP}^{1,1,1,6,9}\) (
h12=2, model_ID=1), the standard test geometry already used in NB05/NB07/NB08.§coni-LCS / §Bulk / §Series — a CYTools-built polytope with a conifold curve \(C_{\rm cf} = [-1,1,0]\) — a 3-dimensional Mori cone, so \(h^{2,1} = 3\) on the JAXVacua side (mirror \(h^{1,1}_{\widetilde X} = 3\)). The basis change
[[0,1,1],[1,1,0],[0,0,1]]puts the conifold modulus in the first coordinate.§Custom — the K-point geometry \(X_{3,3}\) (one-modulus, closed-form prepotential), where we ship the analytic \(F(X)\) directly through
prepotential_input=F.
The shared §4 setup only imports jaxvacua as jvc; each downstream section builds its own model + sampler since the limits differ in construction.
LCS limits#
Introduction#
At Large Complex Structure (LCS), the prepotential can be expressed as
Here, the polynomial part \(F_{\mathrm{poly}}\) of the LCS prepotential \(F_{\mathrm{LCS}}\) can be expressed in terms of the periods \(X^I=(X^0,X^i)\) as
Here, \(\widetilde{\kappa}_{ijk}\) are the triple intersection numbers of the mirror dual Calabi-Yau threefold \(\widetilde{X}\). Here, we defined
The instanton part \(F_{\mathrm{inst}}\) of the LCS prepotential \(F_{\mathrm{LCS}}\) can be expressed in terms of the periods \(X^I=(X^0,X^i)\) as
Here the sum is performed over all effective curve classes \(q\in\mathcal{M}(\widetilde{X})\) in the Mori cone \(\mathcal{M}(\widetilde{X})\) of the mirror dual manifold \(\widetilde{X}\). Here, the \(n_q^{0}\) are the genus-0 Gopakumar-Vafa (GV) invariants which can be computed systematically using methods described in hep-th/9308122.
The infinite sum appearing in the polylogarithm \(\text{Li}_3\) can be rewritten to arrive at
in terms of genus-0 Gromov-Witten (GW) invariants \(N_q\). We typically work with the latter to simplify the calculation.
The above functions are implemented in the periods class and are selected automatically when we specify limit="LCS".
Example at \(h^{1,2} = 2\) - \(\mathbb{CP}[1,1,1,6,9]\)#
We load a pre-built LCS model using stored topological data. The key constructor parameters are:
h12: number of complex structure moduli \(h^{1,2}\)model_ID: selects among the pre-stored models of the given type andh12value (here the defaultmodel_type="KS"refers to models in the Kreuzer–Skarke database)limit="LCS": activates the LCS prepotential (implemented in theperiodsclass), which evaluates \(F = F_{\rm poly} + F_{\rm inst}\)maximum_degree: truncates the GV instanton sum at curves up to this total degree;maximum_degree=1keeps only the two leading worldsheet corrections, which is sufficient for a quick benchmark but shifts the minimum compared to the fully converged result
model = jvc.FluxVacuaFinder(h12=2,model_ID=1,maximum_degree=1, limit="LCS")
model
This is the \(\mathbb{CP}^4_{[1,1,1,6,9]}\) hypersurface discussed in 1912.10047. The topological data is accessible through the lcs_tree attribute. We can verify that the intersection numbers, the \(a\)-matrix \(a_{ij}\), and the \(b\)-vector \(b_i\) match the expected values:
model.lcs_tree.a_matrix = jnp.array([[4.5,1.5],[1.5,0.]])
model.lcs_tree.intnums,model.lcs_tree.a_matrix,model.lcs_tree.b_vector
JAXVacua uses the genus-0 Gromov–Witten (GW) invariants \(N_q\) to evaluate \(F_{\rm inst}\) (obtained from the GV invariants \(n_q^0\) via \(N_q = \sum_{d|q} n_{q/d}^0 / d^3\)). With maximum_degree=1, only the two simplest curve classes on the mirror contribute:
\(q = (1,0)\): GV invariant \(n_q^0 = 540\)
\(q = (0,1)\): GV invariant \(n_q^0 = 3\)
gv_invariants holds the values \(n_q^0\) and gv_charges holds the corresponding curve class vectors \(q_i\). Including higher-degree curves (e.g. maximum_degree=2 adds \(q = (1,1), (2,0), (0,2), \ldots\)) shifts the vacuum by a sub-leading correction — typically affecting only the 3rd significant digit for maximum_degree \(\geq 2\).
model.lcs_tree.gv_invariants,model.lcs_tree.gv_charges
As a concrete example, we target the small-\(|W_0|\) minimum from 2501.03984 (Eq. 4.11). The initial moduli and flux values are taken from the paper. Note that [2501.03984] used a higher GV truncation degree; with our maximum_degree=1 the Newton solver will converge to the nearby minimum of the truncated model, which differs slightly from the paper’s exact vacuum.
z0 = jnp.array([0.5+ 2.36817528j, 0.5+ 2.51175911j])
fluxes = jnp.array([4, 12, 2, -1, 0, -1, 36, -1, 0, 0, 1, -1.])
tau0 = 0.5+1j*1.48121567
At these starting values (taken from a higher-degree GV model), the \(F\)-term residuals are \(O(1)\) — the initial guess is far from the maximum_degree=1 vacuum:
model.DW(z0,jnp.conj(z0),tau0,jnp.conj(tau0),fluxes)
We refine numerically using newton_method_flux_vacua. Key parameters:
step_size_Newton=1: full Newton step; gives quadratic convergence near the minimum (reduce to ~0.1 for non-SUSY vacua where the basin of attraction may be small)tol=1e-12: terminates when \(\max_I |D_I W| < 10^{-12}\)mode="SUSY": solves the system \(D_I W = 0\) (supersymmetric vacuum); usemode="non-SUSY"to minimise \(|\partial_i V|^2\) insteadsolver_mode="complex": works in the complex moduli parameterisation (more efficient for SUSY vacua than the real-variable solver)
(Note: the first call triggers jax.jit compilation, which can take a few seconds.)
moduli,tau,residual = model.newton_method_flux_vacua(z0,tau0,fluxes,step_size_Newton = 1,tol=1e-12,max_iters=100,mode="SUSY",solver_mode="complex")
print(f"|z-z0|: {np.abs(z0-moduli)} |tau-tau0|: {np.abs(tau0-tau)} |DW|: {residual}")
Newton’s method has converged to the maximum_degree=1 minimum. The moduli are shifted from the paper’s starting values because the truncated GV sum changes the location of the minimum. The \(F\)-term conditions are now satisfied to machine precision:
model.DW(moduli,jnp.conj(moduli),tau,jnp.conj(tau),fluxes)
Newton convergence#
newton_method_flux_vacua internally works in the real representation: _convert_complex_to_real(z, z̄, τ, τ̄) packs the real and imaginary parts of \((z, \tau)\) into a flat real array x ∈ ℝ^{2(h^{1,2}+1)}, and DW_x(x, flux) evaluates all \(\{{\rm Re}(D_I W), {\rm Im}(D_I W)\}\) as a flat real vector. The Jacobian dDW_x is computed analytically via JAX. We can run the Newton step manually to visualise the (quadratic) convergence of \(\max_I|D_I W|\):
# Manual Newton iteration to track convergence
x = model._convert_complex_to_real(z0, jnp.conj(z0), tau0, jnp.conj(tau0))
n_iters = 12
residuals = []
for _ in range(n_iters):
dw = model.DW_x(x, fluxes)
residuals.append(float(jnp.max(jnp.abs(dw))))
J = model.dDW_x(x, fluxes)
x = x - jnp.linalg.solve(J, dw)
fig, ax = plt.subplots(figsize=(5.5, 3.5))
ax.semilogy(range(n_iters), residuals, "o-", color="steelblue", linewidth=2, markersize=5)
ax.set_xlabel("Newton iteration")
ax.set_ylabel(r"$\max_I\,|D_I W|$")
ax.set_title(r"Newton convergence — LCS example ($h^{1,2}=2$)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
We extract the physical observables at the vacuum:
tadpole(fluxes)returns the D3-brane tadpole contribution \(N_{\rm flux} = \tfrac{1}{2}\int H_3 \wedge F_3\)superpotential_gauge_invariantcomputes \(W_0 = W/X^0\) in the gauge-invariant normalisation (divided by the period \(X^0\), making it independent of the overall period normalisation); this is the quantity directly compared to literature values
Nflux = model.tadpole(fluxes)
W = model.superpotential_gauge_invariant(moduli,tau,fluxes)
print(f"Nflux: {Nflux} |W0|: {np.abs(W)} ")
This matches the value \(|W_0|\approx 5.5\times 10^{-5}\) stated in Eq. (4.11) in 2501.03984. Note that small deviations are noticeable here because we are only using the leading order GVs. Sub-leading corrections shift the minimum and the value of \(W_0\) slightly, but affecting the 3rd significant digit.
coni-LCS limits#
Introduction#
To construct a Klebanov-Strassler throat region in a flux compactification, we must stabilise the complex structure moduli near a conifold locus. To do so, we work near a boundary of moduli space where a conifold curve shrinks following the approach of 2009.03312, see also 2009.03325 for similar results. Here we will just sketch the construction, referring the reader to the original work for more details.
A conifold singularity is a locus in the complex structure moduli space of a Calabi-Yau threefold \(X\) where a set of \(n_{\mathrm{cf}}\) three-cycles, all of which lie in the same homology class \([\mathcal{C}] \in H_3(X,\mathbb{Z})\), shrink to zero volume. In an LCS patch, we can identify the complex structure moduli space of \(X\) with the complexified K”ahler cone \(\mathcal{K}(\widetilde{X})\) of the mirror threefold \(\widetilde{X}\). In this picture, the conifold locus is identified with the facet of \(\mathcal{K}(\widetilde{X})\) where a fixed set of curves \(\mathcal{C}_{\mathrm{cf}}\) in some effective curve class \(\tilde{\mathbf{q}}^{\mathrm{cf}} \in \mathcal{M}(\widetilde{X})\cap H_2(\widetilde{X},\mathbb{Z})\), which we call the conifold class, shrink to zero size. We will defer discussion of how to actually find conifolds and how to compute \(n_{\mathrm{cf}}\) to 2406.13751, and for now just assume the existence of some conifold class \(\tilde{\mathbf{q}}^{\mathrm{cf}}\). The volume of the curves \(\mathcal{C}_{\mathrm{cf}}\) is measured by the absolute value of
In the following, it is convenient to work in a basis where \(z_{\text{cf}}=z^1\) and \(z_{\text{bulk}}^\alpha = z^\alpha\) for \(\alpha=2,\ldots,h^{1,2}\). The prepotential can then be written as an expansion in powers of \(z_{\text{cf}}\) as (see 2009.03312) \begin{equation} F_{\mathrm{coni-LCS}}(z) = n_{\text{cf}},\dfrac{z_{\text{cf}}^2}{4\pi\mathrm{i}},\ln(-2\pi\mathrm{i}z_{\text{cf}}) +\sum_{n=0}^{\infty}, \dfrac{F^{(n)}(z^\alpha)}{n!}, z_{\text{cf}}^n \end{equation} where the higher order terms are obtained from \begin{equation} F^{(n)}(z^\alpha) = (\partial_{z_{\text{cf}}}^n F_{\mathrm{poly}})\bigl |{z{\text{cf}}=0} - n_{\text{cf}}\dfrac{\hat{\zeta}(3-n)}{(2\pi\mathrm{i})^{3-n}},- \dfrac{1}{(2\pi\mathrm{i})^{3-n}}, \sum_{[\mathcal{C}]\neq [\mathcal{C}{\mathrm{cf}}]}, n{\mathcal{C}}^0, (\beta_1^\mathcal{C})^n, \mathrm{Li}{3-n}(q^{\mathcal{C}})\bigl |{z_{\text{cf}}=0} \end{equation} in terms of the polynomial prepotential \begin{equation} F_{\mathrm{poly}}(z) = -\frac{1}{6}\widetilde{\kappa}{abc}z^a z^b z^c+\frac{1}{2}a{ab}z^a z^b +b_{a}z^a + \dfrac{\text{i}}{2}\tilde{\xi}, . \end{equation}
The prepotential \(F_{\mathrm{coni-LCS}}(z)\) can be computed to any order in the expansion around \(|z_{\text{cf}}|\ll 1\). For reasonably large \(|z_{\text{cf}}| \gtrsim 10^{-5}\), it can be beneficial to check the full theory by taking the LCS prepotential without using Euler’s reflection formula for the 3rd polylogarithm.
Coni-LCS limits from CYTools#
We study the original example of 2009.03312 with \(h^{1,1}=99\) and \(h^{1,2}=3\). The mirror dual CY can be defined as follows
from cytools import Polytope
poly = Polytope([[-1,3,-2,-1],[1,-1,0,0],[-1,0,0,1],[-1,0,0,0],[-1,0,1,1],[-1,0,2,0],[-1,0,1,0]])
cy = poly.triangulate().get_cy()
cy
To find the conifold curve, we compute GV invariants to high degree and search for nilpotent rays: effective curve classes \(\mathbf{q}\) such that \(n_\mathbf{q}^0 \neq 0\) but \(n_{m\mathbf{q}}^0 = 0\) for all multiples \(m \geq 2\). A nilpotent ray signals a rational curve that does not have multi-cover contributions — physically, it shrinks to a point (a conifold singularity) as the corresponding Kähler modulus of the mirror \(\widetilde{X}\) is taken to zero. The number of shrinking three-cycles is \(n_{\rm cf} = n_\mathbf{q}^0 / 2\).
gvs = cy.compute_gvs(max_deg=20)
grading_vector = gvs.grading_vec
gvs = gvs.dok
keys = gvs.keys()
curve_charges = np.array(list(keys))
GV_invariants = np.array(list(gvs.values()))
The nilpotent curve search: for each degree-1 curve (innermost Mori cone generator), check that all integer multiples \(m\mathbf{q}\) for \(m = 2, \ldots, 19\) have vanishing GV invariant. We record both the curve class and the GV value \(n_\mathbf{q}^0\) (from which \(n_{\rm cf} = n_\mathbf{q}^0 / 2\) follows):
nilpotent_curves = []
for key in keys:
q = np.array(key)
degree = q@grading_vector
if degree==1:
flag=True
for i in range(2,20):
gv=gvs.get(tuple(i*q),0)
if gv!=0:
flag=False
break
if flag:
nilpotent_curves.append([q,gvs[key]])
nilpotent_curves
All three nilpotent curves coincide with the generators of the toric Mori cone (as expected for this model). The Mori cone generators are:
mori = cy.toric_mori_cone(in_basis=True)
mori.extremal_rays()
In the example of 2009.03312, the authors studied the conifold singularity arising when shrinking the curve associated with the class [-1,1,0].
Let us define this as our conifold curve:
conifold_curve = np.array([-1,1,0])
We can then find a suitable basis transformation in which this curve is represented by the class [1,0,0] by calling jvc.get_basis_change:
basis_matrix = jvc.get_basis_change(conifold_curve)
basis_matrix
Let us check that it leads to the expected result
conifold_curve@basis_matrix.T
The basis change matrix is not unique: any integer unimodular matrix \(B\) satisfying \(\tilde{\mathbf{q}}^{\rm cf} \cdot B^T = (1, 0, \ldots, 0)\) is valid. get_basis_change returns one canonical choice; another valid matrix is:
basis_matrix = np.array([[0, 1, 1], [1, 1, 0], [0, 0, 1]])
conifold_curve@basis_matrix.T
coni-LCS example in JAXVacua#
We initialise the coni-LCS model. The additional parameters compared to the LCS case are:
use_cytools=True,mirror_cy=cy: reads topological data directly from the CYToolsCalabiYauobjectQ=cy.h11()+cy.h12()+2: total flux lattice dimension \(Q = 2(h^{1,2}+1)\); by mirror symmetry \(h^{1,1}_{\widetilde{X}} = h^{1,2}_X\), so \(Q = 2(h^{1,1}+1) = 200\)ncf=2: number of shrinking three-cycles \(n_{\rm cf}\) at the conifold locus (read from \(n_\mathbf{q}^0 = 4\), but the GV invariant counts the curve with multiplicity 2 in this example, giving \(n_{\rm cf} = 2\))basis_change=basis_matrix: integer unimodular matrix rotating the moduli basis so the conifold class becomes \((1,0,0)\), as required by the coni-LCS prepotential ansatz
basis_matrix = np.array([[0, 1, 1], [1, 1, 0], [0, 0, 1]])
conifold_curve = np.array([-1,1,0])
model = jvc.FluxVacuaFinder(h12=cy.h11(),
Q=cy.h11()+cy.h12()+2,
use_cytools=True,
mirror_cy=cy,
ncf=2,
maximum_degree=6,
basis_change=basis_matrix,
limit="coniLCS",
conifold_curve=conifold_curve,conifold_basis=True,
prange=10,
use_gvs=True)
We can check that the data obtained matches Eq. (4.42) in 2009.03312:
model.lcs_tree.a_matrix,model.lcs_tree.b_vector*24
We reproduce the solution from section 4.3 of 2009.03312. In the PFV (perfect flux vacuum) parametrisation, the flux vector is expressed in terms of two integer vectors \(M = (M_1, M_2, M_3)\) and \(K = (K_1, K_2, K_3)\) of length \(h^{1,2} = 3\). These encode the RR and NSNS flux quanta threading the conifold three-cycle and its dual, following the notation of [2009.03312]. Given \((M, K, \tau)\), the PFV approximation analytically solves \(D_{z_{\rm cf}} W = 0\) for the conifold modulus \(z_{\rm cf}\) and determines the bulk moduli \(z^\alpha\) via pfv_to_moduli. We start from the values in the paper:
M = np.array([4,-8,8])
K = np.array([-8,3,-6])
pfv_to_flux(M, K) converts the PFV flux integers \((M, K)\) into the full symplectic flux vector \([f \mid h] \in \mathbb{Z}^Q\) using the basis change and the coni-LCS structure. For \(h^{1,2} = 3\), the result has length \(Q = 2(h^{1,1}+1) = 200\) (though most components are zero for this simple example):
flux = model.pfv_to_flux(M,K)
flux
We now derive the corresponding \(F\)-term minimum in two ways.
Starting from two-term racetrack approximation#
Using \(g_s = 0.38\) (i.e. \(\tau_0 = i/g_s\)) from [2009.03312], we call pfv_to_moduli(M, K, τ) to obtain the initial bulk moduli and conifold modulus from the PFV analytical solution. Notice that \(z^1 = z_{\rm cf}\) is tiny (\(\sim 5 \times 10^{-6}\)) — the vacuum is deep inside the conifold regime:
gs = 0.38
tau0 = 1j/gs
z0 = model.pfv_to_moduli(M,K,tau0)
z0
The PFV approximation satisfies \(D_{z_{\rm cf}} W = 0\) by construction, but the remaining F-terms \(D_{z^\alpha} W\) and \(D_\tau W\) are only approximately zero (at the level of the racetrack truncation). The residuals confirm that this is a useful starting point but not yet the exact vacuum:
model.DW(z0,jnp.conj(z0),tau0,jnp.conj(tau0),flux)
We use scipy.optimize.root with the real representation of the F-term system. The methods used are:
_convert_complex_to_real(z, z̄, τ, τ̄)→x ∈ ℝ^{2(h^{1,2}+1)}: packs \({\rm Re}(z^a), {\rm Im}(z^a), {\rm Re}(\tau), {\rm Im}(\tau)\) into a flat arrayDW_x(x, flux)→ℝ^{2(h^{1,2}+1)}: evaluates \(\{{\rm Re}(D_I W), {\rm Im}(D_I W)\}\) as a flat real vectordDW_x(x, flux): the full Jacobian ofDW_x, computed analytically via JAX autodiff (usingjac=dDW_xavoids finite-difference approximation and significantly speeds up convergence)
# Get array of real moduli values (for optimisation purposes)
x0 = model._convert_complex_to_real(z0,jnp.conj(z0),tau0,jnp.conj(tau0))
# Run scipy.optimize.root method to find solutions to DW=0
res = root(model.DW_x,x0,args=(flux,),jac=model.dDW_x,tol=1e-10,method="hybr")
res
After scipy.optimize.root converges (res.success = True), we recover the solution x1 = res.x in the real representation and verify that all F-terms are small. DW_x returns a real vector of length \(2(h^{1,2}+1) = 8\) interleaving real and imaginary parts of \(D_I W\):
# Get solution
x1 = res.x
# Compute DW
DW = model.DW_x(x1,flux)
print("DW: ",np.abs(DW))
A physical solution must lie inside the Kähler cone. model.periods.hyperplanes stores the normal vectors \(\mathbf{n}_I\) of the cone walls; the condition \({\rm Im}(z^a)\,(\mathbf{n}_I)_a > 0\) for all wall normals \(I\) verifies that all relevant curve volumes are positive. We also check \({\rm Im}(\tau) > 0\) (physical string coupling \(g_s = 1/{\rm Im}(\tau)\)):
z1,_,t1,_ = model._convert_real_to_complex(x1)
# Check Kähler cone: all hyperplane inequalities Im(z^a) n_a > 0 must be satisfied
flag = np.all(z1.imag@model.lcs_tree.hyperplanes.T>0)&(t1.imag>0)
print("Inside facet of Kähler cone: ",flag)
print(f"z_cf = {z1[0]:.3e} (Im(z_cf) = {z1[0].imag:.3e})")
print(f"g_s = {1.0/t1.imag:.4f}")
superpotential(z, τ, flux, normalise=True) divides by the period \(X^0\) to give the gauge-invariant superpotential \(W_0 = W / X^0\), which is independent of the overall period normalisation and directly comparable to literature values. Without normalise=True, the raw superpotential \(W\) depends on the chosen normalisation of the symplectic basis.
W1 = model.superpotential(z1,t1,flux,normalise=True)
print("W0: ",np.abs(W1))
This agrees with the expected value \(|W_0|\approx 6.9\times 10^{-4}\) stated in Eq. (4.59) of 2009.03312.
Deriving PFV input#
We now show how to derive the PFV starting point from scratch, without relying on values quoted in [2009.03312]. In the PFV approximation, pfv_to_moduli(M, K, τ) analytically satisfies \(D_{z^a} W = 0\) for all complex structure moduli given \((M, K, \tau)\). The remaining condition is \(D_\tau W = 0\), which is a two-dimensional real root-finding problem for \({\rm Re}(\tau)\) and \({\rm Im}(\tau)\).
We construct a JIT-compiled objective function that takes \((\tau_{\rm re}, \tau_{\rm im})\), evaluates the PFV moduli, and returns the last two components of DW_x (corresponding to \({\rm Re}(D_\tau W)\) and \({\rm Im}(D_\tau W)\)):
@jit
def obj(x,flux):
tau0 = x[0]+1j*x[1]
z0 = model.pfv_to_moduli(M,K,tau0)
x0 = model._convert_complex_to_real(z0,jnp.conj(z0),tau0,jnp.conj(tau0))
return model.DW_x(x0,flux)[-2:]
We find a minimum as follows
x0 = np.array([0.,1.5])
pfv = root(obj,x0,args=(flux,),tol=1e-10,method="hybr")
pfv
This allows us to define the PFV solution as follows
x1 = pfv.x
tau0 = x1[0]+1j*x1[1]
gs = 1/tau0.imag
z0 = model.pfv_to_moduli(M,K,tau0)
print("g_s at the PFV level: ",gs)
Note that this solution slightly deviates from the value \(g_s=0.38\) stated in 2009.03312 (also used above) since we are not restricting to a simple 2-term racetrack.
At this initial guess, the \(F\)-term conditions are approximately satisfied
model.DW(z0,jnp.conj(z0),tau0,jnp.conj(tau0),flux)
To find the true minimum, we can run the following lines to search for roots of \(D_I W\):
# Get array of real moduli values (for optimisation purposes)
x0 = model._convert_complex_to_real(z0,jnp.conj(z0),tau0,jnp.conj(tau0))
# Run scipy.optimize.root method to find solutions to DW=0
res = root(model.DW_x,x0,args=(flux,),jac=model.dDW_x,tol=1e-10,method="hybr")
res
Let us compute the value of the F-terms
# Get solution
x1 = res.x
# Compute DW
DW = model.DW_x(x1,flux)
print("DW: ",np.abs(DW))
We check that the solution is inside the facet of the Kähler cone
z1,_,t1,_ = model._convert_real_to_complex(x1)
flag = np.all(z1.imag@model.lcs_tree.hyperplanes.T>0)&(t1.imag>0)
print("Inside facet of Kähler cone: ",flag)
Compute value of \(W_0\) with appropriate normalisation
W1 = model.superpotential(z1,t1,flux,normalise=True)
print("W0: ",np.abs(W1))
This agrees with the expected value \(|W_0|\approx 6.9\times 10^{-4}\) stated in Eq. (4.59) of 2009.03312.
Bulk approximation at coniLCS#
Introduction#
The above functions are implemented in the class periods_coniLCS_bulk which is automatically called in periods class if we specify limit="coniLCS_bulk".
In many applications, it can be convenient to integrate out the conifold modulus and simply work with the bulk moduli \(z^{\alpha}\). To do so, we discard the logarithmic term in the prepotential as well as all other higher order corrections arising from expanding around the conifold singularity. We work with the effective bulk prepotential given by \begin{equation} F_{\mathrm{eff. bulk}}(z) = F_{\mathrm{poly}}(z)+F_{\mathrm{inst}}(z) \end{equation} in terms of \begin{equation} F_{\mathrm{poly}}(z) = -\frac{1}{6}\widetilde{\kappa}{abc}z^a z^b z^c+\frac{1}{2}a{ab}z^a z^b +\tilde{b}{a}z^a + \dfrac{\text{i}}{2}\tilde{\xi}; , \quad F{\mathrm{inst}}(z) = -\frac{1}{(2\pi\mathrm{i})^3}, \sum_{\mathbf{q}\in\mathcal{M}(\widetilde{X}), \mathbf{q}\neq \mathbf{q}{\text{cf}}}, n_q^{0}, \text{Li}3\left (\text{e}^{2\pi \text{i} q_a z^a}\right ), , \end{equation} where \begin{equation} \tilde{b}{a} = b_a + \dfrac{n{\text{cf}}}{24} \delta_a^1, . \end{equation}
As we demonstrate below, this description is sufficient to derive the above minimum for the bulk complex structure moduli, while the conifold modulus can be effectively set to zero for most calculations.
Example#
We reproduce the same example from 2009.03312, now using the bulk approximation (limit="coniLCS_bulk"). Compared to limit="coniLCS", we also pass use_gvs=True to include worldsheet instanton corrections from GV invariants in the effective bulk prepotential — these are the non-conifold curves omitted in limit="coniLCS" near \(z_{\rm cf}=0\). The conifold modulus is integrated out, and the model describes only the \(h^{1,2}-1 = 2\) bulk moduli plus the axio-dilaton:
from cytools import Polytope
poly = Polytope([[-1,3,-2,-1],[1,-1,0,0],[-1,0,0,1],[-1,0,0,0],[-1,0,1,1],[-1,0,2,0],[-1,0,1,0]])
cy = poly.triangulate().get_cy()
basis_matrix = np.array([[0, 1, 1], [1, 1, 0], [0, 0, 1]])
conifold_curve = np.array([-1,1,0])
bulkEFT = jvc.FluxVacuaFinder(h12=cy.h11(), Q=cy.h11()+cy.h12()+2,
use_cytools=True,
mirror_cy=cy,
ncf=2,
use_gvs=True,
maximum_degree=10,
basis_change=basis_matrix,
conifold_basis=True,
conifold_curve = conifold_curve,
prange=10,
limit="coniLCS_bulk")
#bulkEFT
The \(a\)-matrix and \(b\)-vector should match those of the full coni-LCS model (the bulk approximation does not change the polynomial part of the prepotential, only removes the logarithmic term). Note that \(\tilde{b}_1 = b_1 + n_{\rm cf}/24\), which adds \(2/24 = 1/12\) to the first entry — this is the integrated-out correction from the conifold modulus:
bulkEFT.lcs_tree.a_matrix,bulkEFT.lcs_tree.b_vector*24
The flux vector and PFV data are the same as in the full coni-LCS case. pfv_to_flux and pfv_to_moduli work identically in both limits since the PFV parametrisation only depends on the conifold structure:
M = np.array([4,-8,8])
K = np.array([-8,3,-6])
flux = bulkEFT.pfv_to_flux(M,K)
flux
Define PFV data
gs = 0.38
tau0 = 1j/gs
z0 = bulkEFT.pfv_to_moduli(M,K,tau0)
z0
In the bulk approximation the conifold modulus is fixed at \(z_{\rm cf} = 0\). The real state vector x for the bulk degrees of freedom has length \(2(h^{1,2}) = 6\) (real/imaginary parts of two bulk moduli plus the axio-dilaton). We prepend jnp.zeros(2) (representing \({\rm Re}(z_{\rm cf}) = {\rm Im}(z_{\rm cf}) = 0\)) to call the full DW_x, then discard the first two components (which give \(D_{z_{\rm cf}} W\), no longer relevant in the bulk approximation). The Jacobian jac is the corresponding \(6\times 6\) sub-block:
@jit
def obj(x,flux):
return bulkEFT.DW_x(jnp.append(jnp.zeros(2),x),flux)[2:]
@jit
def jac(x,flux):
return bulkEFT.dDW_x(jnp.append(jnp.zeros(2),x),flux)[2:,2:]
We can find a root of the F-term conditions as follows
x0 = bulkEFT._convert_complex_to_real(z0,jnp.conj(z0),tau0,jnp.conj(tau0))[2:]
obj(x0,flux)
res = root(obj,x0,args=(flux,),jac=jac,tol=1e-10,method="hybr")
res
Let us check explicitly the F-terms
x1 = res.x
DW = obj(x1,flux)
print("DW: ",np.abs(DW))
For the bulk approximation, the Kähler cone check only applies to the bulk moduli (\(z^\alpha\), \(\alpha \geq 2\)), since \(z_{\rm cf}\) was set to zero. We project onto the bulk hyperplanes (those with nonzero components in the bulk directions) and check \({\rm Im}(\tau) > 0\):
z1,_,t1,_ = bulkEFT._convert_real_to_complex(jnp.append(jnp.zeros(2),x1))
hypers = bulkEFT.lcs_tree.hyperplanes
flag = np.all(hypers[:,1:][np.any(hypers[:,1:]!=0,axis=1)]@z1.imag[1:]>0)&(t1.imag>0)
print("Inside facet of Kähler cone: ",flag)
The gauge-invariant superpotential is again computed with normalise=True. The bulk approximation should give essentially the same \(|W_0|\) as the full coni-LCS model, since the conifold modulus contribution to \(W\) is suppressed by \(z_{\rm cf} \ln z_{\rm cf} \to 0\):
W0 = bulkEFT.superpotential(z1,t1,flux,normalise=True)
print("W0: ",np.abs(W0))
We observe that this again matches the expectation of 2009.03312.
Series expansion of the conifold modulus#
Introduction#
The coniLCS_series limit expands the full coniLCS prepotential in powers of \(z_\text{cf}\) around \(z_\text{cf} = 0\), up to some truncation order nmax. The key step is expanding the polylogarithm contributions associated with the conifold curve: since the conifold curve has charge \(c = (1, 0, \ldots, 0)\) in the conifold basis, the corresponding instanton sum \(\text{Li}_3(e^{2\pi i z_\text{cf}})\) develops a series in \(z_\text{cf}\). This expansion is what generates the characteristic logarithmic behaviour
which is absent in the coniLCS and coniLCS_bulk limits. The remaining instanton contributions (from curves not proportional to the conifold curve) are regular at \(z_\text{cf} = 0\) and are expanded straightforwardly.
The result is a prepotential of the form
where the coefficients \(F^{(n)}_\text{LCS}(z^a)\) depend on the bulk moduli through polynomials and polylogarithms. The truncation order nmax (default 2) controls the accuracy: nmax=2 gives linear accuracy at the level of the superpotential \(W = \partial_i F\), which is sufficient for finding the exponentially small conifold VEV \(|z_\text{cf}| \sim e^{-2\pi K / (n_\text{cf} g_s M)}\).
Unlike the bulk approximation, \(z_\text{cf}\) is a dynamical variable here and is solved for alongside the bulk moduli and the axio-dilaton.
Example#
from cytools import Polytope
poly = Polytope([[-1,3,-2,-1],[1,-1,0,0],[-1,0,0,1],[-1,0,0,0],[-1,0,1,1],[-1,0,2,0],[-1,0,1,0]])
cy = poly.triangulate().get_cy()
basis_matrix = np.array([[0, 1, 1], [1, 1, 0], [0, 0, 1]])
conifold_curve = np.array([-1,1,0])
linearisedEFT = jvc.FluxVacuaFinder(h12=cy.h11(), Q=cy.h11()+cy.h12()+2,
use_cytools=True,
mirror_cy=cy,
ncf=2,
use_gvs=True,
maximum_degree=10,
basis_change=basis_matrix,
conifold_basis=True,
conifold_curve = conifold_curve,
prange=10,
limit="coniLCS_series")
The polynomial input data should match those of the full coni-LCS model. The series approximation keeps \(z_{\rm cf}\) dynamical, but replaces the conifold contribution by its truncated expansion around \(z_{\rm cf}=0\):
linearisedEFT.lcs_tree.a_matrix,linearisedEFT.lcs_tree.b_vector*24
The flux vector and PFV data are the same as in the full coni-LCS case. pfv_to_flux and pfv_to_moduli work identically in both limits since the PFV parametrisation only depends on the conifold structure:
M = np.array([4,-8,8])
K = np.array([-8,3,-6])
flux = linearisedEFT.pfv_to_flux(M,K)
flux
Define PFV data
gs = 0.38
tau0 = 1j/gs
z0 = linearisedEFT.pfv_to_moduli(M,K,tau0)
z0
We can find a root of the F-term conditions as follows
x0 = linearisedEFT._convert_complex_to_real(z0,jnp.conj(z0),tau0,jnp.conj(tau0))
linearisedEFT.DW_x(x0,flux)
res = root(linearisedEFT.DW_x,x0,args=(flux,),jac=linearisedEFT.dDW_x,tol=1e-10,method="hybr")
res
Let us check explicitly the F-terms
x1 = res.x
DW = linearisedEFT.DW_x(x1,flux)
print("DW: ",np.abs(DW))
Check that solution is inside the Kähler cone
z1,_,t1,_ = linearisedEFT._convert_real_to_complex(x1)
hypers = linearisedEFT.lcs_tree.hyperplanes
flag = np.all(hypers@z1.imag>0)&(t1.imag>0)
print("Inside facet of Kähler cone: ",flag)
The gauge-invariant superpotential is again computed with normalise=True. The series approximation should match the full coni-LCS model when \(|z_{\rm cf}|\) is small enough for the truncated conifold expansion to be accurate:
W0 = linearisedEFT.superpotential(z1,t1,flux,normalise=True)
print("W0: ",np.abs(W0))
We observe that this again matches the expectation of 2009.03312.
Custom periods and prepotentials#
This section is the most general entry point in the notebook: JAXVacua is not limited to the built-in Kreuzer-Skarke, CYTools, LCS, coni-LCS, or hypergeometric registries. If you can provide either a period vector \(\Pi(X)\) or a homogeneous prepotential \(F(X)\), the same JAXVacua machinery can evaluate \(K\), \(W\), \(D_I W\), Hessians, Newton solves, samplers and flux utilities.
This is useful in several common situations:
a local model or limiting prepotential is known analytically, but no catalogue entry exists;
a paper uses a period convention that is easier to reproduce directly than to translate into catalogue data;
one wants to compare two approximations to the same geometry, for example an LCS expansion and a local expansion near a K-point, C-point or conifold point;
periods have been computed by an external tool and should be passed to JAXVacua without adding a new model class.
The section proceeds in four stages:
Stage |
Purpose |
|---|---|
Minimal prepotential |
Show the smallest possible |
Minimal period vector |
Show the equivalent |
K-point \(X_{3,3}\) |
Demonstrate a non-LCS boundary prepotential from the literature. |
Vacuum search |
Show that custom periods can be used by |
Use prepotential_input when a homogeneous prepotential \(F(X)\) is the natural object. Use period_input when the period vector itself is known, or when no globally convenient prepotential exists in the chosen frame.
Calling conventions#
JAXVacua accepts two equivalent interfaces for custom geometries, via the constructor arguments prepotential_input and period_input.
Option A: Prepotential input — prepotential_input=F#
def F(X, conj=False):
...
return complex_scalar # JAX complex scalar
Argument |
Type |
Description |
|---|---|---|
|
|
Homogeneous coordinates \((X^0, X^1, \ldots, X^{h^{1,2}})\) |
|
|
When |
Return |
complex scalar |
The prepotential \(F(X)\), or its conjugate branch when |
conj flag guidance:
If your prepotential has only real-valued parameters, the same formula often works on conjugated inputs.
If it involves explicit complex constants, logarithm branches, or a distinguished boundary value such as a K-point rigid period \(\tau\), implement both branches explicitly. The K-point example below shows this.
From F, JAXVacua obtains the period vector by holomorphic autodiff,
Option B: Period vector input — period_input=Pi#
def Pi(X, conj=False):
...
return jnp.array([...]) # shape (2*(h12+1),)
Argument |
Type |
Description |
|---|---|---|
|
|
Homogeneous coordinates. |
|
|
Accept this keyword for compatibility with the special-geometry routines. For real coefficients it can usually be ignored, provided the function is evaluated on the already conjugated coordinates when |
Return |
|
Full period vector \(\Pi = (F_0,F_1,\ldots,X^0,X^1,\ldots)^T\). |
JAX compatibility#
Both F and Pi must be fully JAX-traceable: use jax.numpy rather than NumPy inside the callable, avoid Python branching on traced values, and keep static scalar model data in closures or explicit static arguments.
Symplectic normalisation#
The Kähler potential is
model.periods.sigma is the symplectic matrix. A valid patch of moduli space requires \(i\Pi^\dagger\Sigma\Pi>0\). The overall period normalisation shifts \(K\) by a constant but does not change the supersymmetric equations \(D_IW=0\).
Minimal prepotential input#
We first use the toy one-modulus cubic prepotential
prepotential_input pathway. Once the callable is supplied, FluxEFT can differentiate it with JAX and build the period vector, Kähler potential, superpotential and F-terms.
# The callable must accept homogeneous coordinates X and the optional conj flag.
def F(X, conj=False):
return X[1]**3 / X[0]
# Direct evaluation: useful as a quick smoke test before building FluxEFT.
F(jnp.ones(2) * 1j)
# No catalogue limit is passed here: the callable defines the geometry.
model = jvc.FluxEFT(h12=1, prepotential_input=F, limit=None)
model
# The affine wrapper model.F(z) evaluates the same prepotential at X=(1,z).
model.F(jnp.ones(1) * 1j)
# Because the model now has K, W and Pi, it can already evaluate F-terms.
flux = np.array([9, 10, -6, 7, -1, 1, 0, 6])
z0 = jnp.ones(1) * 1j
tau = 10 * 1j
model.DW(z0, jnp.conj(z0), tau, jnp.conj(tau), flux)
Minimal period-vector input#
The same toy geometry can be supplied directly as a period vector. This is the right interface when the periods come from a Picard-Fuchs computation, a local expansion, numerical analytic continuation, or an external code. The ordering used here is
# Direct period-vector input in the same symplectic ordering used by JAXVacua.
def per(X, conj=False):
F0 = -X[1]**3 / X[0]**2
F1 = 3 * X[1]**2 / X[0]
return jnp.array([F0, F1, X[0], X[1]])
# Again, the callable defines the geometry; no catalogue limit is needed.
model = jvc.FluxEFT(h12=1, limit=None, model_type=None, period_input=per)
model
# The F-terms agree with the prepotential-input model because Pi came from the same F.
flux = np.array([9, 10, -6, 7, -1, 1, 0, 6])
z0 = jnp.ones(1) * 1j
tau = 10 * 1j
model.DW(z0, jnp.conj(z0), tau, jnp.conj(tau), flux)
The agreement with the previous prepotential_input example is the point of this comparison. Users can choose the representation that matches their source material without changing the downstream JAXVacua workflow.
Symplectic pairing validation#
A custom period vector should be checked before it is used in a scan. The basic diagnostic is
periods.kahler_potential_per for the same period vector.
# Test at z = 1.5i (a point in the upper half-plane).
z_test = jnp.array([1.5j])
X_test = jnp.array([1.0 + 0.0j, z_test[0]])
Pi_eval = per(X_test)
sigma = model.periods.sigma
# The period-sector pairing equals exp(-K_cs).
pairing = 1j * (Pi_eval.conj() @ sigma @ Pi_eval)
K_periods = float(model.periods.kahler_potential_per(X_test, jnp.conj(X_test)).real)
K_from_pairing = -np.log(float(pairing.real))
print(f"i Π† Σ Π = {pairing:.6f}")
print(f" real part = {pairing.real:.6f} (> 0)")
print(f" imag part = {pairing.imag:.2e} (should be approximately 0)")
print()
print(f"-log(i Π† Σ Π) = {K_from_pairing:.6f}")
print(f"periods.kahler_potential = {K_periods:.6f}")
print(f"absolute difference = {abs(K_from_pairing - K_periods):.2e}")
print()
print("The full FluxEFT Kähler potential also includes the axio-dilaton term, so it requires tau and tau_c.")
Boundary prepotentials beyond LCS#
The custom interface is most useful when the relevant physics is not captured by a standard LCS expansion. A good example is a one-modulus Calabi-Yau near a non-LCS boundary point, where the period expansion contains logarithms, special constants and model-dependent boundary data.
The next cells reproduce the local K-point and C-point prepotential templates from Bastian, van de Heisteeg and Schlechter, Beyond Large Complex Structure: Quantized Periods and Boundary Data for One-Modulus Singularities, arXiv:2306.01059. NB11 wraps the same data in the higher-level HypergeometricModels registry; here we spell out the callable by hand to show exactly what prepotential_input can accept.
The helper functions below are direct JAX-callable versions of the local K-point and C-point formulae. They are intentionally written as ordinary functions: any user-derived prepotential with the same signature can be passed to FluxEFT.
# K-point and C-point local prepotential templates from arXiv:2306.01059.
# Static arguments are model constants; X remains the traced JAX input.
@partial(jit,static_argnums=(1,2,3,4,5,6,7,8,))
def prepot_Kpoint(X,delta=0,tau=0,gamma=0,c=0,B1=0,B2=0,B3=0,conj=False):
tau2 = tau.imag
if conj:
tau = jnp.conj(tau)
s = (X[1]-tau)/X[0]
F0 = delta/2+gamma*tau
if conj:
F1 = gamma-c*tau2/2/jnp.pi*(jnp.log(-1j*s/2/B1/tau2)-1)
F2 = -1j*c/4/jnp.pi**2*(jnp.log(-1j*s/2/B1/tau2)-2+B2/4/B1**2)
else:
F1 = gamma-c*tau2/2/jnp.pi*(jnp.log(1j*s/2/B1/tau2)-1)
F2 = 1j*c/4/jnp.pi**2*(jnp.log(1j*s/2/B1/tau2)-2+B2/4/B1**2)
F3 = c/16/jnp.pi/tau2*(1-B2**2/B1**4+2*B3/(3*B1**3))
return F0+s*F1+(s**2)*F2+(s**3)*F3
@partial(jit,static_argnums=(1,2,3,4,5,6,7,))
def prepot_Cpoint(X,delta=0,tau=0,gamma=0,k=0,A1=1,A2=1,conj=False):
tau2 = tau.imag
if conj:
tau = jnp.conj(tau)
s = (X[1]-tau)/X[0]
F0 = tau/2
F1 = delta-gamma*tau
if conj:
F2 = -1j*k*(3-2*jnp.log(s)+2*jnp.log(A1)) -gamma*F1
F3 = 1j*k*(3*A1**2*gamma-A2)/12/jnp.pi/A1**2
else:
F2 = 1j*k*(3-2*jnp.log(s)+2*jnp.log(A1)) -gamma*F1
F3 = -1j*k*(3*A1**2*gamma-A2)/12/jnp.pi/A1**2
return F0+s*F1+(s**2)*F2+(s**3)*F3
K-point example: \(X_{3,3}\)#
A K-point is a boundary point in a one-modulus complex-structure moduli space where the monodromy has a finite semisimple part together with a unipotent block. In the hypergeometric classification this occurs when the Picard-Fuchs exponents at \(z=\infty\) appear in two equal pairs,
The model \(X_{3,3}\) is the complete intersection of two cubic hypersurfaces in \(\mathbb{P}^5\). Its Picard-Fuchs exponents are \((1/3,1/3,2/3,2/3)\), so the point at \(z=\infty\) is of K-type. The rigid period is
HypergeometricModels.build("X33", limit="Kpoint").
# Boundary data for X_{3,3} at the K-point.
tau = -1/2 + 1j * np.sqrt(3) / 2
gamma = 1/6
delta = 1/3
B1 = 1
B2 = 0
B3 = 0
c = 2
# arXiv:2306.01059 fixes the instanton normalisation through L(f,1).
import scipy
LF1 = scipy.special.gamma(1/3)**6 / (8 * np.sqrt(3) * np.pi**3)
coeff_norm = -1 / (3 * LF1**2)
# Wrap the boundary prepotential into the exact signature expected by FluxEFT.
@partial(jit, static_argnums=(1,))
def F(X, conj=False):
return prepot_Kpoint(
X,
delta=delta,
tau=tau,
gamma=gamma,
c=c,
B1=coeff_norm * B1,
B2=coeff_norm * B2,
B3=coeff_norm * B3,
conj=conj,
)
# Evaluate close to the K-point in the ordinary branch.
F(jnp.ones(2) * 1j)
# Check that the conjugate branch is available for Kähler-potential routines.
F(-jnp.ones(2) * 1j, conj=True)
The two evaluations above are not physical checks by themselves; they verify that both branches of the callable are defined and JAX-traceable. We now hand this K-point prepotential to FluxEFT. From this point onwards, the model exposes the same methods as a built-in geometry.
# Build a FluxEFT directly from the K-point prepotential.
model = jvc.FluxEFT(h12=1, limit=None, model_type=None, prepotential_input=F)
model
# Affine evaluation at X=(1,z).
model.F(jnp.ones(1) * 1j)
# The K-point model can immediately evaluate F-terms for arbitrary fluxes.
flux = np.array([9, 10, -6, 7, -1, 1, 0, 6])
z0 = jnp.ones(1) * 1j
tau = 10 * 1j
model.DW(z0, jnp.conj(z0), tau, jnp.conj(tau), flux)
Returning to the cubic toy model#
The K-point example demonstrates that genuinely non-LCS boundary data can be supplied by hand. The next two cells return to the simple cubic model to make the equivalence between period_input and prepotential_input completely explicit in a compact setting.
# Period-vector form of the cubic toy model.
def Pi(X, conj=False):
F_0 = -X[1]**3 / X[0]
F_1 = 3 * X[1]**2
return jnp.array([F_0, F_1, X[0], X[1]])
# For user-supplied periods we do not pass a catalogue limit label.
model = jvc.FluxEFT(h12=1, period_input=Pi)
model
# Prepotential form of the same cubic toy model.
def F(X, conj=False):
return -X[1]**3 / X[0]
# For user-supplied prepotentials we likewise omit a catalogue limit label.
model = jvc.FluxEFT(h12=1, prepotential_input=F)
model
Vacuum search with custom periods#
Once a FluxEFT model is built from prepotential_input or period_input, the same idea can be used in FluxVacuaFinder. The only extra ingredient needed by the sampler is minimal cone metadata: here the one-modulus physical region is simply \(\operatorname{Im} z>0\), encoded as a one-ray/one-hyperplane toy cone.
The following cells are a smoke test rather than a production scan. They show that a hand-written period vector can enter the FluxVacuaFinder pipeline, including sampling, linearised flux shifts and F-term checks. For a real ensemble one would tune the flux bounds, increase N, vmap_dim, max_batches, and use geometry-specific constraints.
from jax import vmap
from jaxvacua.util import vmapping_func
# --- define the cubic period vector ---
def Pi_cubic(X, conj=False):
F_0 = -X[1]**3 / X[0]
F_1 = 3 * X[1]**2
return jnp.array([F_0, F_1, X[0], X[1]])
# --- build model (FluxVacuaFinder inherits FluxEFT and adds the sampler) ---
# The toy one-modulus cone is the half-line Im(z)>0. We label the
# metadata tree as LCS-like, while the Pi_cubic callable supplies the
# actual periods used below.
custom_tree = jvc.lcs_tree(
intnums=jnp.array([[[0.0]]]),
c2=jnp.array([0.0]),
hyperplanes=jnp.array([[1.0]]),
kahler_rays=jnp.array([[1.0]]),
limit="LCS",
)
model_vf = jvc.FluxVacuaFinder(
h12=1,
period_input=Pi_cubic,
lcs_tree_input=custom_tree,
limit=None,
Q=40,
)
sampler = jvc.data_sampler(model_vf,
flux_bounds=[-4, 4],
moduli_bounds=[1, 5],
axion_bounds=[-0.5, 0.5],
dilaton_bounds=[2, 10])
print(model_vf)
print(f"D3-tadpole bound Q = {model_vf.D3_tadpole}")
# --- define vmapped objective and optimiser ---
DW_v = vmap(model_vf.DW)
kwargs = {"Q": model_vf.D3_tadpole, "return_flag": True,
"constraints": None, "remove_NANs": True, "in_axes": (0, 0, 0)}
optimiser = vmapping_func(model_vf.linearised_shifts, mode="Fflux", **kwargs)
# --- run a small sampling smoke test ---
# For a production scan, increase N, vmap_dim, max_batches and
# max_resample_attempts after tuning the sampler and flux bounds.
seed = 42
rns_key = jvc.PRNGSequence(seed)
moduli, tau, fluxes, residuals = model_vf.sample_SUSY_flux_vacua(
N=1,
rns_key=rns_key,
sampler=sampler,
optimiser=optimiser,
objective_fct=DW_v,
vmap_dim=20,
moduli_sampling_mode="box",
max_batches=1,
max_resample_attempts=5,
errors="warn",
print_progress=False,
)
print(f"\nFound {len(moduli)} SUSY vacua")
# --- inspect and validate vacua ---
if len(moduli) > 0:
W0v = vmap(model_vf.superpotential_gauge_invariant)
tadpole_v = vmap(model_vf.tadpole)
DW_check = vmap(model_vf.DW)
W0 = W0v(moduli, tau, fluxes)
Nflux = tadpole_v(fluxes)
DW_res = jnp.sum(jnp.abs(DW_check(moduli, jnp.conj(moduli), tau, jnp.conj(tau), fluxes)), axis=1)
print(f"{'z':>25} {'τ':>22} {'|W₀|':>10} {'gₛ':>7} {'N_flux':>7} {'|DW|':>10}")
print("-" * 90)
for i in range(min(8, len(moduli))):
z_i = moduli[i][0]
t_i = tau[i]
print(f" {z_i.real:+.4f}{z_i.imag:+.4f}j "
f"{t_i.real:+.4f}{t_i.imag:+.4f}j "
f"{abs(W0[i]):.4e} "
f"{(1/t_i.imag):.4f} "
f"{int(Nflux[i]):>7} "
f"{float(DW_res[i]):.2e}")
if len(moduli) > 8:
print(f" ... ({len(moduli) - 8} more)")
else:
print("No vacua found in this bounded smoke test. For a real scan, increase N/vmap_dim/max_batches or widen sampler bounds.")
Take-aways#
LCS is the canonical Type IIB limit. The prepotential carries a cubic polynomial in moduli plus exponentially-suppressed worldsheet instantons (GV invariants); the cubic dominates at large \(\operatorname{Im}(z)\gg0\).
coni-LCS is reached by sending one Mori-cone coordinate to the conifold wall while keeping the rest at LCS. The prepotential acquires a \(z_{\rm cf}^2\log z_{\rm cf}\) singularity; the conifold modulus \(z_{\rm cf}\) is treated as a special coordinate via
basis_change,ncf, andconifold_curve.Bulk approximation integrates out the conifold modulus analytically, leaving an effective prepotential in the remaining bulk moduli. Activated by
limit="coniLCS_bulk".Series expansion keeps the conifold modulus dynamical and expands the conifold contribution around \(z_{\rm cf}=0\). Activated by
limit="coniLCS_series"withmaximum_degreecontrolling the retained terms.Custom periods / prepotentials are first-class inputs. Pass either
prepotential_input=Fwhen \(F(X)\) is known analytically, orperiod_input=Piwhen the periods come from another calculation. After construction, the model exposes the sameF,DW,DW_x, Hessian and sampling interfaces as built-in geometries.K-point and C-point boundary expansions are examples of non-LCS data that fit naturally into this interface. The \(X_{3,3}\) K-point example uses the local expansion and quantised boundary data of arXiv:2306.01059.
Validation matters. Check JAX traceability, the conjugate branch when complex constants are present, and the symplectic pairing \(i\Pi^\dagger\Sigma\Pi>0\) before running a scan.
Further reading#
NB10 — coniLCS pipeline (next-step deep dive)
NB11 — Hypergeometric models (registry-based K-point and C-point models)
Bastian, van de Heisteeg and Schlechter, Beyond Large Complex Structure: Quantized Periods and Boundary Data for One-Modulus Singularities, arXiv:2306.01059
JAXVacua framework paper, arXiv:2306.06160
arXiv:2404.12422 (Hulek-Verrill), arXiv:1903.00596 (PFV mechanism)