Moduli-space limits and custom prepotentials#

What’s in this notebook? This notebook is the overview of moduli-space limits in JAXVacua. It explains the LCS limit, introduces what changes near conifold/LCS boundaries, and then shows how custom prepotential_input and period_input let users supply boundary descriptions that are not built into the catalogue.

In this notebook, you will learn:

  • How the LCS prepotential is used in the standard model workflow.

  • What data enters a coni-LCS limit: a conifold curve, a basis change, and the number of shrinking cycles.

  • How to pass an analytic prepotential or period vector directly to JAXVacua.

  • How the custom-input mechanism relates to the built-in one-modulus hypergeometric models in NB11.

Non-goals: the detailed conifold approximation schemes, PFV examples, z_cf reconstruction, and freezer interface are treated in NB10 — coni-LCS limit and PFV pipeline and NB12 — Freezer.

Prerequisites: NB02 — JAXVacua overview and NB03 — CYTools interface for CYTools-derived examples.

Outline#

  1. Setup

  2. LCS limits

  3. coni-LCS limits

  4. Custom periods and prepotentials

  5. Take-aways

  6. Further reading

Setup#

import warnings, time, math
from functools import partial
import numpy as np
from scipy.optimize import root

import jax
from jax import jit
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 overview — a CYTools-built polytope with conifold curve $C_{ m cf} = [-1,1,0]`. Here we identify the conifold data and explain what JAXVacua needs; the detailed approximation schemes are deferred to NB10.

  • Custom inputs — a minimal analytic prepotential and period-vector examples, plus a K-point boundary example that connects this interface to the built-in hypergeometric models in NB11.

The shared setup imports jaxvacua as jvc; each downstream section builds its own model because different limits require different input data.

LCS limits#

Introduction#

At Large Complex Structure (LCS), the prepotential can be expressed as

(1)#\[ F_{\mathrm{LCS}}(X) = F_{\mathrm{poly}}(X) + F_{\mathrm{inst}}(X)\, .\]

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

(2)#\[ F_{\mathrm{poly}}(X) = -\frac{1}{6X^0}\widetilde{\kappa}_{ijk}X^iX^jX^k+\frac{1}{2}a_{ij}X^iX^j +b_{i}X^i\, X^0 + \dfrac{\text{i}}{2}\tilde{\xi}\, (X^0)^2\, ,\]

Here, \(\widetilde{\kappa}_{ijk}\) are the triple intersection numbers of the mirror dual Calabi-Yau threefold \(\widetilde{X}\). Here, we defined

(3)#\[\begin{split} a_{ij} = \dfrac{1}{2}\begin{cases} \widetilde{\kappa}_{iij} & i\geq j\\ \widetilde{\kappa}_{ijj} & i<j \end{cases} \, , \quad b_i = \dfrac{1}{24} \int_{\tilde{D}^i}\, c_2(\widetilde{X})\, , \quad \tilde{\xi}=\frac{\zeta(3)\, \chi(\widetilde{X})}{(2\pi)^3}\, .\end{split}\]

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

(4)#\[ F_{\mathrm{inst}}(X) = -\frac{(X^0)^2}{(2\pi\mathrm{i})^3}\, \sum_{q\in\mathcal{M}(\widetilde{X})}\, n_q^{0}\, \text{Li}_3\left (\text{e}^{2\pi \text{i} q_i X^i / X^0}\right )\; , \quad \text{Li}_3\left (x\right )=\sum_{m=1}^{\infty}\, \dfrac{x^{m}}{m^{3}}\, .\]

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

(5)#\[ \sum_{q\in\mathcal{M}(\widetilde{X})}\, n_q^{0}\, \text{Li}_3\left (\text{e}^{2\pi \text{i} q_i X^i / X^0}\right ) = \sum_{q\in\mathcal{M}(\widetilde{X})}\, N_q\, \text{e}^{2\pi \text{i} q_i X^i / X^0}\]

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 and h12 value (here the default model_type="KS" refers to models in the Kreuzer–Skarke database)

  • limit="LCS": activates the LCS prepotential (implemented in the periods class), 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=1 keeps 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.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); use mode="non-SUSY" to minimise \(|\partial_i V|^2\) instead

  • solver_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_invariant computes \(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

(6)#\[ z_{\text{cf}}= z^a \tilde{\mathbf{q}}^{\mathrm{cf}}_a\, ,\]

which we will refer to as the conifold modulus. Denoting by \({\omega^a}_\alpha\) the generators of the lattice orthogonal to \(\tilde{\mathbf{q}}^{\mathrm{cf}}\), it will be useful to parameterise moduli space according to

(7)#\[z^a={\omega^a}_{\alpha} z^\alpha+ \xi^a z_{\text{cf}}\, ,\]

with \(a \in 1,\ldots,h^{2,1}\) and \(\alpha \in 2,\ldots,h^{2,1}\), in terms of the conifold modulus \(z_{\mathrm{cf}}\) and the bulk moduli \(z^\alpha\). Here, \(\xi^a\) is an arbitrary constant vector satisfying \(\tilde{\mathbf{q}}^{\mathrm{cf}}_a\xi^a=1\).

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)

(8)#\[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\]

where the higher order terms are obtained from

(9)#\[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}\]

in terms of the polynomial prepotential

(10)#\[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}\, .\]

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

From overview to detailed coni-LCS pipelines#

At this point we have identified the data that distinguishes a coni-LCS model from an ordinary LCS model:

  • a conifold curve class conifold_curve,

  • an integer basis_change that puts the conifold modulus in the first coordinate,

  • the multiplicity ncf, and

  • a conifold-aware period limit such as limit="coniLCS".

The detailed approximation schemes are intentionally not expanded here. In practice there are several related workflows: the full coni-LCS prepotential, the bulk approximation in which \(z_{\rm cf}\) is integrated out, the coniLCS_series expansion, PFV initial data, and the ConifoldFreezer reduced-EFT interface. Those details make a coherent pipeline notebook of their own, so they live in NB10 — coni-LCS limit and PFV pipeline.

For the purposes of this overview, the important point is that all of these descriptions are period models: once the period/prepotential data has been supplied, the same FluxEFT and FluxVacuaFinder APIs compute \(K\), \(W\), \(D_IW\), Hessians and Newton refinements.

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 prepotential_input=F example.

Minimal period vector

Show the equivalent period_input=Pi interface and validate the symplectic pairing.

K-point \(X_{3,3}\)

Demonstrate a non-LCS boundary prepotential from the literature.

Vacuum search

Show that custom periods can be used by FluxVacuaFinder just like built-in models.

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

X

jnp.ndarray, shape (h12+1,)

Homogeneous coordinates \((X^0, X^1, \ldots, X^{h^{1,2}})\)

conj

bool

When True, return \(\bar{F}(\bar{X})\) in the same convention as the non-conjugated branch. This is needed when computing the Kähler potential and metric.

Return

complex scalar

The prepotential \(F(X)\), or its conjugate branch when conj=True.

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,

(11)#\[\Pi = (\partial_0 F,\partial_1 F,\ldots,X^0,X^1,\ldots)^T.\]

Option B: Period vector input — period_input=Pi#

def Pi(X, conj=False):
    ...
    return jnp.array([...])   # shape (2*(h12+1),)

Argument

Type

Description

X

jnp.ndarray, shape (h12+1,)

Homogeneous coordinates.

conj

bool

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 conj=True.

Return

jnp.ndarray, shape (2*(h12+1),)

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

(12)#\[K_{\rm cs}=-\log\!\left(i\,\Pi^\dagger\Sigma\Pi\right),\]

where 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

(13)#\[F(X)=\frac{(X^1)^3}{X^0}.\]

This is not meant as a realistic global model; it is the smallest example that exercises the 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 consistency check 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

(14)#\[\Pi=(F_0,F_1,X^0,X^1)^T.\]

For the cubic prepotential above, \(F_0=- (X^1)^3/(X^0)^2\) and \(F_1=3(X^1)^2/X^0\).

# 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

(15)#\[i\,\Pi^\dagger\Sigma\Pi=e^{-K_{\rm cs}}.\]

It should be real and positive in the physical patch. The following cell compares this direct symplectic pairing against 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,

(16)#\[\alpha_1=\alpha_2,\qquad \alpha_3=\alpha_4,\qquad \alpha_2\neq\alpha_3.\]

The natural local coordinate is

(17)#\[s=\frac{X^1-\tau X^0}{X^0},\]

where the rigid period \(\tau\in\mathbb{H}\) fixes the boundary value of \(X^1/X^0\).

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

(18)#\[\tau=-\frac12+\frac{\sqrt3}{2}i=e^{2\pi i/3},\]

and the instanton normalisation is fixed by the special value

(19)#\[L(f,1)=\frac{\Gamma(1/3)^6}{8\sqrt3\,\pi^3}\]

of the associated weight-3 modular form. These conventions follow arXiv:2306.01059; see NB11 for the registry-based version via 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 compact consistency check 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 stringjax_tools 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.Q()}")
# --- define objective and optimiser ---
objective_fct = model_vf.DW
kwargs = {"Q": model_vf.Q(), "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 consistency check ---
# 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=objective_fct,
    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:
    W0     = model_vf.superpotential_gauge_invariant(moduli, tau, fluxes)
    Nflux  = model_vf.tadpole(fluxes)
    DW_res = jnp.sum(jnp.abs(model_vf.DW(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 consistency check. 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 contains a cubic polynomial plus exponentially suppressed worldsheet instantons; the cubic dominates deep in the Kähler cone.

  • coni-LCS limits need extra geometric data. JAXVacua must know which curve shrinks, which basis isolates the conifold modulus, and the number of shrinking cycles ncf.

  • Detailed conifold approximations live in NB10. The full coni-LCS, bulk, series, PFV and freezer workflows are closely related and are easier to understand as one pipeline.

  • Custom periods and prepotentials are first-class inputs. Pass prepotential_input=F when \(F(X)\) is known analytically, or period_input=Pi when periods come from another calculation.

  • NB11 is the built-in one-modulus counterpart. The hypergeometric registry provides curated LCS, K-point and C-point models that use the same boundary-period philosophy without requiring users to write the functions by hand.

  • Validation matters. Check JAX traceability, conjugate branches and the symplectic pairing before running scans with custom period data.

Further reading#