Skip to content

Commit

Permalink
optimize: make function converger an interface
Browse files Browse the repository at this point in the history
Fixes #488.
Updates #677.
  • Loading branch information
btracey committed Dec 8, 2018
1 parent c395f06 commit e06b95b
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 37 deletions.
6 changes: 6 additions & 0 deletions optimize/cmaes_test.go
Expand Up @@ -42,6 +42,7 @@ func cmaTestCases() []cmaTestCase {
},
settings: &Settings{
FunctionThreshold: 0.01,
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != FunctionThreshold {
Expand All @@ -63,6 +64,7 @@ func cmaTestCases() []cmaTestCase {
method: &CmaEsChol{},
settings: &Settings{
FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge {
Expand All @@ -88,6 +90,7 @@ func cmaTestCases() []cmaTestCase {
settings: &Settings{
FunctionThreshold: math.Inf(-1),
MajorIterations: 10,
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != IterationLimit {
Expand Down Expand Up @@ -117,6 +120,7 @@ func cmaTestCases() []cmaTestCase {
settings: &Settings{
FunctionThreshold: math.Inf(-1),
FuncEvaluations: 250, // Somewhere in the middle of an iteration.
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != FunctionEvaluationLimit {
Expand Down Expand Up @@ -147,6 +151,7 @@ func cmaTestCases() []cmaTestCase {
},
settings: &Settings{
FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge {
Expand All @@ -172,6 +177,7 @@ func cmaTestCases() []cmaTestCase {
},
settings: &Settings{
FunctionThreshold: math.Inf(-1),
Converger: NeverTerminate{},
},
good: func(result *Result, err error, concurrent int) error {
if result.Status != MethodConverge {
Expand Down
39 changes: 34 additions & 5 deletions optimize/functionconvergence.go
Expand Up @@ -4,10 +4,38 @@

package optimize

import "math"
import (
"math"
)

// FunctionConverge tests for the convergence of function values. See comment
// in Settings.
// Converger returns the convergence of the optimization based on
// locations found during optimization. Converger must not modify the value of
// the provided Location in any of the methods.
type Converger interface {
Init(dim int)
Converged(loc *Location) Status
}

// NeverTerminate implements Converger, always reporting NotTerminated.
type NeverTerminate struct{}

func (NeverTerminate) Init(dim int) {}

func (NeverTerminate) Converged(loc *Location) Status {
return NotTerminated
}

// FunctionConverge tests for insufficient improvement in the optimum value
// over the last iterations. A FunctionConvergence status is returned if
// there is no significant decrease for FunctionConverge.Iterations. A
// significant decrease is considered if
// f < f_best
// and
// f_best - f > FunctionConverge.Relative * maxabs(f, f_best) + FunctionConverge.Absolute
// If the decrease is significant, then the iteration counter is reset and
// f_best is updated.
//
// If FunctionConverge.Iterations == 0, it has no effect.
type FunctionConverge struct {
Absolute float64
Relative float64
Expand All @@ -18,13 +46,14 @@ type FunctionConverge struct {
iter int
}

func (fc *FunctionConverge) Init() {
func (fc *FunctionConverge) Init(dim int) {
fc.first = true
fc.best = 0
fc.iter = 0
}

func (fc *FunctionConverge) FunctionConverged(f float64) Status {
func (fc *FunctionConverge) Converged(l *Location) Status {
f := l.F
if fc.first {
fc.best = f
fc.first = false
Expand Down
2 changes: 1 addition & 1 deletion optimize/global.go
Expand Up @@ -12,7 +12,7 @@ import (
func DefaultSettingsGlobal() *Settings {
return &Settings{
FunctionThreshold: math.Inf(-1),
FunctionConverge: &FunctionConverge{
Converger: &FunctionConverge{
Absolute: 1e-10,
Iterations: 100,
},
Expand Down
4 changes: 3 additions & 1 deletion optimize/listsearch_test.go
Expand Up @@ -48,7 +48,9 @@ func TestListSearch(t *testing.T) {
method := &ListSearch{
Locs: locs,
}
settings := &Settings{}
settings := &Settings{
Converger: NeverTerminate{},
}
initX := make([]float64, c)
result, err := Minimize(p, initX, settings, method)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion optimize/local_example_test.go
Expand Up @@ -22,7 +22,7 @@ func ExampleMinimize() {
settings := optimize.DefaultSettingsLocal()
settings.Recorder = nil
settings.GradientThreshold = 1e-12
settings.FunctionConverge = nil
settings.Converger = optimize.NeverTerminate{}

result, err := optimize.Minimize(p, x, settings, &optimize.BFGS{})
if err != nil {
Expand Down
30 changes: 16 additions & 14 deletions optimize/minimize.go
Expand Up @@ -143,12 +143,17 @@ func Minimize(p Problem, initX []float64, settings *Settings, method Method) (*R
optLoc := newLocation(dim, method)
optLoc.F = math.Inf(1)

if settings.FunctionConverge != nil {
settings.FunctionConverge.Init()
}

initOp, initLoc := getInitLocation(dim, initX, settings.InitValues, method)

converger := settings.Converger
if converger == nil {
converger = &FunctionConverge{
Absolute: 1e-10,
Iterations: 20,
}
}
converger.Init(dim)

stats.Runtime = time.Since(startTime)

// Send initial location to Recorder
Expand All @@ -161,7 +166,7 @@ func Minimize(p Problem, initX []float64, settings *Settings, method Method) (*R

// Run optimization
var status Status
status, err = minimize(&p, method, settings, stats, initOp, initLoc, optLoc, startTime)
status, err = minimize(&p, method, settings, converger, stats, initOp, initLoc, optLoc, startTime)

// Cleanup and collect results
if settings.Recorder != nil && err == nil {
Expand All @@ -184,7 +189,7 @@ func getDefaultMethod(p *Problem) Method {

// minimize performs an optimization. minimize updates the settings and optLoc,
// and returns the final Status and error.
func minimize(prob *Problem, method Method, settings *Settings, stats *Stats, initOp Operation, initLoc, optLoc *Location, startTime time.Time) (Status, error) {
func minimize(prob *Problem, method Method, settings *Settings, converger Converger, stats *Stats, initOp Operation, initLoc, optLoc *Location, startTime time.Time) (Status, error) {
dim := len(optLoc.X)
nTasks := settings.Concurrent
if nTasks == 0 {
Expand Down Expand Up @@ -317,7 +322,7 @@ func minimize(prob *Problem, method Method, settings *Settings, stats *Stats, in
case NoOperation:
// Just send the task back.
case MajorIteration:
status = performMajorIteration(optLoc, task.Location, stats, startTime, settings)
status = performMajorIteration(optLoc, task.Location, stats, converger, startTime, settings)
case MethodDone:
methodDone = true
status = MethodConverge
Expand Down Expand Up @@ -504,7 +509,7 @@ func updateEvaluationStats(stats *Stats, op Operation) {
// the convergence criteria given by settings. Otherwise a corresponding status is
// returned.
// Unlike checkLimits, checkConvergence is called only at MajorIterations.
func checkLocationConvergence(loc *Location, settings *Settings) Status {
func checkLocationConvergence(loc *Location, settings *Settings, converger Converger) Status {
if math.IsInf(loc.F, -1) {
return FunctionNegativeInfinity
}
Expand All @@ -517,10 +522,7 @@ func checkLocationConvergence(loc *Location, settings *Settings) Status {
if loc.F < settings.FunctionThreshold {
return FunctionThreshold
}
if settings.FunctionConverge != nil {
return settings.FunctionConverge.FunctionConverged(loc.F)
}
return NotTerminated
return converger.Converged(loc)
}

// checkEvaluationLimits checks the optimization limits after an evaluation
Expand Down Expand Up @@ -559,11 +561,11 @@ func checkIterationLimits(loc *Location, stats *Stats, settings *Settings) Statu
// performMajorIteration does all of the steps needed to perform a MajorIteration.
// It increments the iteration count, updates the optimal location, and checks
// the necessary convergence criteria.
func performMajorIteration(optLoc, loc *Location, stats *Stats, startTime time.Time, settings *Settings) Status {
func performMajorIteration(optLoc, loc *Location, stats *Stats, converger Converger, startTime time.Time, settings *Settings) Status {
copyLocation(optLoc, loc)
stats.MajorIterations++
stats.Runtime = time.Since(startTime)
status := checkLocationConvergence(optLoc, settings)
status := checkLocationConvergence(optLoc, settings, converger)
if status != NotTerminated {
return status
}
Expand Down
24 changes: 12 additions & 12 deletions optimize/types.go
Expand Up @@ -185,18 +185,18 @@ type Settings struct {
// The default value is 1e-6.
GradientThreshold float64

// FunctionConverge tests that the function value decreases by a
// significant amount over the specified number of iterations.
// Converger checks if the optimization has converged based on the (history
// of) locations found during the optimizaiton. Minimize will pass the
// Location at every MajorIteration to the Converger.
//
// If f < f_best and
// f_best - f > FunctionConverge.Relative * maxabs(f, f_best) + FunctionConverge.Absolute
// then a significant decrease has occurred, and f_best is updated.
//
// If there is no significant decrease for FunctionConverge.Iterations
// major iterations, FunctionConvergence status is returned.
//
// If this is nil or if FunctionConverge.Iterations == 0, it has no effect.
FunctionConverge *FunctionConverge
// If the Converger is nil, a default value of
// FunctionConverge {
// Absolute: 1e-10,
// Iterations: 100,
// }
// will be used. NeverTerminated can be used to always return a
// NotTerminated status.
Converger Converger

// MajorIterations is the maximum number of iterations allowed.
// IterationLimit status is returned if the number of major iterations
Expand Down Expand Up @@ -245,7 +245,7 @@ func DefaultSettingsLocal() *Settings {
return &Settings{
GradientThreshold: defaultGradientAbsTol,
FunctionThreshold: math.Inf(-1),
FunctionConverge: &FunctionConverge{
Converger: &FunctionConverge{
Absolute: 1e-10,
Iterations: 20,
},
Expand Down
8 changes: 5 additions & 3 deletions optimize/unconstrained_test.go
Expand Up @@ -1164,16 +1164,18 @@ func testLocal(t *testing.T, tests []unconstrainedTest, method Method) {
settings.Recorder = nil
if method != nil && method.Needs().Gradient {
// Turn off function convergence checks for gradient-based methods.
settings.FunctionConverge = nil
settings.Converger = NeverTerminate{}
} else {
if test.fIter == 0 {
test.fIter = 20
}
settings.FunctionConverge.Iterations = test.fIter
c := settings.Converger.(*FunctionConverge)
c.Iterations = test.fIter
if test.fAbsTol == 0 {
test.fAbsTol = 1e-12
}
settings.FunctionConverge.Absolute = test.fAbsTol
c.Absolute = test.fAbsTol
settings.Converger = c
}
if test.gradTol == 0 {
test.gradTol = 1e-12
Expand Down

0 comments on commit e06b95b

Please sign in to comment.