Multivariate Polynomial Interpolation#
In the previous guide, you learned the basics of polynomial interpolation in Minterpy for approximating one-dimensional functions. As the name “Minterpy” suggests, the package also supports constructing multivariate polynomials to approximate multidimensional functions.
In this in-depth tutorial, you will learn how to create an interpolating polynomial that approximates a two-dimensional function. We use a two-dimensional example for ease of visualization, but the main steps you’ll learn are applicable to polynomials of higher dimensions as well.
Before we begin, let’s import the necessary packages to follow along with this guide.
import minterpy as mp
import numpy as np
import matplotlib.pyplot as plt
Motivating function#
Consider the following two-dimensional function:
where \(\boldsymbol{x} = (x_1, x_2) \in [-1, 1]^2\).
Note
This function is known in the literature as the Franke function[1]. Here, however, the domain has been redefined to be \([-1, 1]^2\) instead of \([0, 1]^2\).
The function can be defined in Python as follows:
def fun(xx: np.ndarray):
xx0 = 9 * xx[:, 0]
xx1 = 9 * xx[:, 1]
# Compute the (first) Franke function
term_1 = 0.75 * np.exp(-0.25 * ((xx0 - 2) ** 2 + (xx1 - 2) ** 2))
term_2 = 0.75 * np.exp(
-1.00 * ((xx0 + 1) ** 2 / 49.0 + (xx1 + 1) ** 2 / 10.0)
)
term_3 = 0.50 * np.exp(-0.25 * ((xx0 - 7) ** 2 + (xx1 - 3) ** 2))
term_4 = 0.20 * np.exp(-1.00 * ((xx0 - 4) ** 2 + (xx1 - 7) ** 2))
yy = term_1 + term_2 + term_3 - term_4
return yy
The surface and contour plots of the function are shown below.
Show code cell source
from mpl_toolkits.axes_grid1 import make_axes_locatable
# --- Create 2D data
xx_1d = np.linspace(-1.0, 1.0, 1000)[:, np.newaxis]
mesh_2d = np.meshgrid(xx_1d, xx_1d)
xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
yy_2d = fun(xx_2d)
# --- Create two-dimensional plots
fig = plt.figure(figsize=(10, 5))
# Surface
axs_1 = plt.subplot(121, projection='3d')
axs_1.plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_2d.reshape(1000,1000).T,
linewidth=0,
cmap="plasma",
antialiased=False,
alpha=0.5
)
axs_1.set_xlabel("$x_1$", fontsize=14)
axs_1.set_ylabel("$x_2$", fontsize=14)
axs_1.set_title("Surface plot", fontsize=14)
axs_1.tick_params(axis='both', which='major', labelsize=12)
# Contour
axs_2 = plt.subplot(122)
cf = axs_2.contourf(
mesh_2d[0], mesh_2d[1], yy_2d.reshape(1000, 1000).T, cmap="plasma"
)
axs_2.set_xlabel("$x_1$", fontsize=14)
axs_2.set_ylabel("$x_2$", fontsize=14)
axs_2.set_title("Contour plot", fontsize=14)
divider = make_axes_locatable(axs_2)
cax = divider.append_axes('right', size='5%', pad=0.05)
fig.colorbar(cf, cax=cax, orientation='vertical')
axs_2.axis('scaled')
axs_2.tick_params(axis='both', which='major', labelsize=12)
fig.tight_layout(pad=6.0)

