Saturday, 19 October 2013

Spline Interpolation Excel Macro

Introduction

This is my attempt at producing an Excel function to interpolate data using splines. A good introduction to spline interpolation is, once again, given by wikipedia.

The algorithm is taken from the book 'Numerical Analysis, 7th Edition, Burden and Faires'. The macro seems to give the same answers as the example in the book so I'm fairly confident that it mostly works.

The macro takes:
  1. The x coordinates of a set of points
  2. The y coordinates of a set of points
  3. The x coordinates of the points that you want to interpolate
  4. An optional parameter of the gradient at point zero
  5. An optional parameter of the gradient at point n
If the optional parameters are missing then free interpolation is performed, otherwise a clamped interpolation is done. The macro outputs the interpolated Y coordinates.


A check is done to ensure that the x coordinates increase.

Also you will need to create a class module called Point2D. The code for this class is shown below.

The Code

Public Function SplineInterpolation(dataRangeX As range, dataRangeY As range, interpX As range, Optional gradient0 As Variant, Optional gradientN As Variant) As Variant
    'Spline interpolation - algorithm from Numerical Analysis 7th Edition, Burden & Faires
    
    Dim Data() As Point2D
    Dim h() As Double
    Dim alpha() As Double
    Dim l() As Double
    Dim mu() As Double
    Dim z() As Double
    Dim i As Integer
    Dim j As Integer
    Dim numPoints As Integer
    Dim c() As Double
    Dim d() As Double
    Dim b() As Double
    Dim OutputRows As Long
    Dim OutputCols As Long
    Dim output() As Double
    Dim vertData As Boolean
    Dim vertOutput As Boolean
    Dim vertInterp As Boolean
    Dim x1 As Double
    Dim y1 As Double
    Dim fpo As Double
    Dim fpn As Double
    Dim jStop As Integer
    
    'Set up output variables
    With Application.Caller
        OutputRows = .Rows.Count
        OutputCols = .Columns.Count
    End With
    ReDim output(1 To OutputRows, 1 To OutputCols)
    
    'Is the output range verticle or horizontal
    If OutputRows > OutputCols Then
        vertOutput = True
    Else
        vertOutput = False
    End If
    
    'Is the data range verticle or horizontal
    If dataRangeX.Rows.Count > dataRangeX.Columns.Count Then
        vertData = True
    Else
        vertData = False
    End If
    
    'Is the interpolation range verticle or horizontal
    If interpX.Rows.Count > interpX.Columns.Count Then
        vertInterp = True
    Else
        vertInterp = False
    End If
    
    'Is the X and Y area the same?
    If dataRangeX.Rows.Count <> dataRangeY.Rows.Count And dataRangeX.Columns.Count <> dataRangeY.Columns.Count Then
        SplineInterpolation = "#VALUE!"
        Exit Function
    End If
    
    'Check for gradient variables
    If IsMissing(gradient0) Then
        'Natural spline interpolation
        
        If vertData = True Then
            numPoints = dataRangeX.Rows.Count - 1
    
            ReDim Data(dataRangeX.Rows.Count)
            ReDim h(dataRangeX.Rows.Count - 1)
            ReDim alpha(dataRangeX.Rows.Count - 1)
            ReDim l(dataRangeX.Rows.Count)
            ReDim mu(dataRangeX.Rows.Count)
            ReDim z(dataRangeX.Rows.Count)
            ReDim b(numPoints)
            ReDim c(numPoints)
            ReDim d(numPoints)
    
            For i = 0 To numPoints
                Set Data(i) = New Point2D
                Data(i).SetX (dataRangeX(i + 1, 1).Value)
                Data(i).SetY (dataRangeY(i + 1, 1).Value)
            Next
        Else
            numPoints = dataRangeX.Columns.Count - 1
    
            ReDim Data(dataRangeX.Columns.Count)
            ReDim h(dataRangeX.Columns.Count - 1)
            ReDim alpha(dataRangeX.Columns.Count - 1)
            ReDim l(dataRangeX.Columns.Count)
            ReDim mu(dataRangeX.Columns.Count)
            ReDim z(dataRangeX.Columns.Count)
            ReDim b(numPoints)
            ReDim c(numPoints)
            ReDim d(numPoints)
    
            For i = 0 To numPoints
                Set Data(i) = New Point2D
                Data(i).SetX (dataRangeX(1, i + 1).Value)
                Data(i).SetY (dataRangeY(1, i + 1).Value)
            Next
        End If
    
        'Does x only increase?
        For i = 0 To numPoints - 1
            If Data(i).GetX() >= Data(i + 1).GetX() Then
                'If not then exit
                SplineInterpolation = "#VALUE!"
                Exit Function
            End If
        Next
    
        For i = 0 To numPoints - 1
            h(i) = Data(i + 1).GetX() - Data(i).GetX()
        Next
    
        For i = 1 To numPoints - 1
            alpha(i) = (3 / h(i)) * (Data(i + 1).GetY() - Data(i).GetY()) - (3 / h(i - 1)) * (Data(i).GetY() - Data(i - 1).GetY())
        Next
    
        l(0) = 1
        mu(0) = 0
        z(0) = 0
    
        For i = 1 To numPoints - 1
            l(i) = 2 * (Data(i + 1).GetX() - Data(i - 1).GetX()) - h(i - 1) * mu(i - 1)
            mu(i) = h(i) / l(i)
            z(i) = (alpha(i) - h(i - 1) * z(i - 1)) / l(i)
        Next
    
        l(numPoints) = 1
        z(numPoints) = 0
        c(numPoints) = 0
    
        For j = numPoints - 1 To 0 Step -1
            c(j) = z(j) - mu(j) * c(j + 1)
            b(j) = (Data(j + 1).GetY() - Data(j).GetY()) / h(j) - h(j) * (c(j + 1) + 2 * c(j)) / 3
            d(j) = (c(j + 1) - c(j)) / (3 * h(j))
        Next
    Else
        'Clamped spline interpolation
        
        If vertData = True Then
            numPoints = dataRangeX.Rows.Count - 1
    
            ReDim Data(dataRangeX.Rows.Count)
            ReDim h(dataRangeX.Rows.Count - 1)
            ReDim alpha(dataRangeX.Rows.Count)
            ReDim l(dataRangeX.Rows.Count)
            ReDim mu(dataRangeX.Rows.Count)
            ReDim z(dataRangeX.Rows.Count)
            ReDim b(numPoints)
            ReDim c(numPoints)
            ReDim d(numPoints)
    
            For i = 0 To numPoints
                Set Data(i) = New Point2D
                Data(i).SetX (dataRangeX(i + 1, 1).Value)
                Data(i).SetY (dataRangeY(i + 1, 1).Value)
            Next
        Else
            numPoints = dataRangeX.Columns.Count - 1
    
            ReDim Data(dataRangeX.Columns.Count)
            ReDim h(dataRangeX.Columns.Count - 1)
            ReDim alpha(dataRangeX.Columns.Count)
            ReDim l(dataRangeX.Columns.Count)
            ReDim mu(dataRangeX.Columns.Count)
            ReDim z(dataRangeX.Columns.Count)
            ReDim b(numPoints)
            ReDim c(numPoints)
            ReDim d(numPoints)
    
            For i = 0 To numPoints
                Set Data(i) = New Point2D
                Data(i).SetX (dataRangeX(1, i + 1).Value)
                Data(i).SetY (dataRangeY(1, i + 1).Value)
            Next
        End If
        
        'Does x only increase?
        For i = 0 To numPoints - 1
            If Data(i).GetX() >= Data(i + 1).GetX() Then
                'If not then exit
                SplineInterpolation = "#VALUE!"
                Exit Function
            End If
        Next
    
        fpo = gradient0
        fpn = gradientN
    
        For i = 0 To numPoints - 1
            h(i) = Data(i + 1).GetX() - Data(i).GetX()
        Next
    
        alpha(0) = 3 * (Data(1).GetY() - Data(0).GetY()) / h(0) - 3 * fpo
        alpha(numPoints) = 3 * fpn - 3 * (Data(numPoints).GetY() - Data(numPoints - 1).GetY()) / h(numPoints - 1)
    
        For i = 1 To numPoints - 1
            alpha(i) = (3 / h(i)) * (Data(i + 1).GetY() - Data(i).GetY()) - (3 / h(i - 1)) * (Data(i).GetY() - Data(i - 1).GetY())
        Next
    
        l(0) = 2 * h(0)
        mu(0) = 0.5
        z(0) = alpha(0) / l(0)
    
        For i = 1 To numPoints - 1
            l(i) = 2 * (Data(i + 1).GetX() - Data(i - 1).GetX()) - h(i - 1) * mu(i - 1)
            mu(i) = h(i) / l(i)
            z(i) = (alpha(i) - h(i - 1) * z(i - 1)) / l(i)
        Next
    
        l(numPoints) = h(numPoints - 1) * (2 - mu(numPoints - 1))
        z(numPoints) = (alpha(numPoints) - h(numPoints - 1) * z(numPoints - 1)) / l(numPoints)
        c(numPoints) = z(numPoints)
    
        For j = numPoints - 1 To 0 Step -1
            c(j) = z(j) - mu(j) * c(j + 1)
            b(j) = (Data(j + 1).GetY() - Data(j).GetY()) / h(j) - h(j) * (c(j + 1) + 2 * c(j)) / 3
            d(j) = (c(j + 1) - c(j)) / (3 * h(j))
        Next
    End If
    
    'Create output data
    If vertInterp = True Then
        For i = 1 To interpX.Rows.Count
            For j = 0 To numPoints - 1
                If interpX(i, 1).Value >= Data(j).GetX() Then
                    x1 = Data(j).GetX()
                    y1 = Data(j).GetY()
                    jStop = j
                    'Exit For
                End If
            Next
            If vertOutput = True Then
                output(i, 1) = y1 + b(jStop) * (interpX(i, 1).Value - x1) + c(jStop) * (interpX(i, 1).Value - x1) ^ 2 + d(jStop) * (interpX(i, 1).Value - x1) ^ 3
            Else
                output(1, i) = y1 + b(jStop) * (interpX(i, 1).Value - x1) + c(jStop) * (interpX(i, 1).Value - x1) ^ 2 + d(jStop) * (interpX(i, 1).Value - x1) ^ 3
            End If
        Next
    Else
        For i = 1 To interpX.Columns.Count
            For j = 0 To numPoints - 1
                If interpX(1, i).Value >= Data(j).GetX() Then
                    x1 = Data(j).GetX()
                    y1 = Data(j).GetY()
                    jStop = j
                    'Exit For
                End If
            Next
            If vertOutput = True Then
                output(i, 1) = y1 + b(jStop) * (interpX(1, i).Value - x1) + c(jStop) * (interpX(1, i).Value - x1) ^ 2 + d(jStop) * (interpX(1, i).Value - x1) ^ 3
            Else
                output(1, i) = y1 + b(jStop) * (interpX(1, i).Value - x1) + c(jStop) * (interpX(1, i).Value - x1) ^ 2 + d(jStop) * (interpX(1, i).Value - x1) ^ 3
            End If
        Next
    End If
    
    SplineInterpolation = output
    
