// Copyright ©2020 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package interp import ( "math" "sort" "gonum.org/v1/gonum/mat" ) const ( differentLengths = "interp: input slices have different lengths" tooFewPoints = "interp: too few points for interpolation" xsNotStrictlyIncreasing = "interp: xs values not strictly increasing" ) // Predictor predicts the value of a function. It handles both // interpolation and extrapolation. type Predictor interface { // Predict returns the predicted value at x. Predict(x float64) float64 } // Fitter fits a predictor to data. type Fitter interface { // Fit fits a predictor to (X, Y) value pairs provided as two slices. // It panics if len(xs) < 2, elements of xs are not strictly increasing // or len(xs) != len(ys). Returns an error if fitting fails. Fit(xs, ys []float64) error } // FittablePredictor is a Predictor which can fit itself to data. type FittablePredictor interface { Fitter Predictor } // DerivativePredictor predicts both the value and the derivative of // a function. It handles both interpolation and extrapolation. type DerivativePredictor interface { Predictor // PredictDerivative returns the predicted derivative at x. PredictDerivative(x float64) float64 } // Constant predicts a constant value. type Constant float64 // Predict returns the predicted value at x. func (c Constant) Predict(x float64) float64 { return float64(c) } // Function predicts by evaluating itself. type Function func(float64) float64 // Predict returns the predicted value at x by evaluating fn(x). func (fn Function) Predict(x float64) float64 { return fn(x) } // PiecewiseLinear is a piecewise linear 1-dimensional interpolator. type PiecewiseLinear struct { // Interpolated X values. xs []float64 // Interpolated Y data values, same len as ys. ys []float64 // Slopes of Y between neighbouring X values. len(slopes) + 1 == len(xs) == len(ys). slopes []float64 } // Fit fits a predictor to (X, Y) value pairs provided as two slices. // It panics if len(xs) < 2, elements of xs are not strictly increasing // or len(xs) != len(ys). Always returns nil. func (pl *PiecewiseLinear) Fit(xs, ys []float64) error { n := len(xs) if len(ys) != n { panic(differentLengths) } if n < 2 { panic(tooFewPoints) } m := n - 1 pl.slopes = make([]float64, m) for i := 0; i < m; i++ { dx := xs[i+1] - xs[i] if dx <= 0 { panic(xsNotStrictlyIncreasing) } pl.slopes[i] = (ys[i+1] - ys[i]) / dx } pl.xs = make([]float64, n) pl.ys = make([]float64, n) copy(pl.xs, xs) copy(pl.ys, ys) return nil } // Predict returns the interpolation value at x. func (pl PiecewiseLinear) Predict(x float64) float64 { i := findSegment(pl.xs, x) if i < 0 { return pl.ys[0] } xI := pl.xs[i] if x == xI { return pl.ys[i] } n := len(pl.xs) if i == n-1 { return pl.ys[n-1] } return pl.ys[i] + pl.slopes[i]*(x-xI) } // PiecewiseConstant is a left-continous, piecewise constant // 1-dimensional interpolator. type PiecewiseConstant struct { // Interpolated X values. xs []float64 // Interpolated Y data values, same len as ys. ys []float64 } // Fit fits a predictor to (X, Y) value pairs provided as two slices. // It panics if len(xs) < 2, elements of xs are not strictly increasing // or len(xs) != len(ys). Always returns nil. func (pc *PiecewiseConstant) Fit(xs, ys []float64) error { n := len(xs) if len(ys) != n { panic(differentLengths) } if n < 2 { panic(tooFewPoints) } for i := 1; i < n; i++ { if xs[i] <= xs[i-1] { panic(xsNotStrictlyIncreasing) } } pc.xs = make([]float64, n) pc.ys = make([]float64, n) copy(pc.xs, xs) copy(pc.ys, ys) return nil } // Predict returns the interpolation value at x. func (pc PiecewiseConstant) Predict(x float64) float64 { i := findSegment(pc.xs, x) if i < 0 { return pc.ys[0] } if x == pc.xs[i] { return pc.ys[i] } n := len(pc.xs) if i == n-1 { return pc.ys[n-1] } return pc.ys[i+1] } // PiecewiseCubic is a piecewise cubic 1-dimensional interpolator with // continuous value and first derivative. type PiecewiseCubic struct { // Interpolated X values. xs []float64 // Coefficients of interpolating cubic polynomials, with // len(xs) - 1 rows and 4 columns. The interpolated value // for xs[i] <= x < xs[i + 1] is defined as // sum_{k = 0}^3 coeffs.At(i, k) * (x - xs[i])^k // To guarantee left-continuity, coeffs.At(i, 0) == ys[i]. coeffs mat.Dense // Last interpolated Y value, corresponding to xs[len(xs) - 1]. lastY float64 // Last interpolated dY/dX value, corresponding to xs[len(xs) - 1]. lastDyDx float64 } // Predict returns the interpolation value at x. func (pc *PiecewiseCubic) Predict(x float64) float64 { i := findSegment(pc.xs, x) if i < 0 { return pc.coeffs.At(0, 0) } m := len(pc.xs) - 1 if x == pc.xs[i] { if i < m { return pc.coeffs.At(i, 0) } return pc.lastY } if i == m { return pc.lastY } dx := x - pc.xs[i] a := pc.coeffs.RawRowView(i) return ((a[3]*dx+a[2])*dx+a[1])*dx + a[0] } // PredictDerivative returns the predicted derivative at x. func (pc *PiecewiseCubic) PredictDerivative(x float64) float64 { i := findSegment(pc.xs, x) if i < 0 { return pc.coeffs.At(0, 1) } m := len(pc.xs) - 1 if x == pc.xs[i] { if i < m { return pc.coeffs.At(i, 1) } return pc.lastDyDx } if i == m { return pc.lastDyDx } dx := x - pc.xs[i] a := pc.coeffs.RawRowView(i) return (3*a[3]*dx+2*a[2])*dx + a[1] } // FitWithDerivatives fits a piecewise cubic predictor to (X, Y, dY/dX) value // triples provided as three slices. // It panics if len(xs) < 2, elements of xs are not strictly increasing, // len(xs) != len(ys) or len(xs) != len(dydxs). func (pc *PiecewiseCubic) FitWithDerivatives(xs, ys, dydxs []float64) { n := len(xs) if len(ys) != n { panic(differentLengths) } if len(dydxs) != n { panic(differentLengths) } if n < 2 { panic(tooFewPoints) } m := n - 1 pc.coeffs.Reset() pc.coeffs.ReuseAs(m, 4) for i := 0; i < m; i++ { dx := xs[i+1] - xs[i] if dx <= 0 { panic(xsNotStrictlyIncreasing) } dy := ys[i+1] - ys[i] // a_0 pc.coeffs.Set(i, 0, ys[i]) // a_1 pc.coeffs.Set(i, 1, dydxs[i]) // Solve a linear equation system for a_2 and a_3. pc.coeffs.Set(i, 2, (3*dy-(2*dydxs[i]+dydxs[i+1])*dx)/dx/dx) pc.coeffs.Set(i, 3, (-2*dy+(dydxs[i]+dydxs[i+1])*dx)/dx/dx/dx) } pc.xs = make([]float64, n) copy(pc.xs, xs) pc.lastY = ys[m] pc.lastDyDx = dydxs[m] } // AkimaSpline is a piecewise cubic 1-dimensional interpolator with // continuous value and first derivative, which can be fitted to (X, Y) // value pairs without providing derivatives. // See https://www.iue.tuwien.ac.at/phd/rottinger/node60.html for more details. type AkimaSpline struct { cubic PiecewiseCubic } // Predict returns the interpolation value at x. func (as *AkimaSpline) Predict(x float64) float64 { return as.cubic.Predict(x) } // PredictDerivative returns the predicted derivative at x. func (as *AkimaSpline) PredictDerivative(x float64) float64 { return as.cubic.PredictDerivative(x) } // Fit fits a predictor to (X, Y) value pairs provided as two slices. // It panics if len(xs) < 2, elements of xs are not strictly increasing // or len(xs) != len(ys). Always returns nil. func (as *AkimaSpline) Fit(xs, ys []float64) error { n := len(xs) if len(ys) != n { panic(differentLengths) } dydxs := make([]float64, n) if n == 2 { dx := xs[1] - xs[0] slope := (ys[1] - ys[0]) / dx dydxs[0] = slope dydxs[1] = slope as.cubic.FitWithDerivatives(xs, ys, dydxs) return nil } slopes := akimaSlopes(xs, ys) for i := 0; i < n; i++ { wLeft, wRight := akimaWeights(slopes, i) dydxs[i] = akimaWeightedAverage(slopes[i+1], slopes[i+2], wLeft, wRight) } as.cubic.FitWithDerivatives(xs, ys, dydxs) return nil } // akimaSlopes returns slopes for Akima spline method, including the approximations // of slopes outside the data range (two on each side). // It panics if len(xs) <= 2, elements of xs are not strictly increasing // or len(xs) != len(ys). func akimaSlopes(xs, ys []float64) []float64 { n := len(xs) if n <= 2 { panic(tooFewPoints) } if len(ys) != n { panic(differentLengths) } m := n + 3 slopes := make([]float64, m) for i := 2; i < m-2; i++ { dx := xs[i-1] - xs[i-2] if dx <= 0 { panic(xsNotStrictlyIncreasing) } slopes[i] = (ys[i-1] - ys[i-2]) / dx } slopes[0] = 3*slopes[2] - 2*slopes[3] slopes[1] = 2*slopes[2] - slopes[3] slopes[m-2] = 2*slopes[m-3] - slopes[m-4] slopes[m-1] = 3*slopes[m-3] - 2*slopes[m-4] return slopes } // findSegment returns 0 <= i < len(xs) such that xs[i] <= x < xs[i + 1], where xs[len(xs)] // is assumed to be +Inf. If no such i is found, it returns -1. It assumes that len(xs) >= 2 // without checking. func findSegment(xs []float64, x float64) int { return sort.Search(len(xs), func(i int) bool { return xs[i] > x }) - 1 } // akimaWeightedAverage returns (v1 * w1 + v2 * w2) / (w1 + w2) for w1, w2 >= 0 (not checked). // If w1 == w2 == 0, it returns a simple average of v1 and v2. func akimaWeightedAverage(v1, v2, w1, w2 float64) float64 { w := w1 + w2 if w > 0 { return (v1*w1 + v2*w2) / w } return 0.5*v1 + 0.5*v2 } // akimaWeights returns the left and right weight for approximating // the i-th derivative with neighbouring slopes. func akimaWeights(slopes []float64, i int) (float64, float64) { wLeft := math.Abs(slopes[i+2] - slopes[i+3]) wRight := math.Abs(slopes[i+1] - slopes[i]) return wLeft, wRight }