[49]:
use_gpu = True # Set to False to use CPU
dev = str(0)

import os
if use_gpu:
    try:
        import cupy as xp
        from cupyx.scipy.interpolate import Akima1DInterpolator as stock_akima
        os.environ["OMP_NUM_THREADS"] = str(1)
        os.system("OMP_NUM_THREADS=1")
        os.system("CUDA_VISIBLE_DEVICES=" + dev)
        os.environ["CUDA_VISIBLE_DEVICES"] = dev
        os.system("echo $CUDA_VISIBLE_DEVICES")

        os.environ["XLA_FLAGS"] = '--xla_force_host_platform_device_count=1'
    except:
        print('No GPUs available, using CPUs')
        import numpy as np
        from scipy.interpolate import Akima1DInterpolator as stock_akima

else:
    import numpy as xp
    from scipy.interpolate import Akima1DInterpolator as stock_akima

import numpy as np
import matplotlib.pyplot as plt
from cudakima import AkimaInterpolant1D

import scipy
0
[3]:
%load_ext autoreload
%autoreload 2
[4]:
# just for plotting purposes
def to_cpu(x):
    try:
        return x.get()
    except:
        return x

Getting started

[19]:
cubic_interp = AkimaInterpolant1D(sanitize=True, use_gpu=use_gpu, threadsperblock=512, order='cubic')
linear_interp = AkimaInterpolant1D(sanitize=True, use_gpu=use_gpu, threadsperblock=512, order='linear')

Arrays of equal shape

[20]:
# Generate some random data
xmin, xmax = 0, 10 # X Domain
ymin, ymax = -100, 100 # Y Domain
npoints = 10 # Number of points
nrealizations = 100 # Number of realizations

x = xp.random.random(size=(nrealizations, npoints)) * (xmax - xmin) + xmin
# Make sure the first and last points are the same
x[:, 0] = xmin
x[:, -1] = xmax
y = xp.random.random(size=(nrealizations, npoints)) * (ymax - ymin) + ymin


# Define the x values to interpolate
xinterp = xp.linspace(xmin, xmax, 1000)
[21]:
# Generate some random data
xmin, xmax = -4, np.log10(2.9e-2) # X Domain
ymin, ymax = -100, 100 # Y Domain
npoints = 10 # Number of points
nrealizations = 100 # Number of realizations

x = xp.random.random(size=(nrealizations, npoints)) * (xmax - xmin) + xmin
# Make sure the first and last points are the same
x[:, 0] = xmin
x[:, -1] = xmax
y = xp.random.random(size=(nrealizations, npoints)) * (ymax - ymin) + ymin

y = scipy.stats.truncnorm(loc=0, scale=0.1, a=-3, b=3).rvs(size=(nrealizations, npoints))


# Define the x values to interpolate
xinterp = xp.linspace(xmin, xmax, 1000)
[22]:
linear_result = linear_interp(xinterp, x, y)
cubic_result = cubic_interp(xinterp, x, y)
[23]:
realization = 4

for result, label in zip([linear_result, cubic_result], ['Linear Akima', 'Cubic Akima']):
    plt.plot(to_cpu(xinterp), 10**to_cpu(result)[realization], label=label)

plt.scatter(to_cpu(x)[realization], 10**to_cpu(y)[realization], marker='x', c='k', label='Knots')



plt.legend()
plt.show()
../_images/notebooks_tutorial_9_0.png

Arrays of different shapes

You can combine arrays of different shapes padding them with xp.nan and stacking them together

[24]:
# generate random arrays as before, but fill some of them with nans
x = xp.random.random(size=(nrealizations, npoints)) * (xmax - xmin) + xmin
# Make sure the first and last points are the same
x[:, 0] = xmin
x[:, -4] = xmax
y = xp.random.random(size=(nrealizations, npoints)) * (ymax - ymin) + ymin

x[0, -3:] = xp.nan
x[1, -2:] = xp.nan
x[2, -1:] = xp.nan

y[0, -3:] = xp.nan
y[1, -2:] = xp.nan
y[2, -1:] = xp.nan
[25]:
linear_result = linear_interp(xinterp, x, y)
cubic_result = cubic_interp(xinterp, x, y)
[26]:
realization = 3

