UQ with Sparse Grids

PyApprox Tutorial Library

Compute moments from sparse grid quadrature, convert to PCE for Sobol indices, and compare efficiency against Monte Carlo.

Learning Objectives

After completing this tutorial, you will be able to:

  • Compute mean and variance directly from a sparse grid surrogate (Smolyak quadrature)
  • Explain why sparse grids can represent the same function as a PCE with nested point sets
  • Convert a Lagrange-based sparse grid to a PCE using SparseGridToPCEConverter
  • Compute covariance and Sobol sensitivity indices from the converted PCE
  • Compare sparse grid vs Monte Carlo moment estimation efficiency

Prerequisites

Complete:

Setup: Build an Isotropic Sparse Grid

We use the same 1D Euler–Bernoulli cantilever beam with 3 KLE terms and 3 QoIs as in UQ with Polynomial Chaos, enabling direct comparison of the two approaches.

We build a level-3 isotropic sparse grid with Gauss–Legendre nodes.

import numpy as np
import matplotlib.pyplot as plt
from pyapprox.util.backends.numpy import NumpyBkd
from pyapprox.benchmarks.instances.pde.cantilever_beam import (
    cantilever_beam_1d,
)
from pyapprox.surrogates.sparsegrids import (
    IsotropicSparseGridFitter,
    TensorProductSubspaceFactory,
    create_basis_factories,
)
from pyapprox.surrogates.affine.indices import LinearGrowthRule

bkd = NumpyBkd()

benchmark = cantilever_beam_1d(bkd, num_kle_terms=3)
model = benchmark.function()
prior = benchmark.prior()
marginals = prior.marginals()
nvars = model.nvars()
nqoi = model.nqoi()

qoi_labels = [
    r"$\delta_{\mathrm{tip}}$",
    r"$\sigma_{\mathrm{int}}$",
    r"$\kappa_{\max}$",
]

# Build isotropic sparse grid
factories = create_basis_factories(marginals, bkd, basis_type="gauss")
growth = LinearGrowthRule(scale=2, shift=1)
tp_factory = TensorProductSubspaceFactory(bkd, factories, growth)
level = 3
fitter = IsotropicSparseGridFitter(bkd, tp_factory, level)
samples = fitter.get_samples()
values = model(samples)
result = fitter.fit(values)
surrogate = result.surrogate

Native Moments from Smolyak Quadrature

CombinationSurrogate computes moments via the Smolyak combination of tensor product quadrature rules — no extra sampling and no basis conversion needed:

\[ \mathbb{E}[\hat{f}] = \sum_k c_k \int \hat{f}_k(\boldsymbol{\theta})\, d\mu(\boldsymbol{\theta}), \qquad \text{Var}[\hat{f}] = \sum_k c_k \left(\int \hat{f}_k^2\, d\mu - \left(\int \hat{f}_k\, d\mu\right)^2\right) \]

where \(c_k\) are Smolyak coefficients and each integral is exact for the polynomial subspace \(\hat{f}_k\).

sg_mean = bkd.to_numpy(surrogate.mean())
sg_var = bkd.to_numpy(surrogate.variance())
ImportantExact for the surrogate

These formulas are exact for the fitted sparse grid surrogate \(\hat{f}\), not the true model \(f\). The only source of error is the surrogate approximation itself — same caveat as PCE.

NoteCovariance and Sobol indices require conversion

CombinationSurrogate provides mean() and variance() natively. For covariance, Sobol indices, or marginalization, we need to convert the sparse grid to a PCE — that is the subject of the next section.

Converting Sparse Grids to PCE

A Lagrange interpolant on each tensor product subspace can be re-expanded in an orthonormal polynomial basis via spectral projection. SparseGridToPCEConverter iterates all subspaces, projects each to PCE, applies Smolyak coefficients, and merges duplicate multi-indices into a standard PolynomialChaosExpansion.

The key ingredients are:

  1. Orthonormal 1D bases matching the input distribution, created via create_bases_1d()
  2. The fitted CombinationSurrogate
from pyapprox.surrogates.affine.univariate import create_bases_1d
from pyapprox.surrogates.sparsegrids.converters.pce import (
    SparseGridToPCEConverter,
)

bases_1d = create_bases_1d(marginals, bkd)
converter = SparseGridToPCEConverter(bkd, bases_1d)
pce = converter.convert(surrogate)

The converted PCE gives us the full analytical moment toolkit. Let us verify that its mean and variance match the native sparse grid moments:

pce_mean = bkd.to_numpy(pce.mean())
pce_var = bkd.to_numpy(pce.variance())
pce_cov = bkd.to_numpy(pce.covariance())

The SG and PCE moments should agree to near machine precision, since both represent the same polynomial interpolant — just in different bases.

Sensitivity Analysis via Converted PCE

SparseGridSensitivityAnalysis is a convenience wrapper that converts the sparse grid to PCE internally and provides Sobol index computation in a single step.

