Olov Lindstrom
Olov Lindstrom

Reputation: 13

Simple linear interpolation in Swift

I'm converting a script from Python to Swift. In the original Python code I'm doing a simple interpolation between two lists, using Numpy interp in the following format:

>>> xp = [1, 2, 3]
>>> fp = [3, 2, 0]
>>> np.interp(2.5, xp, fp)
1.0

This is from the Numpy documentation, not my actual code, but this is exactly what I require and have used in the Python script. In short, I have the x and y axes as arrays, and I want to do one-dimensional interpolation to evaluate an x coordinate.

All this works great with Python/Numpy, but I cannot get it to work in Swift. The Accelerate vDSP framework has linearInterpolate(VectorA, VectorB, using: X), but it returns an array rather than a single value.

var y = vDSP.linearInterpolate([1.0, 2.0, 3.0], [3.0, 2.0, 0.0], using: 2.5)

Results in:

[6, 2, -4.5]

This should be very simple, and I guess one option is to just write my own function. But surely there must be an easy one-liner way to do this? I'm new to Swift and it feels like I'm missing something very obvious.

Upvotes: 1

Views: 873

Answers (1)

Rob
Rob

Reputation: 437882

That rendition of linearInterpolate(_:_:using:) is designed to solve a very different problem:

For example, the following code creates two arrays, vectorA and vectorB, that contain sine waves:

let n = 1024

let vectorA: [Float] = (0 ... n).map {
    return 2 + sin(Float($0) * 0.07)
}

let vectorB: [Float] = (0 ... n).map {
    return -2 + sin(Float($0) * 0.03)
}

Use linearInterpolate(_:_:using:) with an interpolation constant of 0.5 to generate a new vector that's the average of the two sine waves:

let result = vDSP.linearInterpolate(vectorA, vectorB,
                                    using: 0.5)

The following figure visualizes the two source vectors: the blue lines at the top and bottom, and the interpolation result: the red line in the center:

enter image description here


The linearInterpolate(elementsOf:using:) does something closer to what you are looking for (though you only supply the y values, and it assumes integer x index values).

So consider:

>>> xp = [0, 1, 2]
>>> fp = [3, 2, 0]
>>> np.interp(1.5, xp, fp)
1.0

The equivalent vDSP call might be:

let z = vDSP.linearInterpolate(elementsOf: [3.0, 2.0, 0.0], using: [1.5])

[1.0]

Now that employs implicit x values that are a zero-based index (e.g. 0, 1, 2, etc.), thus I tweaked your Python example to use zero-based index values for x.

So, this vDSP solution is a special case of interpolation between neighboring values, but I don't know whether that is sufficient, or whether you needed a more general solution (for arbitrary x values in your input). If so, yes, you might just have to write your own little helper function.


Here is an example of a more general solution that retrieves for only one x value:

func linearInterpolate<S, T>(_ sequenceX: S, _ sequenceY: S, at index: T) -> T? where S: Sequence, S.Element == T, T: FloatingPoint {
    var xIterator = sequenceX.makeIterator()
    var yIterator = sequenceY.makeIterator()

    guard
        var previousX = xIterator.next(),
        var previousY = yIterator.next()
    else {
        return nil
    }

    while true {
        if index == previousX { return previousY }

        guard
            let x = xIterator.next(),
            let y = yIterator.next()
        else {
            return nil
        }

        if (index > previousX && index < x) || (index > x && index < previousX) {
            let percent = (index - previousX) / (x - previousX)
            return previousY + percent * (y - previousY)
        }

        previousX = x
        previousY = y
    }
}

and

let xp: [Double] = [1, 2, 3]
let fp: [Double] = [3, 2, 0]

if let y = linearInterpolate(xp, fp, at: 2.5) {
    print(y) // 1.0
}

Upvotes: 1

Related Questions