Create an interpolating polynomial#
The steps to create a multidimensional interpolating polynomial in Minterpy from scratch are similar to those used for the one-dimensional case. The steps are:
Define the multi-index set of the polynomial
Construct an interpolation grid
Evaluate the function of interest at the grid points (so-called unisolvent nodes)
Create a polynomial in the Lagrange basis
Transformation from the Lagrange basis to another basis, preferrably the Newton basis, for further manipulation.
As before, we will go through these steps one at a time, highlighting any differences that arise compared to the one-dimensional case.
Define the multi-index set#
As demonstrated in the previous tutorial, multi-index sets in Minterpy are represented by the MultiIndexSet
class.
You can use the class method from_degree()
to create a complete multi-index set for a given spatial dimension (first argument), polynomial degree and polynomial degree (second argument).
For multivariate polynomials, you also need to specify the \(l_p\)-degree (\(> 0.0\)) of the multi-index set (third argument).
A complete multi-index set with spatial dimension \(m\), polynomial degree \(n\), and \(l_p\)-degree \(p\) denoted by \(A_{m, n, p}\) includes all multi-indices \(\boldsymbol{\alpha}\) that satisfy the condition \(\lVert \boldsymbol{\alpha} \rVert_p \leq n\), specifically
For instance, the complete multi-index set for \(m = 2\), \(n = 5\), \(p = 2.0\):
m = 2
n = 4
p = 2.0
mi = mp.MultiIndexSet.from_degree(m, n, p)
mi
MultiIndexSet(
array([[0, 0],
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[0, 1],
[1, 1],
[2, 1],
[3, 1],
[0, 2],
[1, 2],
[2, 2],
[3, 2],
[0, 3],
[1, 3],
[2, 3],
[0, 4]]),
lp_degree=2.0
)
The set contains the exponents of a multidimensional polynomial. Each column corresponds to the exponents per spatial dimension, while each row indicates the multi-index elements.
To check how many elements the set contains, you can use the built-in len()
function:
len(mi)
17
Several common choices for \(l_p\)-degree are \(1.0\) (total-degree set), \(2.0\) (Euclidean-degree set), and \(\infty\) (maximum-degree set). The illustration below shows the difference between these three different choices of \(l_p\)-degree (for the same spatial dimension and polynomial degree) in relation to the exponents as multi-indices.
Show code cell source
mi_1 = mp.MultiIndexSet.from_degree(m, n, 1.0)
mi_inf = mp.MultiIndexSet.from_degree(m, n, np.inf)
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(12, 4))
axs[0].scatter(mi_1.exponents[:, 0], mi_1.exponents[:, 1], marker='x', color="black")
axs[0].set_xlabel("exponent (dimension = 1)", fontsize=14);
axs[0].set_ylabel("exponent (dimension = 2)", fontsize=14);
axs[0].set_xticks(np.arange(0, n + 1));
axs[0].set_yticks(np.arange(0, n + 1));
axs[0].set_title(rf"m = {m}, n = {n}, p = 1.0", fontsize=16);
axs[0].tick_params(axis='both', which='major', labelsize=12)
axs[1].scatter(mi.exponents[:, 0], mi.exponents[:, 1], marker='x', color="black")
axs[1].set_xlabel("exponent (dimension = 1)", fontsize=14);
axs[1].set_xticks(np.arange(0, n + 1));
axs[1].set_yticks(np.arange(0, n + 1));
axs[1].set_title(rf"m = {m}, n = {n}, p = 2.0", fontsize=16)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[2].scatter(mi_inf.exponents[:, 0], mi_inf.exponents[:, 1], marker='x', color="black")
axs[2].set_xlabel("exponent (dimension = 1)", fontsize=14);
axs[2].set_xticks(np.arange(0, n + 1));
axs[2].set_yticks(np.arange(0, n + 1));
axs[2].set_title(rf"m = {m}, n = {n}, p = $\infty$", fontsize=16);
axs[2].tick_params(axis='both', which='major', labelsize=12)

