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 }