gonum.org/v1/gonum@v0.14.0/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  	"gonum.org/v1/gonum/floats"
    12  	"gonum.org/v1/gonum/floats/scalar"
    13  	"gonum.org/v1/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  }
   674  
   675  func TestPiecewiseCubicFitWithSecondDerivatives(t *testing.T) {
   676  	t.Parallel()
   677  	const tol = 1e-14
   678  	xs := []float64{-2, 0, 3}
   679  	ys := []float64{2.5, 1, 2.5}
   680  	d2ydx2s := []float64{1, 2, 3}
   681  	var pc PiecewiseCubic
   682  	pc.fitWithSecondDerivatives(xs, ys, d2ydx2s)
   683  	m := len(xs) - 1
   684  	if pc.lastY != ys[m] {
   685  		t.Errorf("Mismatch in lastY: got %v, want %g", pc.lastY, ys[m])
   686  	}
   687  	if !floats.Equal(pc.xs, xs) {
   688  		t.Errorf("Mismatch in xs: got %v, want %v", pc.xs, xs)
   689  	}
   690  	for i := 0; i < len(xs); i++ {
   691  		yHat := pc.Predict(xs[i])
   692  		if math.Abs(yHat-ys[i]) > tol {
   693  			t.Errorf("Mismatch in predicted Y[%d]: got %v, want %g", i, yHat, ys[i])
   694  		}
   695  		var d2ydx2Hat float64
   696  		if i < m {
   697  			d2ydx2Hat = 2 * pc.coeffs.At(i, 2)
   698  		} else {
   699  			d2ydx2Hat = 2*pc.coeffs.At(m-1, 2) + 6*pc.coeffs.At(m-1, 3)*(xs[m]-xs[m-1])
   700  		}
   701  		if math.Abs(d2ydx2Hat-d2ydx2s[i]) > tol {
   702  			t.Errorf("Mismatch in predicted d2Y/dX2[%d]: got %v, want %g", i, d2ydx2Hat, d2ydx2s[i])
   703  		}
   704  	}
   705  	// Test pc.lastDyDx without copying verbatim the calculation from the tested method:
   706  	lastDyDx := pc.PredictDerivative(xs[m] - tol/1000)
   707  	if math.Abs(lastDyDx-pc.lastDyDx) > tol {
   708  		t.Errorf("Mismatch in lastDxDy: got %v, want %g", pc.lastDyDx, lastDyDx)
   709  	}
   710  }
   711  
   712  func TestPiecewiseCubicFitWithSecondDerivativesErrors(t *testing.T) {
   713  	t.Parallel()
   714  	for _, test := range []struct {
   715  		xs, ys, d2ydx2s []float64
   716  	}{
   717  		{
   718  			xs:      []float64{0, 1, 2},
   719  			ys:      []float64{10, 20},
   720  			d2ydx2s: []float64{0, 0, 0},
   721  		},
   722  		{
   723  			xs:      []float64{0, 1, 1},
   724  			ys:      []float64{10, 20, 30},
   725  			d2ydx2s: []float64{0, 0, 0, 0},
   726  		},
   727  		{
   728  			xs:      []float64{0},
   729  			ys:      []float64{0},
   730  			d2ydx2s: []float64{0},
   731  		},
   732  		{
   733  			xs:      []float64{0, 1, 1},
   734  			ys:      []float64{10, 20, 10},
   735  			d2ydx2s: []float64{0, 0, 0},
   736  		},
   737  	} {
   738  		var pc PiecewiseCubic
   739  		if !panics(func() { pc.fitWithSecondDerivatives(test.xs, test.ys, test.d2ydx2s) }) {
   740  			t.Errorf("expected panic for xs: %v, ys: %v and d2ydx2s: %v", test.xs, test.ys, test.d2ydx2s)
   741  		}
   742  	}
   743  }
   744  
   745  func TestMakeCubicSplineSecondDerivativeEquations(t *testing.T) {
   746  	t.Parallel()
   747  	const tol = 1e-15
   748  	xs := []float64{-1, 0, 2}
   749  	ys := []float64{2, 0, 2}
   750  	n := len(xs)
   751  	a := mat.NewTridiag(n, nil, nil, nil)
   752  	b := mat.NewVecDense(n, nil)
   753  	makeCubicSplineSecondDerivativeEquations(a, b, xs, ys)
   754  	if b.Len() != n {
   755  		t.Errorf("Mismatch in b size: got %v, want %d", b.Len(), n)
   756  	}
   757  	r, c := a.Dims()
   758  	if r != n || c != n {
   759  		t.Errorf("Mismatch in A size: got %d x %d, want %d x %d", r, c, n, n)
   760  	}
   761  	expectedB := mat.NewVecDense(3, []float64{0, 3, 0})
   762  	var diffB mat.VecDense
   763  	diffB.SubVec(b, expectedB)
   764  	if diffB.Norm(math.Inf(1)) > tol {
   765  		t.Errorf("Mismatch in b values: got %v, want %v", b, expectedB)
   766  	}
   767  	expectedA := mat.NewDense(3, 3, []float64{0, 0, 0, 1 / 6., 1, 2 / 6., 0, 0, 0})
   768  	var diffA mat.Dense
   769  	diffA.Sub(a, expectedA)
   770  	if diffA.Norm(math.Inf(1)) > tol {
   771  		t.Errorf("Mismatch in A values: got %v, want %v", a, expectedA)
   772  	}
   773  }
   774  
   775  func TestNaturalCubicFit(t *testing.T) {
   776  	t.Parallel()
   777  	xs := []float64{-1, 0, 2, 3.5}
   778  	ys := []float64{2, 0, 2, 1.5}
   779  	var nc NaturalCubic
   780  	err := nc.Fit(xs, ys)
   781  	if err != nil {
   782  		t.Errorf("Error when fitting NaturalCubic: %v", err)
   783  	}
   784  	testXs := []float64{-1, -0.99, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.49, 3.5}
   785  	// From scipy.interpolate.CubicSpline:
   786  	want := []float64{
   787  		2.0,
   788  		1.9737725526315788,
   789  		0.7664473684210527,
   790  		0.0,
   791  		0.027960526315789477,
   792  		0.6184210526315789,
   793  		1.3996710526315788,
   794  		2.0,
   795  		2.1403508771929824,
   796  		1.9122807017543857,
   797  		1.508859403508772,
   798  		1.5}
   799  	for i := 0; i < len(testXs); i++ {
   800  		got := nc.Predict(testXs[i])
   801  		if math.Abs(got-want[i]) > 1e-14 {
   802  			t.Errorf("Mismatch in predicted Y value for x = %g: got %v, want %g", testXs[i], got, want[i])
   803  		}
   804  		got = nc.PredictDerivative(testXs[i])
   805  		want := discrDerivPredict(&nc, xs[0], xs[len(xs)-1], testXs[i], 1e-9)
   806  		if math.Abs(got-want) > 1e-6 {
   807  			t.Errorf("Mismatch in predicted dY/dX value for x = %g: got %v, want %g", testXs[i], got, want)
   808  		}
   809  	}
   810  }
   811  
   812  func TestClampedCubicFit(t *testing.T) {
   813  	t.Parallel()
   814  	xs := []float64{-1, 0, 2, 3.5}
   815  	ys := []float64{2, 0, 2, 1.5}
   816  	var cc ClampedCubic
   817  	err := cc.Fit(xs, ys)
   818  	if err != nil {
   819  		t.Errorf("Error when fitting ClampedCubic: %v", err)
   820  	}
   821  	testXs := []float64{-1, -0.99, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.49, 3.5}
   822  	// From scipy.interpolate.CubicSpline:
   823  	want := []float64{
   824  		2.0,
   825  		1.999564111111111,
   826  		1.2021604938271606,
   827  		0.0,
   828  		-0.20833333333333343,
   829  		0.41975308641975284,
   830  		1.337962962962962,
   831  		2.0,
   832  		2.026748971193416,
   833  		1.7078189300411522,
   834  		1.5001129711934151,
   835  		1.4999999999999996,
   836  	}
   837  	for i := 0; i < len(testXs); i++ {
   838  		got := cc.Predict(testXs[i])
   839  		if math.Abs(got-want[i]) > 1e-14 {
   840  			t.Errorf("Mismatch in predicted Y value for x = %g: got %v, want %g", testXs[i], got, want[i])
   841  		}
   842  		got = cc.PredictDerivative(testXs[i])
   843  		want := discrDerivPredict(&cc, xs[0], xs[len(xs)-1], testXs[i], 1e-9)
   844  		if math.Abs(got-want) > 1e-6 {
   845  			t.Errorf("Mismatch in predicted dY/dX value for x = %g: got %v, want %g", testXs[i], got, want)
   846  		}
   847  	}
   848  }
   849  
   850  func TestNotAKnotCubicFit(t *testing.T) {
   851  	t.Parallel()
   852  	xs := []float64{-1, 0, 2, 3.5}
   853  	ys := []float64{2, 0, 2, 1.5}
   854  	var nak NotAKnotCubic
   855  	err := nak.Fit(xs, ys)
   856  	if err != nil {
   857  		t.Errorf("Error when fitting NotAKnotCubic: %v", err)
   858  	}
   859  	testXs := []float64{-1, -0.99, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.49, 3.5}
   860  	// From scipy.interpolate.CubicSpline:
   861  	want := []float64{
   862  		2.0,
   863  		1.961016095238095,
   864  		0.5582010582010581,
   865  		0.0,
   866  		0.09523809523809526,
   867  		0.6137566137566137,
   868  		1.3253968253968258,
   869  		2.0,
   870  		2.407407407407407,
   871  		2.3174603174603177,
   872  		1.5249675026455023,
   873  		1.5000000000000004,
   874  	}
   875  	for i := 0; i < len(testXs); i++ {
   876  		got := nak.Predict(testXs[i])
   877  		if math.Abs(got-want[i]) > 1e-14 {
   878  			t.Errorf("Mismatch in predicted Y value for x = %g: got %v, want %g", testXs[i], got, want[i])
   879  		}
   880  		got = nak.PredictDerivative(testXs[i])
   881  		want := discrDerivPredict(&nak, xs[0], xs[len(xs)-1], testXs[i], 1e-9)
   882  		if math.Abs(got-want) > 1e-6 {
   883  			t.Errorf("Mismatch in predicted dY/dX value for x = %g: got %v, want %g", testXs[i], got, want)
   884  		}
   885  	}
   886  }