Construct an interpolation grid#
An interpolating polynomial, one-dimensional or otherwise, lives on an interpolation grid.
To construct an interpolation grid, pass the previously defined multi-index set to the default constructor of the Grid
class:
grd = mp.Grid(mi)
The unisolvent nodes associated with the interpolating grid has the same spatial dimension as its defining multi-index set. By default, Minterpy generates unisolvent nodes according to the Leja-ordered Chebyshev-Lobatto points. The unisolvent nodes that correspond to the multi-index set are:
grd.unisolvent_nodes
array([[ 1.00000000e+00, -1.00000000e+00],
[-1.00000000e+00, -1.00000000e+00],
[-6.12323400e-17, -1.00000000e+00],
[ 7.07106781e-01, -1.00000000e+00],
[-7.07106781e-01, -1.00000000e+00],
[ 1.00000000e+00, 1.00000000e+00],
[-1.00000000e+00, 1.00000000e+00],
[-6.12323400e-17, 1.00000000e+00],
[ 7.07106781e-01, 1.00000000e+00],
[ 1.00000000e+00, 6.12323400e-17],
[-1.00000000e+00, 6.12323400e-17],
[-6.12323400e-17, 6.12323400e-17],
[ 7.07106781e-01, 6.12323400e-17],
[ 1.00000000e+00, -7.07106781e-01],
[-1.00000000e+00, -7.07106781e-01],
[-6.12323400e-17, -7.07106781e-01],
[ 1.00000000e+00, 7.07106781e-01]])
Each row corresponds to two-dimensional grid points; the first and second column contain all the values of the first and second spatial dimension, respectively. These are the points at which the function of interest is evaluated; the results are then used construct an interpolating polynomial.
The plots below show three different two-dimensional unisolvent nodes that correspond to three different \(l_p\)-degrees with the same spatial dimension and polynomial degree.
Show code cell source
mi_1 = mp.MultiIndexSet.from_degree(m, n, 1.0)
grd_1 = mp.Grid(mi_1)
mi_inf = mp.MultiIndexSet.from_degree(m, n, np.inf)
grd_inf = mp.Grid(mi_inf)
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(12, 4))
axs[0].scatter(grd_1.unisolvent_nodes[:, 0], grd_1.unisolvent_nodes[:, 1], marker='x', color="black")
axs[0].set_xlabel("$x_1$", fontsize=14);
axs[0].set_ylabel("$x_2$", fontsize=14);
axs[0].set_xlim([-1.25, 1.25]);
axs[0].set_ylim([-1.25, 1.25]);
axs[0].set_title(rf"m = {m}, n = {n}, p = 1.0", fontsize=16);
axs[0].tick_params(axis='both', which='major', labelsize=12)
axs[1].scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1], marker='x', color="black")
axs[1].set_xlabel("$x_1$", fontsize=14);
axs[1].set_xlim([-1.25, 1.25]);
axs[1].set_ylim([-1.25, 1.25]);
axs[1].set_title(rf"m = {m}, n = {n}, p = 2.0", fontsize=16)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[2].scatter(grd_inf.unisolvent_nodes[:, 0], grd_inf.unisolvent_nodes[:, 1], marker='x', color="black")
axs[2].set_xlabel("$x_1$", fontsize=14);
axs[2].set_xlim([-1.25, 1.25]);
axs[2].set_ylim([-1.25, 1.25]);
axs[2].set_title(rf"m = {m}, n = {n}, p = $\infty$", fontsize=16);
axs[2].tick_params(axis='both', which='major', labelsize=12)

Evaluate the function at the unisolvent nodes#
An interpolating polynomial satisfies the condition that the polynomial values at the unisolvent nodes coincide with the value of the given function at the same nodes.
Calling an instance of Grid
with a given function evaluates the function at the unisolvent nodes:
coeffs = grd(fun)
coeffs
array([1.61905391e-04, 3.37547431e-04, 1.22099371e-03, 4.12046496e-04,
6.92732015e-04, 2.71238400e-05, 9.22305232e-06, 3.46822360e-05,
5.33538427e-05, 1.07557552e-01, 1.83818849e-01, 7.66420591e-01,
2.74380251e-01, 5.48517554e-03, 1.14357334e-02, 4.13659227e-02,
1.12952506e-02])
Note
Make sure that the given function accepts a two-dimensional array whose each column corresponds to the values per spatial dimension.
Create a polynomial in the Lagrange basis#
The function values at the unisolvent nodes are equal to the coefficients of a polynomial in the Lagrange basis \(c_{\mathrm{lag}, \boldsymbol{\alpha}}\):
where \(A_{m, n, p}\) is the complete multi-index set and \(\Psi_{\mathrm{lag}, \boldsymbol{\alpha}}\)’s are the Lagrange basis polynomials, each of which satisfies:
where \(\delta{\cdot, \cdot}\) denotes the Kronecker delta.
To create a polynomial in the Lagrange basis from the given grid and coefficients, use the from_grid()
class method of LagrangePolynomial
class:
lag_poly = mp.LagrangePolynomial.from_grid(grd, coeffs)
Transform to the Newton basis#
Just like the one-dimensional case, to do more with the polynomial in Minterpy, it must first be transformed to the Newton basis such that the polynomial is of the form:
where \(c_{\mathrm{nwt}, \boldsymbol{\alpha}}\)’s are the coefficients of the polynomial in the multidimensional Newton basis and \(\Psi_{\mathrm{nwt}, \boldsymbol{\alpha}}\)’s are the Newton basis polynomials. The basis polynomial associated with multi-index element \(\boldsymbol{\alpha}\) is defined as follows:
where \(p_{i, j}\)’s are the interpolation points along dimension \(j\) (i.e., the so-called generating points).
Note
In multiple dimensions (i.e., \(m > 1\)), the generating points / nodes are not the same same as the unisolvent nodes.
As before, to transform a polynomial in the Lagrange basis to a polynomial in the Newton basis, use the LagrangeToNewton
class:
nwt_poly = mp.LagrangeToNewton(lag_poly)()
Note
In the above statement, the first part of the call, mp.LagrangeToNewton(lag_poly)
, creates a transformation instance that converts the given polynomial from the Lagrange basis to the Newton basis.
The second call, which is made without any arguments, performs the transformation and returns the actual NewtonPolynomial
instance.
Evaluate the polynomial at a set of query points#
Create a set of random query points in \([-1, 1]^2\):
xx_test = -1 + 2 * np.random.rand(1000, 2)
Then evaluate the original function at these points
yy_test = fun(xx_test)
yy_test[:5]
array([0.41215589, 0.00807249, 0.00082283, 0.16167358, 0.02625827])
Calling an instance of NewtonPolynomial
at these query points evaluate the polynomial at the query points:
yy_poly = nwt_poly(xx_test)
yy_poly[:5]
array([ 0.41536034, -0.01848832, -0.01834247, 0.25030834, 0.18142068])
Keep in mind that the input array should have two columns, as the polynomial is two-dimensional.
As expected, evaluating the polynomial at \(1'000\) query points returns an array with \(1'000\) values:
yy_poly.shape
(1000,)
We can compare the surface plot of the original function with that of the interpolating polynomial, which also includes the interpolating nodes:
Show code cell source
# --- Create 2D data
xx_1d = np.linspace(-1.0, 1.0, 250)[:, np.newaxis]
mesh_2d = np.meshgrid(xx_1d, xx_1d)
xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
yy_plot_test = fun(xx_2d)
yy_plot_poly = nwt_poly(xx_2d)
# --- Create two-dimensional plots
fig, axs = plt.subplots(
nrows=1,
ncols=2,
figsize=(12, 4),
subplot_kw=dict(projection="3d"),
layout="compressed",
)
# Original function
axs[0].plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_plot_test.reshape(250, 250).T,
linewidth=0,
cmap="plasma",
antialiased=False,
alpha=0.5
)
axs[0].set_xlabel("$x_1$", fontsize=14)
axs[0].set_ylabel("$x_2$", fontsize=14)
axs[0].set_title("Original function", fontsize=16)
axs[0].tick_params(axis='both', which='major', labelsize=12)
# Interpolating polynomial
axs[1].plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_plot_poly.reshape(250, 250).T,
linewidth=0,
cmap="plasma",
antialiased=False,
alpha=0.5
)
axs[1].set_xlabel("$x_1$", fontsize=14)
axs[1].set_ylabel("$x_2$", fontsize=14)
axs[1].set_title("Interpolating polynomial", fontsize=16)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[1].scatter(grd.unisolvent_nodes[:, 0], grd.unisolvent_nodes[:, 1], coeffs, color="k");