End Function

An Example

Using these points:

X Y
1 0
2 3
3 2
4 7
5 5
6 3

and interpolating for these points:

X Interpolation
1 0
1.3 1.35
1.7 2.68
2 3
2.3 2.59
2.7 1.84
3 2
3.3 3.29
3.7 5.71
4 7
4.3 7.135
4.7 6.07
5 5
5.3 4.17
5.7 3.43
gave the interpolation column.

Plotting this in Excel and cheating slightly by adding a smoothed line gives:

Excel interpolates slightly differently, although it looks like it uses splines. Adding more points to the interpolated points column would have made the line smoother without having to add the line to the chart.

If there are any errors, omissions, improvements that can be made, etc. then please comment.

Update

I forgot to post details of the custom class I made to hold details of the x,y data.

It's not particularly necessary to have done it this way but I was re-using a class I'd created for a clustering macro.

Anyway, here is the code:

Option Explicit

Private x As Double
Private y As Double
Private cluster As Integer

Public Sub SetX(temp As Double)
    x = temp
End Sub

Public Sub SetY(temp As Double)
    y = temp
End Sub

Public Function GetX() As Double
    GetX = x
End Function

Public Function GetY() As Double
    GetY = y
End Function

Public Sub SetC(temp As Integer)
    cluster = temp
