| """Visualization utilities for physics-informed Bayesian optimization."""
|
|
|
| from typing import Callable, Dict, List, Optional, Tuple
|
|
|
| import torch
|
| from torch import Tensor
|
| import numpy as np
|
|
|
|
|
| def plot_convergence(
|
| campaign_df,
|
| maximize: bool = True,
|
| title: str = "Optimization Convergence",
|
| figsize: Tuple[int, int] = (10, 6),
|
| ):
|
| """Plot the optimization convergence curve.
|
|
|
| Args:
|
| campaign_df: DataFrame from OptimizationCampaign.to_dataframe().
|
| maximize: Whether the objective is being maximized.
|
| title: Plot title.
|
| figsize: Figure size.
|
|
|
| Returns:
|
| matplotlib Figure.
|
| """
|
| import matplotlib.pyplot as plt
|
|
|
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
|
| objectives = campaign_df["objective"].values
|
|
|
|
|
| ax1.plot(range(len(objectives)), objectives, "o-", alpha=0.6, markersize=4)
|
| ax1.set_xlabel("Experiment Number")
|
| ax1.set_ylabel("Objective")
|
| ax1.set_title("All Observations")
|
| ax1.grid(True, alpha=0.3)
|
|
|
|
|
| if maximize:
|
| best_so_far = np.maximum.accumulate(objectives)
|
| else:
|
| best_so_far = np.minimum.accumulate(objectives)
|
|
|
| ax2.plot(range(len(best_so_far)), best_so_far, "s-", color="green", markersize=4)
|
| ax2.set_xlabel("Experiment Number")
|
| ax2.set_ylabel("Best Objective")
|
| ax2.set_title("Best So Far")
|
| ax2.grid(True, alpha=0.3)
|
|
|
| fig.suptitle(title, fontsize=14)
|
| plt.tight_layout()
|
| return fig
|
|
|
|
|
| def plot_surrogate_1d(
|
| surrogate,
|
| bounds: Tuple[float, float],
|
| X_observed: Optional[Tensor] = None,
|
| y_observed: Optional[Tensor] = None,
|
| physics_fn: Optional[Callable] = None,
|
| true_fn: Optional[Callable] = None,
|
| n_grid: int = 200,
|
| title: str = "Surrogate Model",
|
| figsize: Tuple[int, int] = (10, 6),
|
| ):
|
| """Plot a 1D surrogate model with confidence intervals.
|
|
|
| Args:
|
| surrogate: A SurrogateModel instance.
|
| bounds: (lower, upper) for the 1D input.
|
| X_observed: Observed inputs (n, 1).
|
| y_observed: Observed outputs (n, 1).
|
| physics_fn: Optional physics model for comparison.
|
| true_fn: Optional true function for comparison.
|
| n_grid: Number of grid points.
|
| title: Plot title.
|
| figsize: Figure size.
|
|
|
| Returns:
|
| matplotlib Figure.
|
| """
|
| import matplotlib.pyplot as plt
|
|
|
| fig, ax = plt.subplots(figsize=figsize)
|
|
|
| X_grid = torch.linspace(bounds[0], bounds[1], n_grid).unsqueeze(-1).to(torch.float64)
|
| mean, var = surrogate.predict(X_grid)
|
| std = var.sqrt()
|
|
|
| x_np = X_grid.squeeze().numpy()
|
| mean_np = mean.squeeze().detach().numpy()
|
| std_np = std.squeeze().detach().numpy()
|
|
|
|
|
| ax.plot(x_np, mean_np, "b-", label="Surrogate Mean", linewidth=2)
|
| ax.fill_between(
|
| x_np,
|
| mean_np - 2 * std_np,
|
| mean_np + 2 * std_np,
|
| alpha=0.2,
|
| color="blue",
|
| label="95% CI",
|
| )
|
|
|
|
|
| if physics_fn is not None:
|
| with torch.no_grad():
|
| physics_pred = physics_fn(X_grid).squeeze().numpy()
|
| ax.plot(x_np, physics_pred, "r--", label="Physics Model", linewidth=1.5)
|
|
|
|
|
| if true_fn is not None:
|
| with torch.no_grad():
|
| true_pred = true_fn(X_grid).squeeze().numpy()
|
| ax.plot(x_np, true_pred, "k-", label="True Function", linewidth=1.5, alpha=0.7)
|
|
|
|
|
| if X_observed is not None and y_observed is not None:
|
| ax.scatter(
|
| X_observed.squeeze().numpy(),
|
| y_observed.squeeze().numpy(),
|
| c="red",
|
| s=50,
|
| zorder=5,
|
| label="Observations",
|
| edgecolors="black",
|
| )
|
|
|
| ax.set_xlabel("Input")
|
| ax.set_ylabel("Output")
|
| ax.set_title(title)
|
| ax.legend()
|
| ax.grid(True, alpha=0.3)
|
| plt.tight_layout()
|
| return fig
|
|
|
|
|
| def plot_surrogate_2d(
|
| surrogate,
|
| bounds: Tensor,
|
| param_names: Tuple[str, str] = ("x1", "x2"),
|
| X_observed: Optional[Tensor] = None,
|
| n_grid: int = 50,
|
| title: str = "Surrogate Model (2D)",
|
| figsize: Tuple[int, int] = (12, 5),
|
| ):
|
| """Plot 2D surrogate model as contour plots (mean and uncertainty).
|
|
|
| Args:
|
| surrogate: A SurrogateModel instance.
|
| bounds: Tensor of shape (2, 2) with [lower, upper] bounds.
|
| param_names: Names of the two parameters.
|
| X_observed: Observed inputs (n, 2).
|
| n_grid: Grid resolution per dimension.
|
| title: Plot title.
|
| figsize: Figure size.
|
|
|
| Returns:
|
| matplotlib Figure.
|
| """
|
| import matplotlib.pyplot as plt
|
|
|
| x1 = torch.linspace(float(bounds[0, 0]), float(bounds[1, 0]), n_grid)
|
| x2 = torch.linspace(float(bounds[0, 1]), float(bounds[1, 1]), n_grid)
|
| X1, X2 = torch.meshgrid(x1, x2, indexing="ij")
|
| X_grid = torch.stack([X1.flatten(), X2.flatten()], dim=-1).to(torch.float64)
|
|
|
| mean, var = surrogate.predict(X_grid)
|
| mean_2d = mean.squeeze().reshape(n_grid, n_grid).detach().numpy()
|
| std_2d = var.sqrt().squeeze().reshape(n_grid, n_grid).detach().numpy()
|
|
|
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
|
|
|
| c1 = ax1.contourf(
|
| X1.numpy(), X2.numpy(), mean_2d, levels=20, cmap="viridis"
|
| )
|
| plt.colorbar(c1, ax=ax1)
|
| ax1.set_xlabel(param_names[0])
|
| ax1.set_ylabel(param_names[1])
|
| ax1.set_title("Predicted Mean")
|
|
|
|
|
| c2 = ax2.contourf(
|
| X1.numpy(), X2.numpy(), std_2d, levels=20, cmap="plasma"
|
| )
|
| plt.colorbar(c2, ax=ax2)
|
| ax2.set_xlabel(param_names[0])
|
| ax2.set_ylabel(param_names[1])
|
| ax2.set_title("Predicted Std Dev")
|
|
|
|
|
| if X_observed is not None:
|
| for ax in [ax1, ax2]:
|
| ax.scatter(
|
| X_observed[:, 0].numpy(),
|
| X_observed[:, 1].numpy(),
|
| c="red",
|
| s=30,
|
| edgecolors="white",
|
| zorder=5,
|
| )
|
|
|
| fig.suptitle(title, fontsize=14)
|
| plt.tight_layout()
|
| return fig
|
|
|