1

I need to recode an equivalent of the NumPy interp function. In order to do that, I thought of building a pairwise distance matrix, and then using this matrix to define "weights" that I use to multiply the target values.

def interp_diff(x, xp, fp):
    dxp = np.diff(xp)
    dxp = np.concatenate(([dxp[0]], dxp))

    pairwise_distance = np.abs(x[:, np.newaxis] - xp)  # Calculate the absolute differences between x and xp

    # Add a small epsilon to pairwise distances to avoid division by zero
    epsilon = 1e-8
    pairwise_distance += epsilon

    weights = 1 / pairwise_distance  # Calculate the weights based on the differences

    # Normalize the weights
    weights /= np.sum(weights, axis=1)[:, np.newaxis]

    # Apply hardness threshold to the weights
    weights = np.where(weights > 0, weights, 0)

    return np.dot(weights, fp)

This produces expected results when xp values are placed on a regular grid, but it does not work if xp values are not evenly spaced. How can I make this work for any spacing of xp?

The constraint I have is that I can't use index related methods (argsort, argwhere, searchsorted, etc...). Which is what makes it a bit challenging.

Example usage on a regular grid:

x_np = np.linspace(0, 5, 4)
y_np = np.sin(x_np)
x_i = x_np
x_i = np.linspace(0, 5, 10)
y_i = interp_diff(x_i, xp=x_np, fp=y_np)
ax = plt.figure().gca()
ax.scatter(x_np, y_np)
ax.scatter(x_i, y_i, marker='+', s=30, alpha=0.7)
ax.plot(x_i, y_i)
plt.show()

Produces the expected result:

Enter image description here

However, switching to non evenly spaced grid:

def sinspace(start, stop, num):

    ones = 0 * start + 1
    return start + (stop - start) * (1 - np.cos(np.linspace(
        0 * ones,
        np.pi / 2 * ones,
        num
    )))
x_np = sinspace(0, 5, 4)
y_np = np.sin(x_np)
x_i = x_np
x_i = np.linspace(0, 5, 10)
y_i = interp_diff(x_i, xp=x_np, fp=y_np)
ax = plt.figure().gca()
ax.scatter(x_np, y_np)
ax.scatter(x_i, y_i, marker='+', s=30, alpha=0.7)
ax.plot(x_i, y_i)
plt.show()

This is what I get:

Enter image description here

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
carlitador
  • 47
  • 9
  • The constraint of not use index related methods is hard to bypass, idk if it's even doable, maybe working on why you can't use index related methods can be a idea. if you can explain why, I would love to think about it. – lot May 25 '23 at 13:55
  • Explaining why it quite long and I amnot sure it is necessary to answer the question. In short, I am using an adaptation of numpy library (casadi.org) that is used in an automatic differentiation context - that does not support index related methods. So in order to be able to perform linear interpolation in this context, I need to find a way with the remaining equivalent numpy functions I have. I am asking the questions with numpy because it's a well known library (this is not the case of casadi, but they share a lot of common functions) – carlitador May 26 '23 at 22:30
  • The code you posted doesn't seem to produce the output you say it does. Specifically where you say "produces expected result". It doesn't. – David May 27 '23 at 16:11
  • I see strange extra code. 1. The `dxp` variable in interp_diff is unused. 2. In the regular grid example you have `x_i = x_np` immediately followed by another assignment to the same variable. As far as I can tell both examples are code that does nothing. Should they be removed? – Andrey Bienkowski Jun 11 '23 at 06:36

3 Answers3

0

I'm unable to replicate your results with a slightly modified version of the code you supplied:

import numpy
from matplotlib import pyplot

def interp_diff(x, xp, fp):
    dxp = numpy.diff(xp)
    dxp = numpy.concatenate(([dxp[0]], dxp))

    pairwise_distance = numpy.abs(x[:, numpy.newaxis] - xp)  # Calculate the absolute differences between x and xp

    # Add a small epsilon to pairwise distances to avoid division by zero
    epsilon = 1e-8
    pairwise_distance += epsilon

    weights = 1 / pairwise_distance  # Calculate the weights based on the differences

    # Normalize the weights
    weights /= numpy.sum(weights, axis=1)[:, numpy.newaxis]

    # Apply hardness threshold to the weights
    weights = numpy.where(weights > 0, weights, 0)

    return numpy.dot(weights, fp)

