Visualisation Cookbook#

What’s in this notebook? Publication-ready plotting recipes for flux vacuum data produced by JAXVacua. Each recipe is self-contained.

In this notebook, you will learn:

  • How to set up a consistent matplotlib style for publication figures

  • Scatter plots of moduli, axio-dilaton, and \(W_0\) distributions

  • Log-scale histograms and KDE density plots of \(|W_0|\)

  • Correlation plots (\(|W_0|\) vs \(g_s\), \(|W_0|\) vs \(N_{\mathrm{flux}}\))

  • Moduli-space heatmaps and composite multi-panel figures

Prerequisites: NB05, NB06.

(Created: March 2026)

Outline#

  1. Setup and publication style

  2. Generate sample data

  3. Recipe 1: Moduli space overview (2×2 scatter)

  4. Recipe 2: \(|W_0|\) distribution

  5. Recipe 3: \(W_0\) density in the complex plane

  6. Recipe 4: \(|W_0|\) vs \(g_s\) and \(N_\mathrm{flux}\)

  7. Recipe 5: F-term residual convergence

  8. Recipe 6: Tadpole distribution

  9. Recipe 7: Moduli-space density heatmap

  10. Recipe 8: Multi-panel composite with labels

  11. Utility: Export for journals

  12. Take-aways

  13. Further reading

Setup and publication style#

import numpy as np
import warnings
warnings.filterwarnings('ignore')

import jax
import jax.numpy as jnp
jax.config.update("jax_enable_x64", True)
from jax import vmap

import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.gridspec import GridSpec
from matplotlib.colors import Normalize
from scipy.stats import gaussian_kde

import jaxvacua as jvc
# ── Publication style ──────────────────────────────────────
STYLE = {
    "figure.dpi": 200,
    "figure.figsize": (3.4, 2.8),
    "figure.facecolor": "white",
    "savefig.dpi": 300,
    "savefig.bbox": "tight",
    "savefig.pad_inches": 0.03,
    "font.family": "serif",
    "font.serif": ["Computer Modern Roman", "Times New Roman", "DejaVu Serif"],
    "font.size": 10,
    "axes.titlesize": 11,
    "axes.labelsize": 11,
    "xtick.labelsize": 9,
    "ytick.labelsize": 9,
    "legend.fontsize": 9,
    "axes.linewidth": 0.6,
    "axes.grid": False,
    "xtick.major.width": 0.5,
    "ytick.major.width": 0.5,
    "xtick.major.size": 3.5,
    "ytick.major.size": 3.5,
    "xtick.minor.size": 2,
    "ytick.minor.size": 2,
    "xtick.direction": "in",
    "ytick.direction": "in",
    "lines.linewidth": 1.2,
    "lines.markersize": 3,
    "legend.frameon": False,
}
mpl.rcParams.update(STYLE)

# Colours
C = {"blue": "#2563eb", "orange": "#f97316", "purple": "#7c3aed",
     "red": "#dc2626", "green": "#16a34a", "grey": "#64748b", "cyan": "#06b6d4"}

SINGLE = (3.4, 2.8)
DOUBLE = (7.0, 2.8)
TALL = (7.0, 5.2)
CMAP = "viridis"

print("Style loaded.")

Generate sample data#

model = jvc.FluxVacuaFinder(h12=2, model_ID=1, maximum_degree=2, limit="LCS", model_type="KS")
sampler = jvc.data_sampler(model, flux_bounds=[-10, 10], moduli_bounds=[1, 5], dilaton_bounds=[1, 5])
rns_key = jvc.PRNGSequence(42)

moduli, tau, fluxes, residuals = model.sample_SUSY_flux_vacua(
    N=500, sampler=sampler, rns_key=rns_key,
    max_iters=100, tol=1e-10, max_tadpole=model.D3_tadpole,
    mode="ISD", vmap_dim=10**4, print_progress=True,
)
print(f"\nFound {len(moduli)} vacua")
# Derived quantities
W0 = np.array(vmap(model.superpotential_gauge_invariant)(moduli, tau, fluxes))
W0_abs = np.abs(W0)
Nflux = np.array(vmap(model.tadpole)(fluxes))
g_s = np.array(1.0 / tau.imag)
moduli_np = np.array(moduli)
tau_np = np.array(tau)

