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 }