github.com/jingcheng-WU/gonum@v0.9.1-0.20210323123734-f1a2a11a8f7b/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/jingcheng-WU/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 = make([]float64, n)
   108  	copy(pc.xs, xs)
   109  	pc.lastY = ys[m]
   110  	pc.lastDyDx = dydxs[m]
   111  }
   112  
   113  // AkimaSpline is a piecewise cubic 1-dimensional interpolator with
   114  // continuous value and first derivative, which can be fitted to (X, Y)
   115  // value pairs without providing derivatives.
   116  // See https://www.iue.tuwien.ac.at/phd/rottinger/node60.html for more details.
   117  type AkimaSpline struct {
   118  	cubic PiecewiseCubic
   119  }
   120  
   121  // Predict returns the interpolation value at x.
   122  func (as *AkimaSpline) Predict(x float64) float64 {
   123  	return as.cubic.Predict(x)
   124  }
   125  
   126  // PredictDerivative returns the predicted derivative at x.
   127  func (as *AkimaSpline) PredictDerivative(x float64) float64 {
   128  	return as.cubic.PredictDerivative(x)
   129  }
   130  
   131  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   132  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   133  // or len(xs) != len(ys). Always returns nil.
   134  func (as *AkimaSpline) Fit(xs, ys []float64) error {
   135  	n := len(xs)
   136  	if len(ys) != n {
   137  		panic(differentLengths)
   138  	}
   139  	dydxs := make([]float64, n)
   140  
   141  	if n == 2 {
   142  		dx := xs[1] - xs[0]
   143  		slope := (ys[1] - ys[0]) / dx
   144  		dydxs[0] = slope
   145  		dydxs[1] = slope
   146  		as.cubic.FitWithDerivatives(xs, ys, dydxs)
   147  		return nil
   148  	}
   149  	slopes := akimaSlopes(xs, ys)
   150  	for i := 0; i < n; i++ {
   151  		wLeft, wRight := akimaWeights(slopes, i)
   152  		dydxs[i] = akimaWeightedAverage(slopes[i+1], slopes[i+2], wLeft, wRight)
   153  	}
   154  	as.cubic.FitWithDerivatives(xs, ys, dydxs)
   155  	return nil
   156  }
   157  
   158  // akimaSlopes returns slopes for Akima spline method, including the approximations
   159  // of slopes outside the data range (two on each side).
   160  // It panics if len(xs) <= 2, elements of xs are not strictly increasing
   161  // or len(xs) != len(ys).
   162  func akimaSlopes(xs, ys []float64) []float64 {
   163  	n := len(xs)
   164  	if n <= 2 {
   165  		panic(tooFewPoints)
   166  	}
   167  	if len(ys) != n {
   168  		panic(differentLengths)
   169  	}
   170  	m := n + 3
   171  	slopes := make([]float64, m)
   172  	for i := 2; i < m-2; i++ {
   173  		dx := xs[i-1] - xs[i-2]
   174  		if dx <= 0 {
   175  			panic(xsNotStrictlyIncreasing)
   176  		}
   177  		slopes[i] = (ys[i-1] - ys[i-2]) / dx
   178  	}
   179  	slopes[0] = 3*slopes[2] - 2*slopes[3]
   180  	slopes[1] = 2*slopes[2] - slopes[3]
   181  	slopes[m-2] = 2*slopes[m-3] - slopes[m-4]
   182  	slopes[m-1] = 3*slopes[m-3] - 2*slopes[m-4]
   183  	return slopes
   184  }
   185  
   186  // akimaWeightedAverage returns (v1 * w1 + v2 * w2) / (w1 + w2) for w1, w2 >= 0 (not checked).
   187  // If w1 == w2 == 0, it returns a simple average of v1 and v2.
   188  func akimaWeightedAverage(v1, v2, w1, w2 float64) float64 {
   189  	w := w1 + w2
   190  	if w > 0 {
   191  		return (v1*w1 + v2*w2) / w
   192  	}
   193  	return 0.5*v1 + 0.5*v2
   194  }
   195  
   196  // akimaWeights returns the left and right weight for approximating
   197  // the i-th derivative with neighbouring slopes.
   198  func akimaWeights(slopes []float64, i int) (float64, float64) {
   199  	wLeft := math.Abs(slopes[i+2] - slopes[i+3])
   200  	wRight := math.Abs(slopes[i+1] - slopes[i])
   201  	return wLeft, wRight
   202  }
   203  
   204  // FritschButland is a piecewise cubic 1-dimensional interpolator with
   205  // continuous value and first derivative, which can be fitted to (X, Y)
   206  // value pairs without providing derivatives.
   207  // It is monotone, local and produces a C^1 curve. Its downside is that
   208  // exhibits high tension, flattening out unnaturally the interpolated
   209  // curve between the nodes.
   210  // See Fritsch, F. N. and Butland, J., "A method for constructing local
   211  // monotone piecewise cubic interpolants" (1984), SIAM J. Sci. Statist.
   212  // Comput., 5(2), pp. 300-304.
   213  type FritschButland struct {
   214  	cubic PiecewiseCubic
   215  }
   216  
   217  // Predict returns the interpolation value at x.
   218  func (fb *FritschButland) Predict(x float64) float64 {
   219  	return fb.cubic.Predict(x)
   220  }
   221  
   222  // PredictDerivative returns the predicted derivative at x.
   223  func (fb *FritschButland) PredictDerivative(x float64) float64 {
   224  	return fb.cubic.PredictDerivative(x)
   225  }
   226  
   227  // Fit fits a predictor to (X, Y) value pairs provided as two slices.
   228  // It panics if len(xs) < 2, elements of xs are not strictly increasing
   229  // or len(xs) != len(ys). Always returns nil.
   230  func (fb *FritschButland) Fit(xs, ys []float64) error {
   231  	n := len(xs)
   232  	if n < 2 {
   233  		panic(tooFewPoints)
   234  	}
   235  	if len(ys) != n {
   236  		panic(differentLengths)
   237  	}
   238  	dydxs := make([]float64, n)
   239  
   240  	if n == 2 {
   241  		dx := xs[1] - xs[0]
   242  		slope := (ys[1] - ys[0]) / dx
   243  		dydxs[0] = slope
   244  		dydxs[1] = slope
   245  		fb.cubic.FitWithDerivatives(xs, ys, dydxs)
   246  		return nil
   247  	}
   248  	slopes := calculateSlopes(xs, ys)
   249  	m := len(slopes)
   250  	prevSlope := slopes[0]
   251  	for i := 1; i < m; i++ {
   252  		slope := slopes[i]
   253  		if slope*prevSlope > 0 {
   254  			dydxs[i] = 3 * (xs[i+1] - xs[i-1]) / ((2*xs[i+1]-xs[i-1]-xs[i])/slopes[i-1] +
   255  				(xs[i+1]+xs[i]-2*xs[i-1])/slopes[i])
   256  		} else {
   257  			dydxs[i] = 0
   258  		}
   259  		prevSlope = slope
   260  	}
   261  	dydxs[0] = fritschButlandEdgeDerivative(xs, ys, slopes, true)
   262  	dydxs[m] = fritschButlandEdgeDerivative(xs, ys, slopes, false)
   263  	fb.cubic.FitWithDerivatives(xs, ys, dydxs)
   264  	return nil
   265  }
   266  
   267  // fritschButlandEdgeDerivative calculates dy/dx approximation for the
   268  // Fritsch-Butland method for the left or right edge node.
   269  func fritschButlandEdgeDerivative(xs, ys, slopes []float64, leftEdge bool) float64 {
   270  	n := len(xs)
   271  	var dE, dI, h, hE, f float64
   272  	if leftEdge {
   273  		dE = slopes[0]
   274  		dI = slopes[1]
   275  		xE := xs[0]
   276  		xM := xs[1]
   277  		xI := xs[2]
   278  		hE = xM - xE
   279  		h = xI - xE
   280  		f = xM + xI - 2*xE
   281  	} else {
   282  		dE = slopes[n-2]
   283  		dI = slopes[n-3]
   284  		xE := xs[n-1]
   285  		xM := xs[n-2]
   286  		xI := xs[n-3]
   287  		hE = xE - xM
   288  		h = xE - xI
   289  		f = 2*xE - xI - xM
   290  	}
   291  	g := (f*dE - hE*dI) / h
   292  	if g*dE <= 0 {
   293  		return 0
   294  	}
   295  	if dE*dI <= 0 && math.Abs(g) > 3*math.Abs(dE) {
   296  		return 3 * dE
   297  	}
   298  	return g
   299  }