""" Compact W1 vs W2 gradient-landscape figure for the UAI TPM 2026 workshop paper. Saves to uai_workshop/w1_vs_w2_derivatives.pdf at full text width, 1×2 layout. Usage: cd python exp/w1_vs_w2_figure.py """ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) import numpy as np import torch import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib as mpl from wasserstein_ica import WassersteinICA # ── aesthetics ─────────────────────────────────────────────────────────────── mpl.rcParams.update({ 'figure.dpi': 300, 'axes.grid': True, 'grid.alpha': 0.25, 'grid.linestyle': '--', 'axes.axisbelow': True, 'axes.spines.top': False, 'axes.spines.right': False, 'legend.frameon': False, 'font.size': 8, 'axes.labelsize': 8, 'axes.titlesize': 9, 'xtick.labelsize': 7, 'ytick.labelsize': 7, 'legend.fontsize': 7, }) COLOR_W2 = '#029E73' # green COLOR_W1 = '#CC78BC' # purple # ── data ───────────────────────────────────────────────────────────────────── np.random.seed(42) n_samples = 5000 S = np.random.standard_t(df=4, size=(2, n_samples)) A = np.array([[2.0, 0.0], [0.0, 0.5]]) X = torch.tensor(A @ S, dtype=torch.float32) ica = WassersteinICA(X) ica.whiten() # ── sweep ───────────────────────────────────────────────────────────────────── n_pts = 400 thetas = torch.linspace(0, np.pi, steps=n_pts) ws = torch.stack([torch.cos(thetas), torch.sin(thetas)], dim=1) w1_vals, w2_vals = [], [] for w in ws: w_n = w / torch.norm(w) w1_vals.append(ica.wasserstein1_distance(w_n).item()) w2_vals.append(ica.wasserstein2_distance(w_n).item()) w1_vals = np.array(w1_vals) w2_vals = np.array(w2_vals) th_np = thetas.numpy() grad_w1 = np.gradient(w1_vals, th_np) grad_w2 = np.gradient(w2_vals, th_np) grad_w1_n = grad_w1 / np.max(np.abs(grad_w1)) grad_w2_n = grad_w2 / np.max(np.abs(grad_w2)) # ── plot ────────────────────────────────────────────────────────────────────── TEXT_WIDTH_IN = 6.75 # full text width for one-column appendix fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(TEXT_WIDTH_IN, TEXT_WIDTH_IN * 0.38), constrained_layout=True) xticks = [0, np.pi/4, np.pi/2, 3*np.pi/4, np.pi] xticklabels = ['$0$', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$', r'$\pi$'] # panel 1 – landscape ax1.plot(th_np, w2_vals, color=COLOR_W2, lw=1.6, label=r'$W_2^2$') ax1.set_ylabel(r'$W_2^2$', color=COLOR_W2, labelpad=2) ax1.tick_params(axis='y', labelcolor=COLOR_W2) ax1_r = ax1.twinx() ax1_r.plot(th_np, w1_vals, color=COLOR_W1, lw=1.6, label=r'$W_1$', linestyle='--') ax1_r.set_ylabel(r'$W_1$', color=COLOR_W1, labelpad=2) ax1_r.tick_params(axis='y', labelcolor=COLOR_W1) ax1_r.spines['top'].set_visible(False) ax1.set_xticks(xticks); ax1.set_xticklabels(xticklabels) ax1.set_xlabel(r'Rotation angle $\theta$', labelpad=2) ax1.set_title('(a) Objective landscape', loc='left', pad=3) lines = [mpl.lines.Line2D([0],[0], color=COLOR_W2, lw=1.6), mpl.lines.Line2D([0],[0], color=COLOR_W1, lw=1.6, linestyle='--')] ax1.legend(lines, [r'$W_2^2$', r'$W_1$'], loc='lower center', ncol=2) # panel 2 – gradients ax2.axhline(0, color='gray', lw=1.0, alpha=0.5) ax2.plot(th_np, grad_w2_n, color=COLOR_W2, lw=1.6, label=r'$\nabla_\theta W_2^2$') ax2.plot(th_np, grad_w1_n, color=COLOR_W1, lw=1.6, linestyle='--', label=r'$\nabla_\theta W_1$') ax2.set_ylabel('Normalized gradient', labelpad=2) ax2.set_xlabel(r'Rotation angle $\theta$', labelpad=2) ax2.set_xticks(xticks); ax2.set_xticklabels(xticklabels) ax2.set_title('(b) Gradient profiles', loc='left', pad=3) ax2.legend(loc='upper left', ncol=2) # ── save ────────────────────────────────────────────────────────────────────── out_dir = os.path.join(os.path.dirname(__file__), '..', 'uai_workshop') out_path = os.path.join(out_dir, 'w1_vs_w2_derivatives.pdf') fig.savefig(out_path, format='pdf', bbox_inches='tight') print(f"Saved → {os.path.abspath(out_path)}")