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
(Created: March 2026)
Outline#
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 |
|
2 |
Histogram + KDE |
|
3 |
Hexbin density |
|
4 |
Log-log scatter |
|
5 |
Residual histogram |
Tolerance reference line |
6 |
Bar chart |
Integer binning |
7 |
KDE heatmap |
|
8 |
Composite |
|
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#
NB05 — Finding flux vacua — sample data source
NB07 — ISD sampling — sample data source
NB08 — Flux bounding — alternative ensemble source