# 1 : 1 grid
x_np = numpy.linspace(0,5,4)
y_np = numpy.sin(x_np)
x_i = x_np
y_i = interp_diff(x_i,xp=x_np,fp=y_np)
ax = pyplot.figure().gca()
ax.scatter(x_np,y_np)
ax.scatter(x_i,y_i,marker='+',s=30,alpha=0.7)
ax.plot(x_i,y_i)
pyplot.show()

Evenly spaced grid plot

# 1 : 2.5 grid
x_np = numpy.linspace(0,5,4)
y_np = numpy.sin(x_np)
x_i = numpy.linspace(0,5,10)
y_i = interp_diff(x_i,xp=x_np,fp=y_np)
ax = pyplot.figure().gca()
ax.scatter(x_np,y_np)
ax.scatter(x_i,y_i,marker='+',s=30,alpha=0.7)
ax.plot(x_i,y_i)
pyplot.show()

Unevenly spaced grid plot

Which version of the numpy and matplotlib libraries are you using?

Bill Horvath
  • 1,336
  • 9
  • 24
  • This is because you use a regular grid in both cases. I just edited and added the code for non evenly space grid – carlitador May 23 '23 at 09:08
0

The interp_diff function to work with unevenly spaced xp values, i have chnaged with np.subtract.outer function to calculate the pairwise differences between x and xp without relying on index-related methods.

import numpy as np

import matplotlib.pyplot as plt

def interp_diff(x, xp, fp):
    dxp = np.diff(xp)
    dxp = np.concatenate(([dxp[0]], dxp))

pairwise_distance = np.abs(np.subtract.outer(x, xp))  # Calculate the absolute differences between x and xp

# Add a small epsilon to pairwise distances to avoid division by zero
epsilon = 1e-8
pairwise_distance += epsilon

weights = 1 / pairwise_distance  # Calculate the weights based on the differences

# Normalize weights
weights /= np.sum(weights, axis=1)[:, np.newaxis]

# Apply hardness threshold to the weights
weights = np.where(weights > 0, weights, 0)

return np.dot(weights, fp)

x_np = np.linspace(0, 5, 4)
y_np = np.sin(x_np)
x_i = np.linspace(0, 5, 10)
y_i = interp_diff(x_i, xp=x_np, fp=y_np)

ax = plt.figure().gca()
ax.scatter(x_np, y_np)
ax.scatter(x_i, y_i, marker='+', s=30, alpha=0.7)
ax.plot(x_i, y_i)
plt.show()

output image:

0

Not sure this will do exactly what you want, since it uses a few boolean arrays which may cause problems with your gradients. But it produces the correct result (comparing to Numpy) and doesn't use any complicated indexing:

def interp_diff(x, xp, fp):
    diff_filter = np.diff(x[:, np.newaxis] >= xp, axis=-1)
    prev_filter = np.concatenate((diff_filter, ~np.any(
        diff_filter, axis=-1, keepdims=True)), axis=-1)
    next_filter = np.concatenate(
        (np.broadcast_to(False, (diff_filter.shape[0], 1)), diff_filter), axis=-1)

    epsilon = 1e-8
    pairwise_distance = xp - x[:, np.newaxis]
    prev_dist = np.where(prev_filter, -pairwise_distance, 0)
    next_dist = np.where(next_filter, pairwise_distance, 0)
    progress = prev_dist / \
        ((prev_dist + next_dist).sum(axis=-1) + epsilon)[..., np.newaxis]

    return np.sum(np.where(prev_filter, fp, 0) + np.concatenate((np.diff(fp), [0])) * progress, axis=-1)

It works by abusing np.diff on boolean arrays to get the previous and the subsequent point in xp. It then calculates how far between the two points the new point is and uses np.diff again to offset the new point from the previous closest point.

Output of interp_diff function on sample data

David
  • 1,688
  • 1
  • 11
  • 21