print(f"|W0|: [{W0_abs.min():.3f}, {W0_abs.max():.1f}]")
print(f"g_s:  [{g_s.min():.3f}, {g_s.max():.3f}]")
print(f"N_flux: [{int(Nflux.min())}, {int(Nflux.max())}]")

Recipe 1: Moduli space overview (2×2 scatter)#

Distribution of complex structure moduli, axio-dilaton, and \(W_0\) coloured by \(N_{\mathrm{flux}}\).

fig = plt.figure(figsize=(7.2, 5.0), constrained_layout=False)
gs = GridSpec(2, 3, figure=fig, width_ratios=[1, 1, 0.045], wspace=0.08, hspace=0.08)
axes = np.array([
    [fig.add_subplot(gs[0, 0]), fig.add_subplot(gs[0, 1])],
    [fig.add_subplot(gs[1, 0]), fig.add_subplot(gs[1, 1])],
])
cax = fig.add_subplot(gs[:, 2])

norm = Normalize(vmin=Nflux.min(), vmax=Nflux.max())
kw = dict(s=8, alpha=0.65, edgecolors="none", rasterized=True)

axes[0, 0].scatter(moduli_np[:, 0].imag, moduli_np[:, 1].imag, c=Nflux, cmap=CMAP, norm=norm, **kw)
axes[0, 0].set_xlabel(r"Im$(z_1)$")
axes[0, 0].set_ylabel(r"Im$(z_2)$")

axes[0, 1].scatter(moduli_np[:, 0].real, moduli_np[:, 1].real, c=Nflux, cmap=CMAP, norm=norm, **kw)
axes[0, 1].set_xlabel(r"Re$(z_1)$")
axes[0, 1].set_ylabel(r"Re$(z_2)$")

axes[1, 0].scatter(tau_np.real, tau_np.imag, c=Nflux, cmap=CMAP, norm=norm, **kw)
axes[1, 0].set_xlabel(r"Re$(\tau)$")
axes[1, 0].set_ylabel(r"Im$(\tau)$")

sc = axes[1, 1].scatter(W0.real, W0.imag, c=Nflux, cmap=CMAP, norm=norm, **kw)
axes[1, 1].set_xlabel(r"Re$(W_0)$")
axes[1, 1].set_ylabel(r"Im$(W_0)$")

cbar = fig.colorbar(sc, cax=cax)
cbar.set_label(r"$N_{\mathrm{flux}}$", fontsize=11)
cbar.ax.tick_params(labelsize=9)

fig.subplots_adjust(left=0.10, right=0.92, bottom=0.09, top=0.96)
plt.show()

Recipe 2: \(|W_0|\) distribution (histogram + KDE)#

fig, ax = plt.subplots(figsize=SINGLE,dpi=250)

logW = np.log10(W0_abs[W0_abs > 0])

ax.hist(logW, bins=35, density=True, color=C["blue"], alpha=0.45,
        edgecolor="white", linewidth=0.4, label="Data", zorder=2)

kde = gaussian_kde(logW, bw_method=0.3)
xk = np.linspace(logW.min() - 0.3, logW.max() + 0.3, 200)
ax.plot(xk, kde(xk), color=C["purple"], lw=1.8, label="KDE", zorder=3)

ax.set_xlabel(r"$\log_{10}|W_0|$")
ax.set_ylabel("Probability density")
ax.legend(loc="upper left")

fig.tight_layout()
plt.show()

Recipe 3: \(W_0\) density in the complex plane#

fig, ax = plt.subplots(figsize=(3.4, 3.4),dpi=250)

hb = ax.hexbin(W0.real, W0.imag, gridsize=25, cmap=CMAP, mincnt=1, linewidths=0.2)
ax.set_xlabel(r"Re$(W_0)$")
ax.set_ylabel(r"Im$(W_0)$")
ax.set_aspect("equal")

cbar = fig.colorbar(hb, ax=ax, shrink=0.85, pad=0.03)
cbar.set_label("Count", fontsize=10)
cbar.ax.tick_params(labelsize=9)

fig.tight_layout()
plt.show()

Recipe 4: \(|W_0|\) vs \(g_s\) and \(N_{\mathrm{flux}}\) (log-log)#

fig, axes = plt.subplots(1, 2, figsize=DOUBLE)

