github.com/gopherd/gonum@v0.0.4/interp/cubic.go (about)

     1  // Copyright ©2020 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package interp
     6  
     7  import (
     8  	"math"
     9  
    10  	"github.com/gopherd/gonum/mat"
    11  )
    12  
    13  // PiecewiseCubic is a piecewise cubic 1-dimensional interpolator with
    14  // continuous value and first derivative.
    15  type PiecewiseCubic struct {
    16  	// Interpolated X values.
    17  	xs []float64
    18  
    19  	// Coefficients of interpolating cubic polynomials, with
    20  	// len(xs) - 1 rows and 4 columns. The interpolated value
    21  	// for xs[i] <= x < xs[i + 1] is defined as
    22  	//   sum_{k = 0}^3 coeffs.At(i, k) * (x - xs[i])^k
    23  	// To guarantee left-continuity, coeffs.At(i, 0) == ys[i].
    24  	coeffs mat.Dense
    25  
    26  	// Last interpolated Y value, corresponding to xs[len(xs) - 1].
    27  	lastY float64
    28  
    29  	// Last interpolated dY/dX value, corresponding to xs[len(xs) - 1].
    30  	lastDyDx float64
    31  }
    32  
    33  // Predict returns the interpolation value at x.
    34  func (pc *PiecewiseCubic) Predict(x float64) float64 {
    35  	i := findSegment(pc.xs, x)
    36  	if i < 0 {
    37  		return pc.coeffs.At(0, 0)
    38  	}
    39  	m := len(pc.xs) - 1
    40  	if x == pc.xs[i] {
    41  		if i < m {
    42  			return pc.coeffs.At(i, 0)
    43  		}
    44  		return pc.lastY
    45  	}
    46  	if i == m {
    47  		return pc.lastY
    48  	}
    49  	dx := x - pc.xs[i]
    50  	a := pc.coeffs.RawRowView(i)
    51  	return ((a[3]*dx+a[2])*dx+a[1])*dx + a[0]
    52  }
    53  
    54  // PredictDerivative returns the predicted derivative at x.
    55  func (pc *PiecewiseCubic) PredictDerivative(x float64) float64 {
    56  	i := findSegment(pc.xs, x)
    57  	if i < 0 {
    58  		return pc.coeffs.At(0, 1)
    59  	}
    60  	m := len(pc.xs) - 1
    61  	if x == pc.xs[i] {
    62  		if i < m {
    63  			return pc.coeffs.At(i, 1)
    64  		}
    65  		return pc.lastDyDx
    66  	}
    67  	if i == m {
    68  		return pc.lastDyDx
    69  	}
    70  	dx := x - pc.xs[i]
    71  	a := pc.coeffs.RawRowView(i)
    72  	return (3*a[3]*dx+2*a[2])*dx + a[1]
    73  }
    74  
    75  // FitWithDerivatives fits a piecewise cubic predictor to (X, Y, dY/dX) value
    76  // triples provided as three slices.
    77  // It panics if len(xs) < 2, elements of xs are not strictly increasing,
    78  // len(xs) != len(ys) or len(xs) != len(dydxs).
    79  func (pc *PiecewiseCubic) FitWithDerivatives(xs, ys, dydxs []float64) {
    80  	n := len(xs)
    81  	if len(ys) != n {
    82  		panic(differentLengths)
    83  	}
    84  	if len(dydxs) != n {
    85  		panic(differentLengths)
    86  	}
    87  	if n < 2 {
    88  		panic(tooFewPoints)
    89  	}
    90  	m := n - 1
    91  	pc.coeffs.Reset()
    92  	pc.coeffs.ReuseAs(m, 4)
    93  	for i := 0; i < m; i++ {
    94  		dx := xs[i+1] - xs[i]
    95  		if dx <= 0 {
    96  			panic(xsNotStrictlyIncreasing)
    97  		}
    98  		dy := ys[i+1] - ys[i]
    99  		// a_0
   100  		pc.coeffs.Set(i, 0, ys[i])
   101  		// a_1
   102  		pc.coeffs.Set(i, 1, dydxs[i])
   103  		// Solve a linear equation system for a_2 and a_3.
   104  		pc.coeffs.Set(i, 2, (3*dy-(2*dydxs[i]+dydxs[i+1])*dx)/dx/dx)
   105  		pc.coeffs.Set(i, 3, (-2*dy+(dydxs[i]+dydxs[i+1])*dx)/dx/dx/dx)
   106  	}
   107  	pc.xs = append(pc.xs[:0], xs...)
   108  	pc.lastY = ys[m]
   109  	pc.lastDyDx = dydxs[m]
   110  }
   111  
   112  // AkimaSpline is a piecewise cubic 1-dimensional interpolator with
   113  // continuous value and first derivative, which can be fitted to (X, Y)
   114  // value pairs without providing derivatives.
   115  // See https://www.iue.tuwien.ac.at/phd/rottinger/node60.html for more details.
   116  type AkimaSpline struct {
   117  	cubic PiecewiseCubic
   118  }
   119  
   120  // Predict returns the interpolation value at x.
   121  func (as *AkimaSpline) Predict(x float64) float64 {
   122  	return as.cubic.Predict(x)
   123  }
   124  
   125  // PredictDerivative returns the predicted derivative at x.
   126  func (as *AkimaSpline) PredictDerivative(x float64) float64 {
   127  	return as.cubic.PredictDerivative(x)
   128  }
   129  
   130  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   131  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   132  // or len(xs) != len(ys). Always returns nil.
   133  func (as *AkimaSpline) Fit(xs, ys []float64) error {
   134  	n := len(xs)
   135  	if len(ys) != n {
   136  		panic(differentLengths)
   137  	}
   138  	dydxs := make([]float64, n)
   139  
   140  	if n == 2 {
   141  		dx := xs[1] - xs[0]
   142  		slope := (ys[1] - ys[0]) / dx
   143  		dydxs[0] = slope
   144  		dydxs[1] = slope
   145  		as.cubic.FitWithDerivatives(xs, ys, dydxs)
   146  		return nil
   147  	}
   148  	slopes := akimaSlopes(xs, ys)
   149  	for i := 0; i < n; i++ {
   150  		wLeft, wRight := akimaWeights(slopes, i)
   151  		dydxs[i] = akimaWeightedAverage(slopes[i+1], slopes[i+2], wLeft, wRight)
   152  	}
   153  	as.cubic.FitWithDerivatives(xs, ys, dydxs)
   154  	return nil
   155  }
   156  
   157  // akimaSlopes returns slopes for Akima spline method, including the approximations
   158  // of slopes outside the data range (two on each side).
   159  // It panics if len(xs) <= 2, elements of xs are not strictly increasing
   160  // or len(xs) != len(ys).
   161  func akimaSlopes(xs, ys []float64) []float64 {
   162  	n := len(xs)
   163  	if n <= 2 {
   164  		panic(tooFewPoints)
   165  	}
   166  	if len(ys) != n {
   167  		panic(differentLengths)
   168  	}
   169  	m := n + 3
   170  	slopes := make([]float64, m)
   171  	for i := 2; i < m-2; i++ {
   172  		dx := xs[i-1] - xs[i-2]
   173  		if dx <= 0 {
   174  			panic(xsNotStrictlyIncreasing)
   175  		}
   176  		slopes[i] = (ys[i-1] - ys[i-2]) / dx
   177  	}
   178  	slopes[0] = 3*slopes[2] - 2*slopes[3]
   179  	slopes[1] = 2*slopes[2] - slopes[3]
   180  	slopes[m-2] = 2*slopes[m-3] - slopes[m-4]
   181  	slopes[m-1] = 3*slopes[m-3] - 2*slopes[m-4]
   182  	return slopes
   183  }
   184  
   185  // akimaWeightedAverage returns (v1 * w1 + v2 * w2) / (w1 + w2) for w1, w2 >= 0 (not checked).
   186  // If w1 == w2 == 0, it returns a simple average of v1 and v2.
   187  func akimaWeightedAverage(v1, v2, w1, w2 float64) float64 {
   188  	w := w1 + w2
   189  	if w > 0 {
   190  		return (v1*w1 + v2*w2) / w
   191  	}
   192  	return 0.5*v1 + 0.5*v2
   193  }
   194  
   195  // akimaWeights returns the left and right weight for approximating
   196  // the i-th derivative with neighbouring slopes.
   197  func akimaWeights(slopes []float64, i int) (float64, float64) {
   198  	wLeft := math.Abs(slopes[i+2] - slopes[i+3])
   199  	wRight := math.Abs(slopes[i+1] - slopes[i])
   200  	return wLeft, wRight
   201  }
   202  
   203  // FritschButland is a piecewise cubic 1-dimensional interpolator with
   204  // continuous value and first derivative, which can be fitted to (X, Y)
   205  // value pairs without providing derivatives.
   206  // It is monotone, local and produces a C^1 curve. Its downside is that
   207  // exhibits high tension, flattening out unnaturally the interpolated
   208  // curve between the nodes.
   209  // See Fritsch, F. N. and Butland, J., "A method for constructing local
   210  // monotone piecewise cubic interpolants" (1984), SIAM J. Sci. Statist.
   211  // Comput., 5(2), pp. 300-304.
   212  type FritschButland struct {
   213  	cubic PiecewiseCubic
   214  }
   215  
   216  // Predict returns the interpolation value at x.
   217  func (fb *FritschButland) Predict(x float64) float64 {
   218  	return fb.cubic.Predict(x)
   219  }
   220  
   221  // PredictDerivative returns the predicted derivative at x.
   222  func (fb *FritschButland) PredictDerivative(x float64) float64 {
   223  	return fb.cubic.PredictDerivative(x)
   224  }
   225  
   226  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   227  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   228  // or len(xs) != len(ys). Always returns nil.
   229  func (fb *FritschButland) Fit(xs, ys []float64) error {
   230  	n := len(xs)
   231  	if n < 2 {
   232  		panic(tooFewPoints)
   233  	}
   234  	if len(ys) != n {
   235  		panic(differentLengths)
   236  	}
   237  	dydxs := make([]float64, n)
   238  
   239  	if n == 2 {
   240  		dx := xs[1] - xs[0]
   241  		slope := (ys[1] - ys[0]) / dx
   242  		dydxs[0] = slope
   243  		dydxs[1] = slope
   244  		fb.cubic.FitWithDerivatives(xs, ys, dydxs)
   245  		return nil
   246  	}
   247  	slopes := calculateSlopes(xs, ys)
   248  	m := len(slopes)
   249  	prevSlope := slopes[0]
   250  	for i := 1; i < m; i++ {
   251  		slope := slopes[i]
   252  		if slope*prevSlope > 0 {
   253  			dydxs[i] = 3 * (xs[i+1] - xs[i-1]) / ((2*xs[i+1]-xs[i-1]-xs[i])/slopes[i-1] +
   254  				(xs[i+1]+xs[i]-2*xs[i-1])/slopes[i])
   255  		} else {
   256  			dydxs[i] = 0
   257  		}
   258  		prevSlope = slope
   259  	}
   260  	dydxs[0] = fritschButlandEdgeDerivative(xs, ys, slopes, true)
   261  	dydxs[m] = fritschButlandEdgeDerivative(xs, ys, slopes, false)
   262  	fb.cubic.FitWithDerivatives(xs, ys, dydxs)
   263  	return nil
   264  }
   265  
   266  // fritschButlandEdgeDerivative calculates dy/dx approximation for the
   267  // Fritsch-Butland method for the left or right edge node.
   268  func fritschButlandEdgeDerivative(xs, ys, slopes []float64, leftEdge bool) float64 {
   269  	n := len(xs)
   270  	var dE, dI, h, hE, f float64
   271  	if leftEdge {
   272  		dE = slopes[0]
   273  		dI = slopes[1]
   274  		xE := xs[0]
   275  		xM := xs[1]
   276  		xI := xs[2]
   277  		hE = xM - xE
   278  		h = xI - xE
   279  		f = xM + xI - 2*xE
   280  	} else {
   281  		dE = slopes[n-2]
   282  		dI = slopes[n-3]
   283  		xE := xs[n-1]
   284  		xM := xs[n-2]
   285  		xI := xs[n-3]
   286  		hE = xE - xM
   287  		h = xE - xI
   288  		f = 2*xE - xI - xM
   289  	}
   290  	g := (f*dE - hE*dI) / h
   291  	if g*dE <= 0 {
   292  		return 0
   293  	}
   294  	if dE*dI <= 0 && math.Abs(g) > 3*math.Abs(dE) {
   295  		return 3 * dE
   296  	}
   297  	return g
   298  }
   299  
   300  // fitWithSecondDerivatives fits a piecewise cubic predictor to (X, Y, d^2Y/dX^2) value
   301  // triples provided as three slices.
   302  // It panics if any of these is true:
   303  // - len(xs) < 2,
   304  // - elements of xs are not strictly increasing,
   305  // - len(xs) != len(ys),
   306  // - len(xs) != len(d2ydx2s).
   307  // Note that this method does not guarantee on its own the continuity of first derivatives.
   308  func (pc *PiecewiseCubic) fitWithSecondDerivatives(xs, ys, d2ydx2s []float64) {
   309  	n := len(xs)
   310  	switch {
   311  	case len(ys) != n, len(d2ydx2s) != n:
   312  		panic(differentLengths)
   313  	case n < 2:
   314  		panic(tooFewPoints)
   315  	}
   316  	m := n - 1
   317  	pc.coeffs.Reset()
   318  	pc.coeffs.ReuseAs(m, 4)
   319  	for i := 0; i < m; i++ {
   320  		dx := xs[i+1] - xs[i]
   321  		if dx <= 0 {
   322  			panic(xsNotStrictlyIncreasing)
   323  		}
   324  		dy := ys[i+1] - ys[i]
   325  		dm := d2ydx2s[i+1] - d2ydx2s[i]
   326  		pc.coeffs.Set(i, 0, ys[i])                             // a_0
   327  		pc.coeffs.Set(i, 1, (dy-(d2ydx2s[i]+dm/3)*dx*dx/2)/dx) // a_1
   328  		pc.coeffs.Set(i, 2, d2ydx2s[i]/2)                      // a_2
   329  		pc.coeffs.Set(i, 3, dm/6/dx)                           // a_3
   330  	}
   331  	pc.xs = append(pc.xs[:0], xs...)
   332  	pc.lastY = ys[m]
   333  	lastDx := xs[m] - xs[m-1]
   334  	pc.lastDyDx = pc.coeffs.At(m-1, 1) + 2*pc.coeffs.At(m-1, 2)*lastDx + 3*pc.coeffs.At(m-1, 3)*lastDx*lastDx
   335  }
   336  
   337  // makeCubicSplineSecondDerivativeEquations generates the basic system of linear equations
   338  // which have to be satisfied by the second derivatives to make the first derivatives of a
   339  // cubic spline continuous. It panics if elements of xs are not strictly increasing, or
   340  // len(xs) != len(ys).
   341  // makeCubicSplineSecondDerivativeEquations fills a banded matrix a and a vector b
   342  // defining a system of linear equations a*m = b for second derivatives vector m.
   343  // Parameters a and b are assumed to have correct dimensions and initialised to zero.
   344  func makeCubicSplineSecondDerivativeEquations(a mat.MutableBanded, b mat.MutableVector, xs, ys []float64) {
   345  	n := len(xs)
   346  	if len(ys) != n {
   347  		panic(differentLengths)
   348  	}
   349  	m := n - 1
   350  	if n > 2 {
   351  		for i := 0; i < m; i++ {
   352  			dx := xs[i+1] - xs[i]
   353  			if dx <= 0 {
   354  				panic(xsNotStrictlyIncreasing)
   355  			}
   356  			slope := (ys[i+1] - ys[i]) / dx
   357  			if i > 0 {
   358  				b.SetVec(i, b.AtVec(i)+slope)
   359  				a.SetBand(i, i, a.At(i, i)+dx/3)
   360  				a.SetBand(i, i+1, dx/6)
   361  			}
   362  			if i < m-1 {
   363  				b.SetVec(i+1, b.AtVec(i+1)-slope)
   364  				a.SetBand(i+1, i+1, a.At(i+1, i+1)+dx/3)
   365  				a.SetBand(i+1, i, dx/6)
   366  			}
   367  		}
   368  	}
   369  }
   370  
   371  // NaturalCubic is a piecewise cubic 1-dimensional interpolator with
   372  // continuous value, first and second derivatives, which can be fitted to (X, Y)
   373  // value pairs without providing derivatives. It uses the boundary conditions
   374  // Y''(left end ) = Y''(right end) = 0.
   375  // See e.g. https://www.math.drexel.edu/~tolya/cubicspline.pdf for details.
   376  type NaturalCubic struct {
   377  	cubic PiecewiseCubic
   378  }
   379  
   380  // Predict returns the interpolation value at x.
   381  func (nc *NaturalCubic) Predict(x float64) float64 {
   382  	return nc.cubic.Predict(x)
   383  }
   384  
   385  // PredictDerivative returns the predicted derivative at x.
   386  func (nc *NaturalCubic) PredictDerivative(x float64) float64 {
   387  	return nc.cubic.PredictDerivative(x)
   388  }
   389  
   390  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   391  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   392  // or len(xs) != len(ys). It returns an error if solving the required system
   393  // of linear equations fails.
   394  func (nc *NaturalCubic) Fit(xs, ys []float64) error {
   395  	n := len(xs)
   396  	a := mat.NewTridiag(n, nil, nil, nil)
   397  	b := mat.NewVecDense(n, nil)
   398  	makeCubicSplineSecondDerivativeEquations(a, b, xs, ys)
   399  	// Add boundary conditions y''(left) = y''(right) = 0:
   400  	b.SetVec(0, 0)
   401  	b.SetVec(n-1, 0)
   402  	a.SetBand(0, 0, 1)
   403  	a.SetBand(n-1, n-1, 1)
   404  	x := mat.NewVecDense(n, nil)
   405  	err := a.SolveVecTo(x, false, b)
   406  	if err == nil {
   407  		nc.cubic.fitWithSecondDerivatives(xs, ys, x.RawVector().Data)
   408  	}
   409  	return err
   410  }
   411  
   412  // ClampedCubic is a piecewise cubic 1-dimensional interpolator with
   413  // continuous value, first and second derivatives, which can be fitted to (X, Y)
   414  // value pairs without providing derivatives. It uses the boundary conditions
   415  // Y'(left end ) = Y'(right end) = 0.
   416  type ClampedCubic struct {
   417  	cubic PiecewiseCubic
   418  }
   419  
   420  // Predict returns the interpolation value at x.
   421  func (cc *ClampedCubic) Predict(x float64) float64 {
   422  	return cc.cubic.Predict(x)
   423  }
   424  
   425  // PredictDerivative returns the predicted derivative at x.
   426  func (cc *ClampedCubic) PredictDerivative(x float64) float64 {
   427  	return cc.cubic.PredictDerivative(x)
   428  }
   429  
   430  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   431  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   432  // or len(xs) != len(ys). It returns an error if solving the required system
   433  // of linear equations fails.
   434  func (cc *ClampedCubic) Fit(xs, ys []float64) error {
   435  	n := len(xs)
   436  	a := mat.NewTridiag(n, nil, nil, nil)
   437  	b := mat.NewVecDense(n, nil)
   438  	makeCubicSplineSecondDerivativeEquations(a, b, xs, ys)
   439  	// Add boundary conditions y''(left) = y''(right) = 0:
   440  	// Condition Y'(left end) = 0:
   441  	dxL := xs[1] - xs[0]
   442  	b.SetVec(0, (ys[1]-ys[0])/dxL)
   443  	a.SetBand(0, 0, dxL/3)
   444  	a.SetBand(0, 1, dxL/6)
   445  	// Condition Y'(right end) = 0:
   446  	m := n - 1
   447  	dxR := xs[m] - xs[m-1]
   448  	b.SetVec(m, (ys[m]-ys[m-1])/dxR)
   449  	a.SetBand(m, m, -dxR/3)
   450  	a.SetBand(m, m-1, -dxR/6)
   451  	x := mat.NewVecDense(n, nil)
   452  	err := a.SolveVecTo(x, false, b)
   453  	if err == nil {
   454  		cc.cubic.fitWithSecondDerivatives(xs, ys, x.RawVector().Data)
   455  	}
   456  	return err
   457  }
   458  
   459  // NotAKnotCubic is a piecewise cubic 1-dimensional interpolator with
   460  // continuous value, first and second derivatives, which can be fitted to (X, Y)
   461  // value pairs without providing derivatives. It imposes the condition that
   462  // the third derivative of the interpolant is continuous in the first and
   463  // last interior node.
   464  // See http://www.cs.tau.ac.il/~turkel/notes/numeng/spline_note.pdf for details.
   465  type NotAKnotCubic struct {
   466  	cubic PiecewiseCubic
   467  }
   468  
   469  // Predict returns the interpolation value at x.
   470  func (nak *NotAKnotCubic) Predict(x float64) float64 {
   471  	return nak.cubic.Predict(x)
   472  }
   473  
   474  // PredictDerivative returns the predicted derivative at x.
   475  func (nak *NotAKnotCubic) PredictDerivative(x float64) float64 {
   476  	return nak.cubic.PredictDerivative(x)
   477  }
   478  
   479  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   480  // It panics if len(xs) < 3 (because at least one interior node is required),
   481  // elements of xs are not strictly increasing or len(xs) != len(ys).
   482  // It returns an error if solving the required system of linear equations fails.
   483  func (nak *NotAKnotCubic) Fit(xs, ys []float64) error {
   484  	n := len(xs)
   485  	if n < 3 {
   486  		panic(tooFewPoints)
   487  	}
   488  	a := mat.NewBandDense(n, n, 2, 2, nil)
   489  	b := mat.NewVecDense(n, nil)
   490  	makeCubicSplineSecondDerivativeEquations(a, b, xs, ys)
   491  	// Add boundary conditions.
   492  	// First interior node:
   493  	dxOuter := xs[1] - xs[0]
   494  	dxInner := xs[2] - xs[1]
   495  	a.SetBand(0, 0, 1/dxOuter)
   496  	a.SetBand(0, 1, -1/dxOuter-1/dxInner)
   497  	a.SetBand(0, 2, 1/dxInner)
   498  	if n > 3 {
   499  		// Last interior node:
   500  		m := n - 1
   501  		dxOuter = xs[m] - xs[m-1]
   502  		dxInner = xs[m-1] - xs[m-2]
   503  		a.SetBand(m, m, 1/dxOuter)
   504  		a.SetBand(m, m-1, -1/dxOuter-1/dxInner)
   505  		a.SetBand(m, m-2, 1/dxInner)
   506  	}
   507  	x := mat.NewVecDense(n, nil)
   508  	err := x.SolveVec(a, b)
   509  	if err == nil {
   510  		nak.cubic.fitWithSecondDerivatives(xs, ys, x.RawVector().Data)
   511  	}
   512  	return err
   513  }