Assess the accuracy of the polynomial#
As you can see in the plot above, the chosen polynomial degree isn’t sufficient to accurately approximate the original function. The interpolating polynomial misses some of the key features of the original function.
The infinity norm provides a measure of the greatest error of the interpolant over the whole domain. The norm is defined as:
The infinity norm of \(Q\) can be approximated using the \(1'000\) test points created above:
np.max(np.abs(yy_test - yy_poly))
np.float64(0.868209330432235)
This number suggests that the interpolating polynomial hasn’t converged yet with the current choices of \(n\) and \(p\).
Assess the empirical convergence of interpolating polynomials#
To assess the convergence of interpolating polynomials, you can once again resort to the high-level function interpolate()
as a shortcut to the steps taken above.
Let’s investigate the convergence of interpolating polynomials with increasing polynomial degrees and three different choices of \(l_p\)-degrees:
poly_degrees = np.arange(0, 121, 4)
lp_degrees = [1.0, 2.0, np.inf]
yy_poly = np.empty((len(xx_test), len(poly_degrees), len(lp_degrees)))
for i, n in enumerate(poly_degrees):
for j, p in enumerate(lp_degrees):
fx_interp = mp.interpolate(fun, spatial_dimension=2, poly_degree=n, lp_degree=p)
yy_poly[:, i, j] = fx_interp(xx_test)
errors = np.max(np.abs(yy_test[:, np.newaxis, np.newaxis] - yy_poly), axis=0)
The convergence of the error is shown below:
Show code cell source
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 5))
# Convergence with respect to the polynomial degrees
axs[0].plot(poly_degrees, errors[:, 0], '-x', label="$p = 1.0$");
axs[0].plot(poly_degrees, errors[:, 1], '-x', label="$p = 2.0$");
axs[0].plot(poly_degrees, errors[:, 2], '-x', label="$p = \infty$");
axs[0].set_yscale("log")
axs[0].legend(fontsize=16);
axs[0].set_xlabel("Polynomial degree ($n$)", fontsize=14)
axs[0].set_ylabel(r"$|| f - Q ||_\infty$", fontsize=14)
axs[0].tick_params(axis='both', which='major', labelsize=12)
# Convergence with respect to the number of coefficients
num_coeffs = np.zeros((len(poly_degrees), len(lp_degrees)))
for i, n in enumerate(poly_degrees):
for j, p in enumerate(lp_degrees):
num_coeffs[i, j] = len(mp.MultiIndexSet.from_degree(m, n, p))
axs[1].plot(num_coeffs[:, 0], errors[:, 0], '-x', label="$p = 1.0$");
axs[1].plot(num_coeffs[:, 1], errors[:, 1], '-x', label="$p = 2.0$");
axs[1].plot(num_coeffs[:, 2], errors[:, 2], '-x', label="$p = \infty$");
axs[1].set_yscale("log")
axs[1].set_xscale("log")
axs[1].set_xlabel("Number of coefficients", fontsize=14)
axs[1].tick_params(axis='both', which='major', labelsize=12)
fig.tight_layout()