kw = dict(s=10, alpha=0.55, edgecolors="none", rasterized=True)

axes[0].scatter(W0_abs, g_s, c=C["blue"], **kw)
axes[0].set_xscale("log")
axes[0].set_yscale("log")
axes[0].set_xlabel(r"$|W_0|$")
axes[0].set_ylabel(r"$g_s$")

axes[1].scatter(W0_abs, Nflux, c=C["orange"], **kw)
axes[1].set_xscale("log")
axes[1].set_xlabel(r"$|W_0|$")
axes[1].set_ylabel(r"$N_{\mathrm{flux}}$")

fig.tight_layout()
plt.show()

Recipe 5: F-term residual convergence#

fig, ax = plt.subplots(figsize=SINGLE)

log_res = np.log10(np.array(residuals))
log_res = log_res[np.isfinite(log_res)]

ax.hist(log_res, bins=35, color=C["green"], alpha=0.65,
        edgecolor="white", linewidth=0.4)
ax.axvline(x=-10, color=C["red"], ls="--", lw=1.0, zorder=1,
           label=r"tol $= 10^{-10}$")

ax.set_xlabel(r"$\log_{10}\,\mathrm{residual}$")
ax.set_ylabel("Count")
ax.legend(loc="upper right", frameon=True, framealpha=0.9,
          facecolor="white", edgecolor="none")

fig.tight_layout()
plt.show()

Recipe 6: Tadpole distribution#

fig, ax = plt.subplots(figsize=SINGLE)

Nf_int = Nflux.astype(int)
span = int(Nf_int.max() - Nf_int.min() + 1)
bin_width = max(1, int(np.ceil(span / 24)))
lo = bin_width * np.floor(Nf_int.min() / bin_width) - 0.5
hi = bin_width * np.ceil(Nf_int.max() / bin_width) + bin_width - 0.5
bins = np.arange(lo, hi + bin_width, bin_width)
counts, edges = np.histogram(Nf_int, bins=bins)
centres = 0.5 * (edges[:-1] + edges[1:])
mask = counts > 0

ax.bar(centres[mask], counts[mask], width=0.85 * bin_width, color=C["cyan"],
       edgecolor="white", linewidth=0.3, alpha=0.8)
ax.set_xlabel(r"$N_{\mathrm{flux}}$")
ax.set_ylabel("Count")

fig.tight_layout()
plt.show()

Recipe 7: Moduli-space density heatmap#

fig, ax = plt.subplots(figsize=(3.6, 3.4))

x = moduli_np[:, 0].imag
y = moduli_np[:, 1].imag

x_upper = min(5.0, float(np.nanpercentile(x, 99)))
y_upper = min(5.0, float(np.nanpercentile(y, 99)))
lcs_floor = 1.0
mask = (x >= lcs_floor) & (y >= lcs_floor) & (x <= x_upper) & (y <= y_upper)
if mask.sum() < 10:
    mask = np.ones_like(x, dtype=bool)
    x_upper = float(np.nanpercentile(x, 99))
    y_upper = float(np.nanpercentile(y, 99))

x_plot = x[mask]
y_plot = y[mask]
xy = np.vstack([x_plot, y_plot])
kernel = gaussian_kde(xy, bw_method=0.3)
xi = np.linspace(max(lcs_floor, float(x_plot.min())), x_upper, 100)
yi = np.linspace(max(lcs_floor, float(y_plot.min())), y_upper, 100)
Xi, Yi = np.meshgrid(xi, yi)
Zi = kernel(np.vstack([Xi.ravel(), Yi.ravel()])).reshape(Xi.shape)

im = ax.pcolormesh(Xi, Yi, Zi, cmap=CMAP, shading="gouraud", rasterized=True)
ax.scatter(x_plot, y_plot, s=3, c="white", alpha=0.25, edgecolors="none")
ax.set_xlim(xi.min(), xi.max())
ax.set_ylim(yi.min(), yi.max())

ax.set_xlabel(r"Im$(z_1)$")
ax.set_xlim(max(lcs_floor, float(x_plot.min())), x_upper)
ax.set_ylim(max(lcs_floor, float(y_plot.min())), y_upper)
ax.set_ylabel(r"Im$(z_2)$")

