github.com/jingcheng-WU/gonum@v0.9.1-0.20210323123734-f1a2a11a8f7b/interp/cubic_test.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  	"testing"
    10  
    11  	"github.com/jingcheng-WU/gonum/floats"
    12  	"github.com/jingcheng-WU/gonum/floats/scalar"
    13  	"github.com/jingcheng-WU/gonum/mat"
    14  )
    15  
    16  func TestPiecewiseCubic(t *testing.T) {
    17  	t.Parallel()
    18  	const (
    19  		h        = 1e-8
    20  		valueTol = 1e-13
    21  		derivTol = 1e-6
    22  		nPts     = 100
    23  	)
    24  	for i, test := range []struct {
    25  		xs []float64
    26  		f  func(float64) float64
    27  		df func(float64) float64
    28  	}{
    29  		{
    30  			xs: []float64{-1.001, 0.2, 2},
    31  			f:  func(x float64) float64 { return x * x },
    32  			df: func(x float64) float64 { return 2 * x },
    33  		},
    34  		{
    35  			xs: []float64{-1.2, -1.001, 0, 0.2, 2.01, 2.1},
    36  			f:  func(x float64) float64 { return 4*math.Pow(x, 3) - 2*x*x + 10*x - 7 },
    37  			df: func(x float64) float64 { return 12*x*x - 4*x + 10 },
    38  		},
    39  		{
    40  			xs: []float64{-1.001, 0.2, 10},
    41  			f:  func(x float64) float64 { return 1.5*x - 1 },
    42  			df: func(x float64) float64 { return 1.5 },
    43  		},
    44  		{
    45  			xs: []float64{-1.001, 0.2, 10},
    46  			f:  func(x float64) float64 { return -1 },
    47  			df: func(x float64) float64 { return 0 },
    48  		},
    49  	} {
    50  		ys := applyFunc(test.xs, test.f)
    51  		dydxs := applyFunc(test.xs, test.df)
    52  		var pc PiecewiseCubic
    53  		pc.FitWithDerivatives(test.xs, ys, dydxs)
    54  		n := len(test.xs)
    55  		m := n - 1
    56  		x0 := test.xs[0]
    57  		x1 := test.xs[m]
    58  		x := x0 - 0.1
    59  		got := pc.Predict(x)
    60  		want := ys[0]
    61  		if got != want {
    62  			t.Errorf("Mismatch in value extrapolated to the left for test case %d: got %v, want %g", i, got, want)
    63  		}
    64  		got = pc.PredictDerivative(x)
    65  		want = dydxs[0]
    66  		if got != want {
    67  			t.Errorf("Mismatch in derivative extrapolated to the left for test case %d: got %v, want %g", i, got, want)
    68  		}
    69  		x = x1 + 0.1
    70  		got = pc.Predict(x)
    71  		want = ys[m]
    72  		if got != want {
    73  			t.Errorf("Mismatch in value extrapolated to the right for test case %d: got %v, want %g", i, got, want)
    74  		}
    75  		got = pc.PredictDerivative(x)
    76  		want = dydxs[m]
    77  		if got != want {
    78  			t.Errorf("Mismatch in derivative extrapolated to the right for test case %d: got %v, want %g", i, got, want)
    79  		}
    80  		for j := 0; j < n; j++ {
    81  			x := test.xs[j]
    82  			got := pc.Predict(x)
    83  			want := test.f(x)
    84  			if math.Abs(got-want) > valueTol {
    85  				t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want)
    86  			}
    87  			if j < m {
    88  				got = pc.coeffs.At(j, 0)
    89  				if math.Abs(got-want) > valueTol {
    90  					t.Errorf("Mismatch in 0-th order interpolation coefficient in %d-th node for test case %d: got %v, want %g", j, i, got, want)
    91  				}
    92  				dx := (test.xs[j+1] - x) / nPts
    93  				for k := 1; k < nPts; k++ {
    94  					xk := x + float64(k)*dx
    95  					got := pc.Predict(xk)
    96  					want := test.f(xk)
    97  					if math.Abs(got-want) > valueTol {
    98  						t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want)
    99  					}
   100  					got = pc.PredictDerivative(xk)
   101  					want = discrDerivPredict(&pc, x0, x1, xk, h)
   102  					if math.Abs(got-want) > derivTol {
   103  						t.Errorf("Mismatch in interpolated derivative at x == %g for test case %d: got %v, want %g", x, i, got, want)
   104  					}
   105  				}
   106  			} else {
   107  				got = pc.lastY
   108  				if math.Abs(got-want) > valueTol {
   109  					t.Errorf("Mismatch in lastY for test case %d: got %v, want %g", i, got, want)
   110  				}
   111  			}
   112  
   113  			if j > 0 {
   114  				dx := test.xs[j] - test.xs[j-1]
   115  				got = ((pc.coeffs.At(j-1, 3)*dx+pc.coeffs.At(j-1, 2))*dx+pc.coeffs.At(j-1, 1))*dx + pc.coeffs.At(j-1, 0)
   116  				if math.Abs(got-want) > valueTol {
   117  					t.Errorf("Interpolation coefficients in %d-th node produce mismatch in interpolated value at %g for test case %d: got %v, want %g", j-1, x, i, got, want)
   118  				}
   119  			}
   120  			got = discrDerivPredict(&pc, x0, x1, x, h)
   121  			want = test.df(x)
   122  			if math.Abs(got-want) > derivTol {
   123  				t.Errorf("Mismatch in numerical derivative of interpolated function at x == %g for test case %d: got %v, want %g", x, i, got, want)
   124  			}
   125  			got = pc.PredictDerivative(x)
   126  			if math.Abs(got-want) > valueTol {
   127  				t.Errorf("Mismatch in interpolated derivative value at x == %g for test case %d: got %v, want %g", x, i, got, want)
   128  			}
   129  		}
   130  	}
   131  }
   132  
   133  func TestPiecewiseCubicFitWithDerivatives(t *testing.T) {
   134  	t.Parallel()
   135  	xs := []float64{-1, 0, 1}
   136  	ys := make([]float64, 3)
   137  	dydxs := make([]float64, 3)
   138  	leftPoly := func(x float64) float64 {
   139  		return x*x - x + 1
   140  	}
   141  	leftPolyDerivative := func(x float64) float64 {
   142  		return 2*x - 1
   143  	}
   144  	rightPoly := func(x float64) float64 {
   145  		return x*x*x - x + 1
   146  	}
   147  	rightPolyDerivative := func(x float64) float64 {
   148  		return 3*x*x - 1
   149  	}
   150  	ys[0] = leftPoly(xs[0])
   151  	ys[1] = leftPoly(xs[1])
   152  	ys[2] = rightPoly(xs[2])
   153  	dydxs[0] = leftPolyDerivative(xs[0])
   154  	dydxs[1] = leftPolyDerivative(xs[1])
   155  	dydxs[2] = rightPolyDerivative(xs[2])
   156  	var pc PiecewiseCubic
   157  	pc.FitWithDerivatives(xs, ys, dydxs)
   158  	lastY := rightPoly(xs[2])
   159  	if pc.lastY != lastY {
   160  		t.Errorf("Mismatch in lastY: got %v, want %g", pc.lastY, lastY)
   161  	}
   162  	lastDyDx := rightPolyDerivative(xs[2])
   163  	if pc.lastDyDx != lastDyDx {
   164  		t.Errorf("Mismatch in lastDxDy: got %v, want %g", pc.lastDyDx, lastDyDx)
   165  	}
   166  	if !floats.Equal(pc.xs, xs) {
   167  		t.Errorf("Mismatch in xs: got %v, want %v", pc.xs, xs)
   168  	}
   169  	coeffs := mat.NewDense(2, 4, []float64{3, -3, 1, 0, 1, -1, 0, 1})
   170  	if !mat.EqualApprox(&pc.coeffs, coeffs, 1e-14) {
   171  		t.Errorf("Mismatch in coeffs: got %v, want %v", pc.coeffs, coeffs)
   172  	}
   173  }
   174  
   175  func TestPiecewiseCubicFitWithDerivativesErrors(t *testing.T) {
   176  	t.Parallel()
   177  	for _, test := range []struct {
   178  		xs, ys, dydxs []float64
   179  	}{
   180  		{
   181  			xs:    []float64{0, 1, 2},
   182  			ys:    []float64{10, 20},
   183  			dydxs: []float64{0, 0, 0},
   184  		},
   185  		{
   186  			xs:    []float64{0, 1, 1},
   187  			ys:    []float64{10, 20, 30},
   188  			dydxs: []float64{0, 0, 0, 0},
   189  		},
   190  		{
   191  			xs:    []float64{0},
   192  			ys:    []float64{0},
   193  			dydxs: []float64{0},
   194  		},
   195  		{
   196  			xs:    []float64{0, 1, 1},
   197  			ys:    []float64{10, 20, 10},
   198  			dydxs: []float64{0, 0, 0},
   199  		},
   200  	} {
   201  		var pc PiecewiseCubic
   202  		if !panics(func() { pc.FitWithDerivatives(test.xs, test.ys, test.dydxs) }) {
   203  			t.Errorf("expected panic for xs: %v, ys: %v and dydxs: %v", test.xs, test.ys, test.dydxs)
   204  		}
   205  	}
   206  }
   207  
   208  func TestAkimaSpline(t *testing.T) {
   209  	t.Parallel()
   210  	const (
   211  		derivAbsTol = 1e-8
   212  		derivRelTol = 1e-7
   213  		h           = 1e-8
   214  		nPts        = 100
   215  		tol         = 1e-14
   216  	)
   217  	for i, test := range []struct {
   218  		xs []float64
   219  		f  func(float64) float64
   220  	}{
   221  		{
   222  			xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3},
   223  			f:  func(x float64) float64 { return x * x },
   224  		},
   225  		{
   226  			xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3},
   227  			f:  func(x float64) float64 { return math.Pow(x, 3.) - x*x + 2 },
   228  		},
   229  		{
   230  			xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3},
   231  			f:  func(x float64) float64 { return -10 * x },
   232  		},
   233  		{
   234  			xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3},
   235  			f:  math.Sin,
   236  		},
   237  		{
   238  			xs: []float64{0, 1},
   239  			f:  math.Exp,
   240  		},
   241  		{
   242  			xs: []float64{-1, 0.5},
   243  			f:  math.Cos,
   244  		},
   245  	} {
   246  		var as AkimaSpline
   247  		n := len(test.xs)
   248  		m := n - 1
   249  		x0 := test.xs[0]
   250  		x1 := test.xs[m]
   251  		ys := applyFunc(test.xs, test.f)
   252  		err := as.Fit(test.xs, ys)
   253  		if err != nil {
   254  			t.Errorf("Error when fitting AkimaSpline in test case %d: %v", i, err)
   255  		}
   256  		for j := 0; j < n; j++ {
   257  			x := test.xs[j]
   258  			got := as.Predict(x)
   259  			want := test.f(x)
   260  			if math.Abs(got-want) > tol {
   261  				t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want)
   262  			}
   263  			if j < m {
   264  				dx := (test.xs[j+1] - x) / nPts
   265  				for k := 1; k < nPts; k++ {
   266  					xk := x + float64(k)*dx
   267  					got = as.PredictDerivative(xk)
   268  					want = discrDerivPredict(&as, x0, x1, xk, h)
   269  					if math.Abs(got-want) > derivRelTol*math.Abs(want)+derivAbsTol {
   270  						t.Errorf("Mismatch in interpolated derivative at x == %g for test case %d: got %v, want %g", x, i, got, want)
   271  					}
   272  				}
   273  			}
   274  		}
   275  		if n == 2 {
   276  			got := as.cubic.coeffs.At(0, 1)
   277  			want := (ys[1] - ys[0]) / (test.xs[1] - test.xs[0])
   278  			if math.Abs(got-want) > tol {
   279  				t.Errorf("Mismatch in approximated slope for length-2 test case %d: got %v, want %g", i, got, want)
   280  			}
   281  			for j := 2; i < 4; j++ {
   282  				got := as.cubic.coeffs.At(0, j)
   283  				if got != 0 {
   284  					t.Errorf("Non-zero order-%d coefficient for length-2 test case %d: got %v", j, i, got)
   285  				}
   286  			}
   287  		}
   288  	}
   289  }
   290  
   291  func TestAkimaSplineFitErrors(t *testing.T) {
   292  	t.Parallel()
   293  	for _, test := range []struct {
   294  		xs, ys []float64
   295  	}{
   296  		{
   297  			xs: []float64{0, 1, 2},
   298  			ys: []float64{10, 20},
   299  		},
   300  		{
   301  			xs: []float64{0, 1},
   302  			ys: []float64{10, 20, 30},
   303  		},
   304  		{
   305  			xs: []float64{0},
   306  			ys: []float64{0},
   307  		},
   308  		{
   309  			xs: []float64{0, 1, 1},
   310  			ys: []float64{10, 20, 10},
   311  		},
   312  		{
   313  			xs: []float64{0, 2, 1},
   314  			ys: []float64{10, 20, 10},
   315  		},
   316  		{
   317  			xs: []float64{0, 0},
   318  			ys: []float64{-1, 2},
   319  		},
   320  		{
   321  			xs: []float64{0, -1},
   322  			ys: []float64{-1, 2},
   323  		},
   324  	} {
   325  		var as AkimaSpline
   326  		if !panics(func() { _ = as.Fit(test.xs, test.ys) }) {
   327  			t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys)
   328  		}
   329  	}
   330  }
   331  
   332  func TestAkimaWeightedAverage(t *testing.T) {
   333  	t.Parallel()
   334  	for i, test := range []struct {
   335  		v1, v2, w1, w2, want float64
   336  		// "want" values calculated by hand.
   337  	}{
   338  		{
   339  			v1:   -1,
   340  			v2:   1,
   341  			w1:   0,
   342  			w2:   0,
   343  			want: 0,
   344  		},
   345  		{
   346  			v1:   -1,
   347  			v2:   1,
   348  			w1:   1e6,
   349  			w2:   1e6,
   350  			want: 0,
   351  		},
   352  		{
   353  			v1:   -1,
   354  			v2:   1,
   355  			w1:   1e-10,
   356  			w2:   0,
   357  			want: -1,
   358  		},
   359  		{
   360  			v1:   -1,
   361  			v2:   1,
   362  			w1:   0,
   363  			w2:   1e-10,
   364  			want: 1,
   365  		},
   366  		{
   367  			v1:   0,
   368  			v2:   1000,
   369  			w1:   1e-13,
   370  			w2:   3e-13,
   371  			want: 750,
   372  		},
   373  		{
   374  			v1:   0,
   375  			v2:   1000,
   376  			w1:   3e-13,
   377  			w2:   1e-13,
   378  			want: 250,
   379  		},
   380  	} {
   381  		got := akimaWeightedAverage(test.v1, test.v2, test.w1, test.w2)
   382  		if !scalar.EqualWithinAbsOrRel(got, test.want, 1e-14, 1e-14) {
   383  			t.Errorf("Mismatch in test case %d: got %v, want %g", i, got, test.want)
   384  		}
   385  	}
   386  }
   387  
   388  func TestAkimaSlopes(t *testing.T) {
   389  	t.Parallel()
   390  	for i, test := range []struct {
   391  		xs, ys, want []float64
   392  		// "want" values calculated by hand.
   393  	}{
   394  		{
   395  			xs:   []float64{-2, 0, 1},
   396  			ys:   []float64{2, 0, 1.5},
   397  			want: []float64{-6, -3.5, -1, 1.5, 4, 6.5},
   398  		},
   399  		{
   400  			xs:   []float64{-2, -0.5, 1},
   401  			ys:   []float64{-2, -0.5, 1},
   402  			want: []float64{1, 1, 1, 1, 1, 1},
   403  		},
   404  		{
   405  			xs:   []float64{-2, -0.5, 1},
   406  			ys:   []float64{1, 1, 1},
   407  			want: []float64{0, 0, 0, 0, 0, 0},
   408  		},
   409  		{
   410  			xs:   []float64{0, 1.5, 2, 4, 4.5, 5, 6, 7.5, 8},
   411  			ys:   []float64{-5, -4, -3.5, -3.25, -3.25, -2.5, -1.5, -1, 2},
   412  			want: []float64{0, 1. / 3, 2. / 3, 1, 0.125, 0, 1.5, 1, 1. / 3, 6, 12 - 1./3, 18 - 2./3},
   413  		},
   414  	} {
   415  		got := akimaSlopes(test.xs, test.ys)
   416  		if !floats.EqualApprox(got, test.want, 1e-14) {
   417  			t.Errorf("Mismatch in test case %d: got %v, want %v", i, got, test.want)
   418  		}
   419  	}
   420  }
   421  
   422  func TestAkimaSlopesErrors(t *testing.T) {
   423  	t.Parallel()
   424  	for _, test := range []struct {
   425  		xs, ys []float64
   426  	}{
   427  		{
   428  			xs: []float64{0, 1, 2},
   429  			ys: []float64{10, 20},
   430  		},
   431  		{
   432  			xs: []float64{0, 1},
   433  			ys: []float64{10, 20, 30},
   434  		},
   435  		{
   436  			xs: []float64{0, 2},
   437  			ys: []float64{0, 1},
   438  		},
   439  		{
   440  			xs: []float64{0, 1, 1},
   441  			ys: []float64{10, 20, 10},
   442  		},
   443  		{
   444  			xs: []float64{0, 2, 1},
   445  			ys: []float64{10, 20, 10},
   446  		},
   447  		{
   448  			xs: []float64{0, 0},
   449  			ys: []float64{-1, 2},
   450  		},
   451  		{
   452  			xs: []float64{0, -1},
   453  			ys: []float64{-1, 2},
   454  		},
   455  	} {
   456  		if !panics(func() { akimaSlopes(test.xs, test.ys) }) {
   457  			t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys)
   458  		}
   459  	}
   460  }
   461  
   462  func TestAkimaWeights(t *testing.T) {
   463  	t.Parallel()
   464  	const tol = 1e-14
   465  	slopes := []float64{-2, -1, -0.1, 0.2, 1.2, 2.5}
   466  	// "want" values calculated by hand.
   467  	want := [][]float64{
   468  		{0.3, 1},
   469  		{1, 0.9},
   470  		{1.3, 0.3},
   471  	}
   472  	for i := 0; i < len(want); i++ {
   473  		gotLeft, gotRight := akimaWeights(slopes, i)
   474  		if math.Abs(gotLeft-want[i][0]) > tol {
   475  			t.Errorf("Mismatch in left weight for node %d: got %v, want %g", i, gotLeft, want[i][0])
   476  		}
   477  		if math.Abs(gotRight-want[i][1]) > tol {
   478  			t.Errorf("Mismatch in left weight for node %d: got %v, want %g", i, gotRight, want[i][1])
   479  		}
   480  	}
   481  }
   482  
   483  func TestFritschButland(t *testing.T) {
   484  	t.Parallel()
   485  	const (
   486  		tol  = 1e-14
   487  		nPts = 100
   488  	)
   489  	for k, test := range []struct {
   490  		xs, ys []float64
   491  	}{
   492  		{
   493  			xs: []float64{0, 2},
   494  			ys: []float64{0, 0.5},
   495  		},
   496  		{
   497  			xs: []float64{0, 2},
   498  			ys: []float64{0, -0.5},
   499  		},
   500  		{
   501  			xs: []float64{0, 2},
   502  			ys: []float64{0, 0},
   503  		},
   504  		{
   505  			xs: []float64{0, 2, 3, 4},
   506  			ys: []float64{0, 1, 2, 2.5},
   507  		},
   508  		{
   509  			xs: []float64{0, 2, 3, 4},
   510  			ys: []float64{0, 1.5, 1.5, 2.5},
   511  		},
   512  		{
   513  			xs: []float64{0, 2, 3, 4},
   514  			ys: []float64{0, 1.5, 1.5, 1},
   515  		},
   516  		{
   517  			xs: []float64{0, 2, 3, 4},
   518  			ys: []float64{0, 2.5, 1.5, 1},
   519  		},
   520  		{
   521  			xs: []float64{0, 2, 3, 4},
   522  			ys: []float64{0, 2.5, 1.5, 2},
   523  		},
   524  		{
   525  			xs: []float64{0, 2, 3, 4},
   526  			ys: []float64{4, 3, 2, 1},
   527  		},
   528  		{
   529  			xs: []float64{0, 2, 3, 4},
   530  			ys: []float64{4, 3, 2, 2},
   531  		},
   532  		{
   533  			xs: []float64{0, 2, 3, 4},
   534  			ys: []float64{4, 3, 2, 5},
   535  		},
   536  		{
   537  			xs: []float64{0, 2, 3, 4, 5, 6},
   538  			ys: []float64{0, 1, 0.5, 0.5, 1.5, 1.5},
   539  		},
   540  		{
   541  			xs: []float64{0, 2, 3, 4, 5, 6},
   542  			ys: []float64{0, 1, 1.5, 2.5, 1.5, 1},
   543  		},
   544  		{
   545  			xs: []float64{0, 2, 3, 4, 5, 6},
   546  			ys: []float64{0, -1, -1.5, -2.5, -1.5, -1},
   547  		},
   548  		{
   549  			xs: []float64{0, 2, 3, 4, 5, 6},
   550  			ys: []float64{0, 1, 0.5, 1.5, 1, 2},
   551  		},
   552  		{
   553  			xs: []float64{0, 2, 3, 4, 5, 6},
   554  			ys: []float64{0, 1, 1.5, 2.5, 3, 4},
   555  		},
   556  		{
   557  			xs: []float64{0, 2, 3, 4, 5, 6},
   558  			ys: []float64{0, 0.0001, -1.5, -2.5, -0.0001, 0},
   559  		},
   560  	} {
   561  		var fb FritschButland
   562  		err := fb.Fit(test.xs, test.ys)
   563  		if err != nil {
   564  			t.Errorf("Error when fitting FritschButland in test case %d: %v", k, err)
   565  		}
   566  		n := len(test.xs)
   567  		for i := 0; i < n; i++ {
   568  			got := fb.Predict(test.xs[i])
   569  			want := test.ys[i]
   570  			if got != want {
   571  				t.Errorf("Mismatch in interpolated value for node %d in test case %d: got %v, want %g", i, k, got, want)
   572  			}
   573  		}
   574  		if n == 2 {
   575  			h := test.xs[1] - test.xs[0]
   576  			want := (test.ys[1] - test.ys[0]) / h
   577  			for i := 0; i < 2; i++ {
   578  				got := fb.PredictDerivative(test.xs[i])
   579  				if !scalar.EqualWithinAbs(got, want, tol) {
   580  					t.Errorf("Mismatch in approximated derivative for node %d in 2-node test case %d: got %v, want %g", i, k, got, want)
   581  				}
   582  			}
   583  			dx := h / (nPts + 1)
   584  			for i := 1; i < nPts; i++ {
   585  				x := test.xs[0] + float64(i)*dx
   586  				got := fb.PredictDerivative(x)
   587  				if !scalar.EqualWithinAbs(got, want, tol) {
   588  					t.Errorf("Mismatch in interpolated derivative for x == %g in 2-node test case %d: got %v, want %g", x, k, got, want)
   589  				}
   590  			}
   591  		} else {
   592  			m := n - 1
   593  			for i := 1; i < m; i++ {
   594  				got := fb.PredictDerivative(test.xs[i])
   595  				slope := (test.ys[i+1] - test.ys[i]) / (test.xs[i+1] - test.xs[i])
   596  				prevSlope := (test.ys[i] - test.ys[i-1]) / (test.xs[i] - test.xs[i-1])
   597  				if slope*prevSlope > 0 {
   598  					if got == 0 {
   599  						t.Errorf("Approximated derivative is zero for node %d in test case %d: %g", i, k, got)
   600  					} else if math.Signbit(slope) != math.Signbit(got) {
   601  						t.Errorf("Approximated derivative has wrong sign for node %d in test case %d: got %g, want %g", i, k, math.Copysign(1, got), math.Copysign(1, slope))
   602  					}
   603  				} else {
   604  					if got != 0 {
   605  						t.Errorf("Approximated derivative is not zero for node %d in test case %d: %g", i, k, got)
   606  					}
   607  				}
   608  			}
   609  			for i := 0; i < m; i++ {
   610  				yL := test.ys[i]
   611  				yR := test.ys[i+1]
   612  				xL := test.xs[i]
   613  				dx := (test.xs[i+1] - xL) / (nPts + 1)
   614  				if yL == yR {
   615  					for j := 1; j < nPts; j++ {
   616  						x := xL + float64(j)*dx
   617  						got := fb.Predict(x)
   618  						if got != yL {
   619  							t.Errorf("Mismatch in interpolated value for x == %g in test case %d: got %v, want %g", x, k, got, yL)
   620  						}
   621  						got = fb.PredictDerivative(x)
   622  						if got != 0 {
   623  							t.Errorf("Interpolated derivative not zero for x == %g in test case %d: got %v", x, k, got)
   624  						}
   625  					}
   626  				} else {
   627  					minY := math.Min(yL, yR)
   628  					maxY := math.Max(yL, yR)
   629  					for j := 1; j < nPts; j++ {
   630  						x := xL + float64(j)*dx
   631  						got := fb.Predict(x)
   632  						if got < minY || got > maxY {
   633  							t.Errorf("Interpolated value out of [%g, %g] bounds for x == %g in test case %d: got %v", minY, maxY, x, k, got)
   634  						}
   635  						got = fb.PredictDerivative(x)
   636  						dy := yR - yL
   637  						if got*dy < 0 {
   638  							t.Errorf("Interpolated derivative has wrong sign for x == %g in test case %d: want %g, got %g", x, k, math.Copysign(1, dy), math.Copysign(1, got))
   639  						}
   640  					}
   641  				}
   642  			}
   643  		}
   644  	}
   645  }
   646  
   647  func TestFritschButlandErrors(t *testing.T) {
   648  	t.Parallel()
   649  	for _, test := range []struct {
   650  		xs, ys []float64
   651  	}{
   652  		{
   653  			xs: []float64{0},
   654  			ys: []float64{0},
   655  		},
   656  		{
   657  			xs: []float64{0, 1, 2},
   658  			ys: []float64{0, 1}},
   659  		{
   660  			xs: []float64{0, 0, 1},
   661  			ys: []float64{0, 0, 0},
   662  		},
   663  		{
   664  			xs: []float64{0, 1, 0},
   665  			ys: []float64{0, 0, 0},
   666  		},
   667  	} {
   668  		var fb FritschButland
   669  		if !panics(func() { _ = fb.Fit(test.xs, test.ys) }) {
   670  			t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys)
   671  		}
   672  	}
   673  }