Below are the surface plots of the original function and the interpolating polynomial with \(n = 100\) and \(p = 2.0\), which appears to be numerically converged based on the above plots.
Show code cell source
from mpl_toolkits.axes_grid1 import make_axes_locatable
# --- Create 2D data
xx_1d = np.linspace(-1.0, 1.0, 250)[:, np.newaxis]
mesh_2d = np.meshgrid(xx_1d, xx_1d)
xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
yy_fun = fun(xx_2d)
yy_poly = mp.interpolate(fun, 2, 128, 1.0)(xx_2d)
# --- Create two-dimensional plots
fig, axs = plt.subplots(
nrows=1,
ncols=2,
figsize=(12, 4),
subplot_kw=dict(projection="3d"),
layout="compressed",
)
# Original function
axs[0].plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_fun.reshape(250, 250).T,
linewidth=0,
cmap="plasma",
antialiased=False,
alpha=0.5
)
axs[0].set_xlabel("$x_1$", fontsize=14)
axs[0].set_ylabel("$x_2$", fontsize=14)
axs[0].set_title("Original function", fontsize=16)
axs[0].tick_params(axis='both', which='major', labelsize=12)
# Interpolating polynomial
axs[1].plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_poly.reshape(250, 250).T,
linewidth=0,
cmap="plasma",
antialiased=False,
alpha=0.5
)
axs[1].set_xlabel("$x_1$", fontsize=14)
axs[1].set_ylabel("$x_2$", fontsize=14)
axs[1].set_title("Interpolating polynomial", fontsize=16)
axs[1].tick_params(axis='both', which='major', labelsize=12)

From Interpolant to polynomial#
As explained in the previous tutorial,
the callable produced by interpolate()
is not a Minterpy polynomial.
There are convenient methods to represent the interpolant in one of the Minterpy polynomials. These methods are summarized in the table below.
Method |
Return the interpolant as a polynomial in the… |
---|---|
These bases will be the topic of Polynomial Bases and Transformations in-depth tutorial.
To get the interpolating polynomial in the Newton basis, call:
fx_interp.to_newton()
<minterpy.polynomials.newton_polynomial.NewtonPolynomial at 0x7fb861d06410>
Summary#
In this tutorial, you learned how to create a two-dimensional (2D) interpolating polynomial from scratch to approximate a given function then evaluate it at a set of query points in Minterpy.
The steps are:
Define a multi-index set
Construct an interpolation grid
Evaluate the given function on the grid points (i.e., the unisolvent nodes)
Construct an interpolating polynomial in the Lagrange basis
Transform the polynomial into the equivalent Newton basis
As you’ve seen, these steps are similar to the ones in the One-Dimensional Polynomial Interpolation tutorial. Here are some key things to keep in mind when working with multivariate polynomials:
You need to specify the \(l_p\)-degree \(p\) to fully define the multi-index set.
Your function of interest should take a 2D array as input, where each row represents a point in multidimensional space and each column corresponds to a value in each dimension.
When evaluating an interpolating polynomial, make sure your input has the same 2D array structure.
While the example here is two-dimensional, the same principles apply to polynomials in higher dimensions.
Finally, you saw that for the chosen function, an interpolating polynomial approximate it with sufficiently high polynomial degree.
You’ve now learned how to construct and work with interpolating polynomials in Minterpy, whether in one or multiple dimensions.
The next two tutorials will explore additional features and capabilities of Minterpy polynomials.