for result, label in zip([linear_result, cubic_result], ['Linear Akima', 'Cubic Akima']):
    plt.plot(to_cpu(xinterp), to_cpu(result)[realization], label=label)

plt.scatter(to_cpu(x)[realization], to_cpu(y)[realization], marker='x', c='k', label='Knots')

plt.legend()
plt.show()
../_images/notebooks_tutorial_14_0.png

arrays containing less than 4 points

The boundary conditions currently implemented require at least 4 points in the interpolation grid. If there are less then 4 non-NaN values, the code falls back on linear interpolation between them.

[27]:
# generate random arrays as before, but fill some of them with nans
x = xp.random.random(size=(nrealizations, npoints)) * (xmax - xmin) + xmin
# Make sure the first and last points are the same
x[:, 0] = xmin
x[:, 1] = xmax
y = xp.random.random(size=(nrealizations, npoints)) * (ymax - ymin) + ymin

# introduce a case with only 2 grid points and one with 3 grid points

x[0, 2:] = xp.nan
x[1, 3:] = xp.nan
x[2, 4:] = xp.nan

y[0, 2:] = xp.nan
y[1, 3:] = xp.nan
y[2, 4:] = xp.nan
[28]:
linear_result = linear_interp(xinterp, x, y)
cubic_result = cubic_interp(xinterp, x, y)
[29]:
fig = plt.figure(figsize=(15, 5))

titles = ['2 points, linear', '3 points, linear', '4+ points, Akima']

for realization, title in zip(range(3), titles):

    plt.subplot(1, 3, realization + 1)
    for result, label in zip([linear_result, cubic_result], ['Linear Akima', 'Cubic Akima']):
        plt.plot(to_cpu(xinterp), to_cpu(result)[realization], label=label)
    plt.scatter(to_cpu(x)[realization], to_cpu(y)[realization], marker='x', c='k', label='Knots')
    plt.title(title)

    plt.legend()
plt.show()
../_images/notebooks_tutorial_19_0.png

Comparison with Cubic Splines

[30]:
from scipy.interpolate import CubicSpline
[31]:
realization = 5 # Choose a realization without NaNs

xhere = to_cpu(x[realization])
yhere = to_cpu(y[realization])
resulthere = to_cpu(cubic_result[realization])

#sort the arrays
idx = np.argsort(xhere)
xhere = xhere[idx]
yhere = yhere[idx]

cs = CubicSpline(xhere, yhere)
resultcs = cs(to_cpu(xinterp))

plt.plot(to_cpu(xinterp), to_cpu(result)[realization], c='k', ls = '--', lw=1, label='Akima Splines')
plt.plot(to_cpu(xinterp), resultcs, c='r', ls = '--', lw=1, label='Cubic Splines')
plt.scatter(to_cpu(x)[realization], to_cpu(y)[realization], marker='x', c='k', label='Knots')

plt.legend()
plt.show()
../_images/notebooks_tutorial_22_0.png

Akima splines show smoother behaviour with respect to Cubic Splines

Timing against the stock implementation in Scipy / Cupy

[32]:
# Generate some random data
xmin, xmax = 0, 10 # X Domain
ymin, ymax = -100, 100 # Y Domain
npoints = 10 # Number of points
nrealizations = 100 # Number of realizations

x = xp.random.random(size=(nrealizations, npoints)) * (xmax - xmin) + xmin
# Make sure the first and last points are the same
x[:, 0] = xmin
x[:, -1] = xmax
y = xp.random.random(size=(nrealizations, npoints)) * (ymax - ymin) + ymin

# Sort the array, cupy and scipy demand sorted x values
x = xp.sort(x, axis=1)

# Define the x values to interpolate
xinterp = xp.linspace(xmin, xmax, 1000)
[33]:
def stock_interpolator(x, x_all, y_all):

    result = xp.empty((len(x_all), len(x)))
    for i, (x_here, y_here) in enumerate(zip(x_all, y_all)):
        interpolator = stock_akima(x_here, y_here)
        result[i] = interpolator(x)
    return result

