// Copyright ©2015 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 functions import ( "math" "testing" "gonum.org/v1/gonum/diff/fd" "gonum.org/v1/gonum/floats" ) // function represents an objective function. type function interface { Func(x []float64) float64 } type gradient interface { Grad(grad, x []float64) } // minimumer is an objective function that can also provide information about // its minima. type minimumer interface { function // Minima returns _known_ minima of the function. Minima() []Minimum } // Minimum represents information about an optimal location of a function. type Minimum struct { // X is the location of the minimum. X may not be nil. X []float64 // F is the value of the objective function at X. F float64 // Global indicates if the location is a global minimum. Global bool } type funcTest struct { X []float64 // F is the expected function value at X. F float64 // Gradient is the expected gradient at X. If nil, it is not evaluated. Gradient []float64 } // TODO(vladimir-ch): Decide and implement an exported testing function: // func Test(f Function, ??? ) ??? { // } const ( defaultTol = 1e-12 defaultGradTol = 1e-9 defaultFDGradTol = 1e-5 ) // testFunction checks that the function can evaluate itself (and its gradient) // correctly. func testFunction(f function, ftests []funcTest, t *testing.T) { // Make a copy of tests because we may append to the slice. tests := make([]funcTest, len(ftests)) copy(tests, ftests) // Get information about the function. fMinima, isMinimumer := f.(minimumer) fGradient, isGradient := f.(gradient) // If the function is a Minimumer, append its minima to the tests. if isMinimumer { for _, minimum := range fMinima.Minima() { // Allocate gradient only if the function can evaluate it. var grad []float64 if isGradient { grad = make([]float64, len(minimum.X)) } tests = append(tests, funcTest{ X: minimum.X, F: minimum.F, Gradient: grad, }) } } for i, test := range tests { F := f.Func(test.X) // Check that the function value is as expected. if math.Abs(F-test.F) > defaultTol { t.Errorf("Test #%d: function value given by Func is incorrect. Want: %v, Got: %v", i, test.F, F) } if test.Gradient == nil { continue } // Evaluate the finite difference gradient. fdGrad := fd.Gradient(nil, f.Func, test.X, &fd.Settings{ Formula: fd.Central, Step: 1e-6, }) // Check that the finite difference and expected gradients match. if !floats.EqualApprox(fdGrad, test.Gradient, defaultFDGradTol) { dist := floats.Distance(fdGrad, test.Gradient, math.Inf(1)) t.Errorf("Test #%d: numerical and expected gradients do not match. |fdGrad - WantGrad|_∞ = %v", i, dist) } // If the function is a Gradient, check that it computes the gradient correctly. if isGradient { grad := make([]float64, len(test.Gradient)) fGradient.Grad(grad, test.X) if !floats.EqualApprox(grad, test.Gradient, defaultGradTol) { dist := floats.Distance(grad, test.Gradient, math.Inf(1)) t.Errorf("Test #%d: gradient given by Grad is incorrect. |grad - WantGrad|_∞ = %v", i, dist) } } } }