from pyapprox.sensitivity import SparseGridSensitivityAnalysis
from pyapprox.tutorial_figures._sparse_grids import plot_sg_sobol

sa = SparseGridSensitivityAnalysis(surrogate, bases_1d)
main_effects = sa.main_effects()   # (nvars, nqoi)
total_effects = sa.total_effects()  # (nvars, nqoi)

fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
plot_sg_sobol(axes, main_effects, total_effects, bkd, qoi_labels)
plt.tight_layout()
plt.show()
Figure 1: Sobol indices computed from the sparse grid surrogate via SG-to-PCE conversion. Left: main effect pie chart. Right: total effects bar chart. The first KLE coefficient dominates the variance for all QoIs.

Marginal Densities: Fair Cost Comparison

To compare the sparse grid surrogate against direct Monte Carlo fairly, both approaches must use the same total wall-clock budget. We use timed() to measure the mean execution time per evaluation:

\[ N_{\text{SG}} \times t_{\text{beam}} + M \times t_{\text{SG}} = 500 \times t_{\text{beam}} \quad\Longrightarrow\quad M = \frac{(500 - N_{\text{SG}}) \times t_{\text{beam}}}{t_{\text{SG}}} \]

from pyapprox.interface.functions.timing import timed
from pyapprox.tutorial_figures._sparse_grids import plot_sg_marginals

# Measure per-evaluation cost
timed_model = timed(model)
np.random.seed(0)
warmup_samples = prior.rvs(50)
_ = timed_model(warmup_samples)
beam_cost = timed_model.timer().get("__call__").median()

timed_sg = timed(surrogate)
_ = timed_sg(warmup_samples)
sg_cost = timed_sg.timer().get("__call__").median()

N_sg = samples.shape[1]

# Equal-cost sample sizes
N_mc = 500
M_sg = int((N_mc - N_sg) * beam_cost / sg_cost)

# MC reference
np.random.seed(123)
samples_mc = prior.rvs(N_mc)
vals_mc = bkd.to_numpy(model(samples_mc))

# SG surrogate: M_sg equivalent-cost evaluations
np.random.seed(99)
samples_sg_eval = prior.rvs(M_sg)
preds_sg = bkd.to_numpy(surrogate(samples_sg_eval))

fig, axes = plt.subplots(1, nqoi, figsize=(14, 4))
plot_sg_marginals(axes, vals_mc, preds_sg, N_mc, N_sg, M_sg, nqoi, qoi_labels)
plt.tight_layout()
plt.show()
Figure 2: Marginal density estimates at equal wall-clock cost. MC (blue) uses 500 beam evaluations. SG (orange) uses N_SG beam evaluations for training plus M equivalent-cost surrogate evaluations. The sparse grid surrogate produces much smoother densities because the cheap surrogate allows orders of magnitude more samples in the same time budget.

SG vs. Monte Carlo: Efficiency Comparison

We compare moment estimation efficiency by increasing the sparse grid level with the budget and measuring relative error in the mean and variance. Monte Carlo uses the same number of raw model evaluations at each budget level.

Figure 3 repeats each experiment 20 times with different random seeds and plots the median error with 10th/90th percentile envelopes.

Figure 3: SG vs. Monte Carlo efficiency. Relative error in the mean (left) and variance (right) of tip deflection as a function of the number of model evaluations. For SG, the level increases with the budget. Lines show median over 20 repetitions; bands show 10th–90th percentiles.

The sparse grid achieves lower error than Monte Carlo for the same number of model evaluations because:

  • Monte Carlo converges at rate \(1/\sqrt{N}\), regardless of the function’s smoothness.
  • The sparse grid exploits the smoothness of the beam model through polynomial interpolation, so its error decays much faster.
  • By increasing the sparse grid level with the budget, the approximation error continues to shrink, while MC progress is slow and steady.

Key Takeaways

  • Sparse grids give mean and variance natively via Smolyak quadrature — no conversion needed
  • For covariance, Sobol indices, and marginalization, convert to PCE via SparseGridToPCEConverter
  • SparseGridSensitivityAnalysis is a convenience wrapper that handles the conversion internally
  • The converted PCE is mathematically equivalent to the sparse grid interpolant, just re-expanded in an orthonormal basis
  • For smooth functions, sparse grid-based UQ converges faster than Monte Carlo

Exercises

  1. Compare SG moments at levels 2, 3, and 4. How quickly do the mean and variance converge?

  2. Use pce.total_sobol_indices() directly on the converted PCE. Verify the results match SparseGridSensitivityAnalysis.total_effects().

  3. Repeat the analysis with Clenshaw–Curtis nodes (basis_type="clenshaw_curtis" in create_basis_factories() + ClenshawCurtisGrowthRule()). Compare point counts and accuracy vs Gauss nodes.

Next Steps