cbar = fig.colorbar(im, ax=ax, shrink=0.85, pad=0.03)
cbar.set_label("Density", fontsize=10)
cbar.ax.tick_params(labelsize=9)

fig.tight_layout()
plt.show()

Recipe 8: Multi-panel composite with labels#

fig = plt.figure(figsize=(6.8, 3.1), constrained_layout=False)
gs = GridSpec(1, 3, figure=fig, wspace=0.18)

kw = dict(s=8, alpha=0.55, edgecolors="none", rasterized=True)
norm = Normalize(vmin=Nflux.min(), vmax=Nflux.max())

# (a) Focus on the populated LCS region instead of letting far outliers set the axes.
ax_a = fig.add_subplot(gs[0])
x = moduli_np[:, 0].imag
y = moduli_np[:, 1].imag
x_upper = min(5.0, float(np.nanpercentile(x, 99)))
y_upper = min(5.0, float(np.nanpercentile(y, 99)))
lcs_floor = 1.0
mask_mod = (x >= lcs_floor) & (y >= lcs_floor) & (x <= x_upper) & (y <= y_upper)
if mask_mod.sum() < 10:
    mask_mod = np.ones_like(x, dtype=bool)
ax_a.scatter(x[mask_mod], y[mask_mod], c=Nflux[mask_mod], cmap=CMAP, norm=norm, **kw)
ax_a.set_xlim(lcs_floor, x_upper)
ax_a.set_ylim(lcs_floor, y_upper)
ax_a.set_xlabel(r"Im$(z_1)$")
ax_a.set_ylabel(r"Im$(z_2)$")
ax_a.text(0.05, 0.9, r"\textbf{(a)}" if mpl.rcParams.get("text.usetex") else "(a)",
          transform=ax_a.transAxes, fontsize=12, fontweight="bold")

# (b)
ax_b = fig.add_subplot(gs[1])
logW_c = np.log10(np.abs(W0)[np.abs(W0) > 0])
ax_b.hist(logW_c, bins=25, density=True, color=C["blue"], alpha=0.55,
          edgecolor="white", linewidth=0.3)
ax_b.set_xlabel(r"$\log_{10}|W_0|$")
ax_b.set_ylabel("Density")
ax_b.text(0.05, 0.9, "(b)", transform=ax_b.transAxes, fontsize=12, fontweight="bold")

# (c)
ax_c = fig.add_subplot(gs[2])
ax_c.scatter(np.abs(W0), g_s, c=C["purple"], **kw)
ax_c.set_xscale("log")
ax_c.set_yscale("log")
ax_c.set_xlabel(r"$|W_0|$")
ax_c.set_ylabel(r"$g_s$")
ax_c.text(0.05, 0.9, "(c)", transform=ax_c.transAxes, fontsize=12, fontweight="bold")

fig.subplots_adjust(left=0.08, right=0.98, bottom=0.18, top=0.92)
plt.show()

Utility: Export for journals#

def savefig_pub(fig, filename, fmt="pdf", journal="jhep"):
    """Save at the correct column width for common journals."""
    widths = {"jhep": 6.5, "prd": 3.375, "prl": 3.375}
    w = widths.get(journal, 6.5)
    orig_w, orig_h = fig.get_size_inches()
    scale = w / orig_w
    fig.set_size_inches(w, orig_h * scale)
    fig.savefig(f"{filename}.{fmt}", format=fmt, dpi=300,
                bbox_inches="tight", pad_inches=0.03)
    fig.set_size_inches(orig_w, orig_h)
    print(f"Saved {filename}.{fmt} ({w:.1f}\" wide, {journal})")

# Usage: savefig_pub(fig, "my_figure", fmt="pdf", journal="jhep")

Take-aways#

Recipe

Plot type

Key settings

1

2×2 scatter

s=8, shared colorbar, rasterized=True

2

Histogram + KDE

bins=35, bw_method=0.3

3

Hexbin density

gridsize=25, equal aspect

4

Log-log scatter

s=10, dual panel

5

Residual histogram

Tolerance reference line

6

Bar chart

Integer binning

7

KDE heatmap

gaussian_kde + scatter overlay

8

Composite

GridSpec, panel labels

All plots use rasterized=True for scatter (small PDFs), 200+ DPI, serif fonts, and in-facing ticks. Uncomment fig.savefig(...) or use savefig_pub() to export.

Further reading#