End Sub

Public Function GetC() As Integer
    GetC = cluster
End Function

Apologies for any confusion.

4 comments:

  1. Hi Sociolist,
    tried your code but there is a problem about the Dim Data() As Point2D
    What kind of dim. is Point2D? Array, Integer?
    Did you run this in Excel 2013 VBA?! Don't get it compiled...

    Thanks

    ReplyDelete
    Replies
    1. Sorry about that. I forgot to add the class I'd created to hold the x and y data. I've added it now and updated the introduction.

      Delete
  2. Hi! This looks like exactly what I need for my purposes (interpolating from a calibration curve for scientific equipment). However, I'm having difficulty with the Point2D class. (I apologize - it's been over a decade since I've done anything with VBA, so my skills are, well, basic.) I've put the second set of code into an inserted class module, but it doesn't seem to accomplish defining the Point2D class. Is there some convention for naming the class module that I should know, or is there a line of code that needs to be added to define the class? Thanks for your work and your help. (BTW using Excel 2013 (the add-in to 2003 I'd used for this purpose no longer compiles)). Ian

    ReplyDelete
    Replies
    1. I don't think so. It should be as simple as adding the class module and renaming to Point2D.

      If this doesn't work you can always replace the Point2D array data() with two double arrays datax() and datay(). I don't think it would make the code any more difficult. To be honest, it might make it a lot easier. Given the problems it seems to be causing I'll probably rewrite it without the custom class.

      Delete