[34]:
cudakima_interpolator = AkimaInterpolant1D(sanitize=False, use_gpu=use_gpu, threadsperblock=128)
[36]:
%%timeit
# time stock scipy / cupy
result_stock = stock_interpolator(xinterp, x, y)
236 ms ± 2.21 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
[37]:
%%timeit
#time cudakima
result_cudakima = cudakima_interpolator(xinterp, x, y)
2.83 ms ± 13.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[38]:
result_cudakima = to_cpu(cudakima_interpolator(xinterp, x, y))
result_stock = to_cpu(stock_interpolator(xinterp, x, y))

realization = np.random.randint(0, nrealizations)
fig = plt.figure(figsize=(12, 6))

plt.subplot(121)
plt.plot(to_cpu(xinterp), result_cudakima[realization], c='k', ls = '-', lw=2, label='Cudakima')
plt.plot(to_cpu(xinterp), result_stock[realization], c='red', ls = '--', lw=2, label='Stock scipy/cupy', alpha=0.5)
plt.scatter(to_cpu(x)[realization], to_cpu(y)[realization], marker='x', c='k', label='Knots')
plt.legend()
plt.subplot(122)
plt.plot(to_cpu(xinterp), result_cudakima[realization] - result_stock[realization], c='k', ls = '-', lw=2, label='difference')
plt.legend()
plt.show()
../_images/notebooks_tutorial_30_0.png

Multidimensional new arrays

[39]:
from cudakima import AkimaInterpolant1DMultiDim
[ ]:

[56]:
linear_multidim_interp = AkimaInterpolant1DMultiDim(sanitize=False, use_gpu=use_gpu, threadsperblock=512, order='linear')
cubic_multidim_interp = AkimaInterpolant1DMultiDim(sanitize=False, use_gpu=use_gpu, threadsperblock=512, order='cubic')
[57]:
# generate some data
batch_shape = (100, 5)
max_points_per_group = 100
full_shape = batch_shape + (max_points_per_group,)

n_interp = 10000

x_data = xp.full(full_shape, xp.nan)
y_data = xp.full(full_shape, xp.nan)
x_new_multidim = xp.full(batch_shape + (n_interp,), xp.nan)

for idx in xp.ndindex(batch_shape):
    # Random group size (at least 4 for Akima)
    group_size = int(xp.random.randint(4, max_points_per_group + 1))

    # Create smooth test function with some noise
    tmp = xp.random.uniform(0, 10, group_size)
    x_group = xp.sort(tmp)
    y_group = xp.sin(x_group) + 0.1 * x_group + 0.05 * xp.random.randn(group_size)

    x_data[idx][:group_size] = x_group
    y_data[idx][:group_size] = y_group

    # Create interpolation points for this group (within data range)
    x_min, x_max = x_group[0], x_group[-1]
    x_new_multidim[idx] = xp.linspace(x_min, x_max, n_interp)
[58]:
for _ in [x_data, y_data, x_new_multidim]:
    print(_.shape)
(100, 5, 100)
(100, 5, 100)
(100, 5, 10000)
[61]:
linear_result = linear_multidim_interp(x_new_multidim, x_data, y_data)
cubic_result = cubic_multidim_interp(x_new_multidim, x_data, y_data)
[64]:
%%timeit

_ = cubic_multidim_interp(x_new_multidim, x_data, y_data)
2.89 ms ± 14.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[65]:
# visualize a few results
N1 = 3
N2 = 3
Nsubplots = 101 + N2 * 10

for i in range(N1):
    plt.figure(figsize=(15, 10))
    for j in range(N2):
        plt.subplot(Nsubplots + j)
        idx = (i, j)
        for result, label in zip([linear_result, cubic_result], ['Linear Akima', 'Cubic Akima']):
            plt.plot(to_cpu(x_new_multidim[idx]), to_cpu(result[idx]), ls = '-', lw=1, label=label)
        valid = ~xp.isnan(x_data[idx]) & ~xp.isnan(y_data[idx])
        plt.scatter(to_cpu(x_data[idx][valid]), to_cpu(y_data[idx][valid]), marker='x', c='k', label='Knots')
        plt.title(f'Indices {idx}, {valid.sum()} points')
        if i == 0 and j == 0:
            plt.legend()

../_images/notebooks_tutorial_39_0.png
../_images/notebooks_tutorial_39_1.png
../_images/notebooks_tutorial_39_2.png
[ ]: