diff --git a/go.mod b/go.mod index 11ccbfb..cf0d89f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/nextmv-io/nextroute go 1.21 require ( - github.com/nextmv-io/sdk v1.8.2 + github.com/nextmv-io/sdk v1.8.3-0.20241219091227-002f36a342d6 gonum.org/v1/gonum v0.14.0 ) diff --git a/go.sum b/go.sum index ea77de9..23bbdf4 100644 --- a/go.sum +++ b/go.sum @@ -301,8 +301,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nextmv-io/sdk v1.8.2 h1:9jqtchlgrt7aDzRCRRBr9AM192tRBaN82hUIxpOV/Xg= -github.com/nextmv-io/sdk v1.8.2/go.mod h1:Y48XLPcIOOxRgO86ICNpqGrH2N5+dd1TDNvef/FD2Kc= +github.com/nextmv-io/sdk v1.8.3-0.20241219091227-002f36a342d6 h1:icnvtf2R9Zg9Qs+SyVbQ6HEZ5fPj6A+7ywxOT2lrWus= +github.com/nextmv-io/sdk v1.8.3-0.20241219091227-002f36a342d6/go.mod h1:Y48XLPcIOOxRgO86ICNpqGrH2N5+dd1TDNvef/FD2Kc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/solve_solver.go b/solve_solver.go index a5e4ad2..93c64e0 100644 --- a/solve_solver.go +++ b/solve_solver.go @@ -30,10 +30,47 @@ type SolverOptions struct { Restart IntParameterOptions `json:"restart" usage:"restart parameter"` } +// PlateauOptions define how the solver should react to plateaus, i.e., periods +// without significant improvement in the best solution. The solver stops when +// either the Duration or Iterations condition is met, depending on which occurs +// first. +type PlateauOptions struct { + // Duration is the maximum duration without (significant) improvement. I.e., + // if the solver does not improve the best solution (significantly) within + // this duration, the solver will stop. A duration of 0 means that the + // solver will not check for (significant) improvements based on duration, + // only based on iterations. If both duration and iterations are set, the + // solver will stop if either one of the conditions is met. Default: 0s + // (time-based plateau detection disabled). + Duration time.Duration `json:"duration" usage:"maximum duration without (significant) improvement" default:"0s"` + // Iterations is the maximum number of iterations without (significant) + // improvement. I.e., if the solver does not improve the best solution + // within this number of iterations, the solver will stop. A negative value + // means that the solver will not check for (significant) improvements based + // on iterations, only based on duration. Default: 0 (iteration-based + // plateau detection disabled). + Iterations int `json:"iterations" usage:"maximum number of iterations without (significant) improvement" default:"0"` + // RelativeThreshold defines the minimum relative improvement of the best + // solution withing the plateau duration or iterations to be considered + // significant. E.g., a value of 0.1 means that the best solution must + // improve by at least 10% to be considered significant, thus resetting the + // plateau timer/counter. A negative value means that the relative threshold + // is disabled. + RelativeThreshold float64 `json:"relative_threshold" usage:"relative threshold for significant improvement" default:"0.0"` + // AbsoluteThreshold defines the minimum absolute improvement of the best + // solution withing the plateau duration or iterations to be considered + // significant. E.g., a value of 10 means that the best solution must + // improve by at least 10 to be considered significant, thus resetting the + // plateau timer/counter. A negative value means that the absolute threshold + // is disabled. + AbsoluteThreshold float64 `json:"absolute_threshold" usage:"absolute threshold for significant improvement" default:"-1.0"` +} + // SolveOptions holds the options for the solve process. type SolveOptions struct { - Iterations int `json:"iterations" usage:"maximum number of iterations, -1 assumes no limit" default:"-1"` - Duration time.Duration `json:"duration" usage:"maximum duration of solver in seconds" default:"30s"` + Iterations int `json:"iterations" usage:"maximum number of iterations, -1 assumes no limit" default:"-1"` + Duration time.Duration `json:"duration" usage:"maximum duration of solver in seconds" default:"30s"` + Plateau PlateauOptions `json:"plateau" usage:"plateau options"` } // Solver is the interface for the Adaptive Local Neighborhood Search algorithm @@ -89,6 +126,7 @@ type solveImpl struct { random *rand.Rand solveOperators SolveOperators parameters SolveParameters + plateauTracker *plateauTracker progression []ProgressionEntry } @@ -211,6 +249,11 @@ func (s *solveImpl) invoke( func (s *solveImpl) newBestSolution(solution Solution, solveInformation *solveInformationImpl) { s.bestSolution = solution.Copy() s.solveEvents.NewBestSolution.Trigger(solveInformation) + s.plateauTracker.onImprovement( + time.Since(solveInformation.Start()).Seconds(), + solveInformation.Iteration(), + solveInformation.Solver().BestSolution().Score(), + ) } func (s *solveImpl) Solve( @@ -253,6 +296,10 @@ func (s *solveImpl) Solve( start.Add(solveOptions.Duration), ) + if plateauTrackingActivated(solveOptions.Plateau) { + s.plateauTracker = newPlateauTracker(solveOptions.Plateau) + } + solveInformation := &solveInformationImpl{ iteration: 0, solver: s, @@ -305,6 +352,9 @@ func (s *solveImpl) Solve( Error: nil, } } + if s.plateauTracker.IsStop(iteration, time.Since(start)) { + break Loop + } } } diff --git a/solve_solver_parallel.go b/solve_solver_parallel.go index a18c5b5..affca01 100644 --- a/solve_solver_parallel.go +++ b/solve_solver_parallel.go @@ -21,11 +21,12 @@ const Iterations string = "iterations" // ParallelSolveOptions holds the options for the parallel solver. type ParallelSolveOptions struct { - Iterations int `json:"iterations" usage:"maximum number of iterations, -1 assumes no limit; iterations are counted after start solutions are generated" default:"-1"` - Duration time.Duration `json:"duration" usage:"maximum duration of the solver" default:"5s"` - ParallelRuns int `json:"parallel_runs" usage:"maximum number of parallel runs, -1 results in using all available resources" default:"-1"` - StartSolutions int `json:"start_solutions" usage:"number of solutions to generate on top of those passed in; one solution generated with sweep algorithm, the rest generated randomly" default:"-1"` - RunDeterministically bool `json:"run_deterministically" usage:"run the parallel solver deterministically"` + Iterations int `json:"iterations" usage:"maximum number of iterations, -1 assumes no limit; iterations are counted after start solutions are generated" default:"-1"` + Duration time.Duration `json:"duration" usage:"maximum duration of the solver" default:"5s"` + Plateau PlateauOptions `json:"plateau" usage:"plateau options"` + ParallelRuns int `json:"parallel_runs" usage:"maximum number of parallel runs, -1 results in using all available resources" default:"-1"` + StartSolutions int `json:"start_solutions" usage:"number of solutions to generate on top of those passed in; one solution generated with sweep algorithm, the rest generated randomly" default:"-1"` + RunDeterministically bool `json:"run_deterministically" usage:"run the parallel solver deterministically"` } // ParallelSolver is the interface for parallel solver. The parallel solver will @@ -136,6 +137,7 @@ type parallelSolverImpl struct { parallelSolveEvents ParallelSolveEvents solveOptionsFactory SolveOptionsFactory solverFactory SolverFactory + plateauTracker *plateauTracker } func (s *parallelSolverImpl) ParallelSolveEvents() ParallelSolveEvents { @@ -199,6 +201,7 @@ func (s *parallelSolverImpl) Solve( ParallelRuns: options.ParallelRuns, StartSolutions: options.StartSolutions, RunDeterministically: options.RunDeterministically, + Plateau: options.Plateau, } if interpretedParallelSolveOptions.ParallelRuns == -1 { @@ -230,6 +233,10 @@ func (s *parallelSolverImpl) Solve( } } + if plateauTrackingActivated(interpretedParallelSolveOptions.Plateau) { + s.plateauTracker = newPlateauTracker(interpretedParallelSolveOptions.Plateau) + } + start := time.Now() if ctx.Value(run.Start) != nil { @@ -284,6 +291,11 @@ func (s *parallelSolverImpl) Solve( Value: solutionContainer.Solution.Score(), Iterations: solutionContainer.Iterations, }) + s.plateauTracker.onImprovement( + time.Since(start).Seconds(), + solutionContainer.Iterations, + solutionContainer.Solution.Score(), + ) } } @@ -351,6 +363,12 @@ func (s *parallelSolverImpl) Solve( if totalIterations.Add(1) >= int64(interpretedParallelSolveOptions.Iterations) { cancel() } + if s.plateauTracker.IsStop( + int(totalIterations.Load()), + time.Since(start), + ) { + cancel() + } }) opt, err := s.solveOptionsFactory( diff --git a/solve_terminate.go b/solve_terminate.go new file mode 100644 index 0000000..1c7c645 --- /dev/null +++ b/solve_terminate.go @@ -0,0 +1,119 @@ +// © 2019-present nextmv.io inc + +package nextroute + +import ( + "time" +) + +type plateauTracker struct { + // progression is the value progression of the solver. This is tracked + // separately of any other progression tracking to avoid conflicts. + progression []ProgressionEntry + // durationIndex is the current index of the first progression entry within + // the duration cutoff. + durationIndex int + // iterationsIndex is the current index of the first progression entry + // within the iterations cutoff. + iterationsIndex int + // options are the options for the plateau tracker. + options PlateauOptions +} + +func newPlateauTracker(options PlateauOptions) *plateauTracker { + return &plateauTracker{ + progression: make([]ProgressionEntry, 0), + durationIndex: 0, + iterationsIndex: 0, + options: options, + } +} + +// plateauTrackingActivated returns true if the plateau tracking should be +// activated based on the provided options. +func plateauTrackingActivated(options PlateauOptions) bool { + // We need to be testing within some duration or iteration cutoff. + return (options.Duration > 0 || options.Iterations > 0) && + // We need to have some threshold configured (negative threshold is deactivating the corresponding check). + (options.AbsoluteThreshold >= 0 || options.RelativeThreshold >= 0) +} + +// onImprovement is called to update the plateau tracker whenever a new +// improvement is found. +func (t *plateauTracker) onImprovement(elapsed float64, iterations int, value float64) { + if t == nil { + return + } + // Add the new progression entry. + t.progression = append(t.progression, ProgressionEntry{ + ElapsedSeconds: elapsed, + Value: value, + Iterations: iterations, + }) +} + +// IsStop returns true if the solver should stop due to a detected plateau. +func (t *plateauTracker) IsStop(iterations int, elapsed time.Duration) bool { + if t == nil { + return false + } + + currentValue := t.progression[len(t.progression)-1].Value + + // Check if no significantly improving solutions were found during the + // configured duration. + if t.options.Duration > 0 { + cutoffSeconds := t.options.Duration.Seconds() + elapsedSeconds := elapsed.Seconds() + // Move the duration index to the first entry within the cutoff. + for t.durationIndex < len(t.progression) && + (elapsedSeconds-t.progression[t.durationIndex].ElapsedSeconds) > cutoffSeconds { + t.durationIndex++ + } + // If the duration index is at the end of the progression, no + // improvement was found within the cutoff. + if t.durationIndex == len(t.progression) { + return true + } + // Compare the current value to the value at the duration index. + cutoffValue := t.progression[t.durationIndex].Value + if t.options.AbsoluteThreshold >= 0 && + currentValue-cutoffValue < t.options.AbsoluteThreshold { + return true + } + if t.options.RelativeThreshold >= 0 && + currentValue > 0 && // Relative threshold is only supported for positive values. + (currentValue-cutoffValue)/currentValue < t.options.RelativeThreshold { + return true + } + } + + // Check if no significantly improving solutions were found during the + // configured iterations. + if t.options.Iterations > 0 { + // Move the iterations index to the first entry within the cutoff. + for t.iterationsIndex < len(t.progression) && + iterations-t.progression[t.iterationsIndex].Iterations > t.options.Iterations { + t.iterationsIndex++ + } + // If the iterations index is at the end of the progression, no + // improvement was found within the cutoff. + if t.iterationsIndex == len(t.progression) { + return true + } + // Compare the current value to the value at the iterations index. + cutoffValue := t.progression[t.iterationsIndex].Value + if t.options.AbsoluteThreshold >= 0 && + currentValue-cutoffValue < t.options.AbsoluteThreshold { + return true + } + if t.options.RelativeThreshold >= 0 && + currentValue > 0 && // Relative threshold is only supported for positive values. + (currentValue-cutoffValue)/currentValue < t.options.RelativeThreshold { + return true + } + } + + // No plateau detected. + return false +} diff --git a/solver_parallel.go b/solver_parallel.go index d1f39f1..1a486ef 100644 --- a/solver_parallel.go +++ b/solver_parallel.go @@ -86,6 +86,7 @@ func (p *parallelSolverWrapperImpl) Solve( ParallelRuns: solveOptions.ParallelRuns, StartSolutions: solveOptions.StartSolutions, RunDeterministically: solveOptions.RunDeterministically, + Plateau: solveOptions.Plateau, } if interpretedParallelSolveOptions.ParallelRuns == -1 { diff --git a/tests/plateau_stopping_criterion/input.json b/tests/plateau_stopping_criterion/input.json new file mode 100644 index 0000000..039c6f4 --- /dev/null +++ b/tests/plateau_stopping_criterion/input.json @@ -0,0 +1,21 @@ +{ + "defaults": { + "vehicles": { + "speed": 20, + "start_time": "2023-01-01T06:00:00-06:00", + "end_time": "2023-01-01T10:00:00-06:00" + } + }, + "stops": [ + { + "id": "Fushimi Inari Taisha", + "location": { "lon": 135.772695, "lat": 34.967146 } + } + ], + "vehicles": [ + { + "id": "v1", + "start_location": { "lon": 135.672009, "lat": 35.017209 } + } + ] +} diff --git a/tests/plateau_stopping_criterion/input.json.duration.golden b/tests/plateau_stopping_criterion/input.json.duration.golden new file mode 100644 index 0000000..150e7db --- /dev/null +++ b/tests/plateau_stopping_criterion/input.json.duration.golden @@ -0,0 +1,166 @@ +{ + "options": { + "check": { + "duration": 30000000000, + "verbosity": "off" + }, + "format": { + "disable": { + "progression": true + } + }, + "model": { + "constraints": { + "disable": { + "attributes": false, + "capacities": null, + "capacity": false, + "distance_limit": false, + "groups": false, + "maximum_duration": false, + "maximum_stops": false, + "maximum_wait_stop": false, + "maximum_wait_vehicle": false, + "mixing_items": false, + "precedence": false, + "start_time_windows": false, + "vehicle_end_time": false, + "vehicle_start_time": false + }, + "enable": { + "cluster": false + } + }, + "objectives": { + "capacities": "", + "cluster": 0, + "early_arrival_penalty": 1, + "late_arrival_penalty": 1, + "min_stops": 1, + "stop_balance": 0, + "travel_duration": 0, + "unplanned_penalty": 1, + "vehicle_activation_penalty": 1, + "vehicles_duration": 1 + }, + "properties": { + "disable": { + "duration_groups": false, + "durations": false, + "initial_solution": false, + "stop_duration_multipliers": false + }, + "maximum_time_horizon": 15552000 + }, + "validate": { + "disable": { + "resources": false, + "start_time": false + }, + "enable": { + "matrix": false, + "matrix_asymmetry_tolerance": 20 + } + } + }, + "solve": { + "duration": 10000000000, + "iterations": -1, + "parallel_runs": 1, + "plateau": { + "absolute_threshold": -1, + "duration": 500000000, + "iterations": 0, + "relative_threshold": 0 + }, + "run_deterministically": true, + "start_solutions": 1 + } + }, + "solutions": [ + { + "objective": { + "name": "1 * vehicles_duration + 1 * unplanned_penalty", + "objectives": [ + { + "base": 536.4530622959137, + "factor": 1, + "name": "vehicles_duration", + "value": 536.4530622959137 + }, + { + "factor": 1, + "name": "unplanned_penalty", + "value": 0 + } + ], + "value": 536.4530622959137 + }, + "unplanned": [], + "vehicles": [ + { + "id": "v1", + "route": [ + { + "arrival_time": "2023-01-01T06:00:00-06:00", + "cumulative_travel_duration": 0, + "end_time": "2023-01-01T06:00:00-06:00", + "start_time": "2023-01-01T06:00:00-06:00", + "stop": { + "id": "v1-start", + "location": { + "lat": 35.017209, + "lon": 135.672009 + } + }, + "travel_duration": 0 + }, + { + "arrival_time": "2023-01-01T06:08:56-06:00", + "cumulative_travel_distance": 10729, + "cumulative_travel_duration": 536, + "end_time": "2023-01-01T06:08:56-06:00", + "start_time": "2023-01-01T06:08:56-06:00", + "stop": { + "id": "Fushimi Inari Taisha", + "location": { + "lat": 34.967146, + "lon": 135.772695 + } + }, + "travel_distance": 10729, + "travel_duration": 536 + } + ], + "route_duration": 536, + "route_travel_distance": 10729, + "route_travel_duration": 536 + } + ] + } + ], + "statistics": { + "result": { + "custom": { + "activated_vehicles": 1, + "max_duration": 536, + "max_stops_in_vehicle": 1, + "max_travel_duration": 536, + "min_duration": 536, + "min_stops_in_vehicle": 1, + "min_travel_duration": 536, + "unplanned_stops": 0 + }, + "duration": 0.123, + "value": 536.4530622959137 + }, + "run": { + "duration": 0.5, + "iterations": 123 + }, + "schema": "v1" + }, + "version": { + "sdk": "VERSION" + } +} diff --git a/tests/plateau_stopping_criterion/input.json.iterations.golden b/tests/plateau_stopping_criterion/input.json.iterations.golden new file mode 100644 index 0000000..1ed9f71 --- /dev/null +++ b/tests/plateau_stopping_criterion/input.json.iterations.golden @@ -0,0 +1,166 @@ +{ + "options": { + "check": { + "duration": 30000000000, + "verbosity": "off" + }, + "format": { + "disable": { + "progression": true + } + }, + "model": { + "constraints": { + "disable": { + "attributes": false, + "capacities": null, + "capacity": false, + "distance_limit": false, + "groups": false, + "maximum_duration": false, + "maximum_stops": false, + "maximum_wait_stop": false, + "maximum_wait_vehicle": false, + "mixing_items": false, + "precedence": false, + "start_time_windows": false, + "vehicle_end_time": false, + "vehicle_start_time": false + }, + "enable": { + "cluster": false + } + }, + "objectives": { + "capacities": "", + "cluster": 0, + "early_arrival_penalty": 1, + "late_arrival_penalty": 1, + "min_stops": 1, + "stop_balance": 0, + "travel_duration": 0, + "unplanned_penalty": 1, + "vehicle_activation_penalty": 1, + "vehicles_duration": 1 + }, + "properties": { + "disable": { + "duration_groups": false, + "durations": false, + "initial_solution": false, + "stop_duration_multipliers": false + }, + "maximum_time_horizon": 15552000 + }, + "validate": { + "disable": { + "resources": false, + "start_time": false + }, + "enable": { + "matrix": false, + "matrix_asymmetry_tolerance": 20 + } + } + }, + "solve": { + "duration": 10000000000, + "iterations": -1, + "parallel_runs": 1, + "plateau": { + "absolute_threshold": -1, + "duration": 0, + "iterations": 20, + "relative_threshold": 0 + }, + "run_deterministically": true, + "start_solutions": 1 + } + }, + "solutions": [ + { + "objective": { + "name": "1 * vehicles_duration + 1 * unplanned_penalty", + "objectives": [ + { + "base": 536.4530622959137, + "factor": 1, + "name": "vehicles_duration", + "value": 536.4530622959137 + }, + { + "factor": 1, + "name": "unplanned_penalty", + "value": 0 + } + ], + "value": 536.4530622959137 + }, + "unplanned": [], + "vehicles": [ + { + "id": "v1", + "route": [ + { + "arrival_time": "2023-01-01T06:00:00-06:00", + "cumulative_travel_duration": 0, + "end_time": "2023-01-01T06:00:00-06:00", + "start_time": "2023-01-01T06:00:00-06:00", + "stop": { + "id": "v1-start", + "location": { + "lat": 35.017209, + "lon": 135.672009 + } + }, + "travel_duration": 0 + }, + { + "arrival_time": "2023-01-01T06:08:56-06:00", + "cumulative_travel_distance": 10729, + "cumulative_travel_duration": 536, + "end_time": "2023-01-01T06:08:56-06:00", + "start_time": "2023-01-01T06:08:56-06:00", + "stop": { + "id": "Fushimi Inari Taisha", + "location": { + "lat": 34.967146, + "lon": 135.772695 + } + }, + "travel_distance": 10729, + "travel_duration": 536 + } + ], + "route_duration": 536, + "route_travel_distance": 10729, + "route_travel_duration": 536 + } + ] + } + ], + "statistics": { + "result": { + "custom": { + "activated_vehicles": 1, + "max_duration": 536, + "max_stops_in_vehicle": 1, + "max_travel_duration": 536, + "min_duration": 536, + "min_stops_in_vehicle": 1, + "min_travel_duration": 536, + "unplanned_stops": 0 + }, + "duration": 0.123, + "value": 536.4530622959137 + }, + "run": { + "duration": 0.123, + "iterations": 21 + }, + "schema": "v1" + }, + "version": { + "sdk": "VERSION" + } +} diff --git a/tests/plateau_stopping_criterion/main.go b/tests/plateau_stopping_criterion/main.go new file mode 100644 index 0000000..0428ae8 --- /dev/null +++ b/tests/plateau_stopping_criterion/main.go @@ -0,0 +1,64 @@ +// © 2019-present nextmv.io inc + +// package main holds the implementation of the nextroute template. +package main + +import ( + "context" + "log" + + "github.com/nextmv-io/nextroute" + "github.com/nextmv-io/nextroute/check" + "github.com/nextmv-io/nextroute/factory" + "github.com/nextmv-io/nextroute/schema" + "github.com/nextmv-io/sdk/run" + runSchema "github.com/nextmv-io/sdk/run/schema" +) + +func main() { + runner := run.CLI(solver) + err := runner.Run(context.Background()) + if err != nil { + log.Fatal(err) + } +} + +type options struct { + Model factory.Options `json:"model,omitempty"` + Solve nextroute.ParallelSolveOptions `json:"solve,omitempty"` + Format nextroute.FormatOptions `json:"format,omitempty"` + Check check.Options `json:"check,omitempty"` +} + +func solver( + ctx context.Context, + input schema.Input, + options options, +) (runSchema.Output, error) { + model, err := factory.NewModel(input, options.Model) + if err != nil { + return runSchema.Output{}, err + } + + solver, err := nextroute.NewParallelSolver(model) + if err != nil { + return runSchema.Output{}, err + } + + solutions, err := solver.Solve(ctx, options.Solve) + if err != nil { + return runSchema.Output{}, err + } + last, err := solutions.Last() + if err != nil { + return runSchema.Output{}, err + } + + output, err := check.Format(ctx, options, options.Check, solver, last) + if err != nil { + return runSchema.Output{}, err + } + output.Statistics.Result.Custom = factory.DefaultCustomResultStatistics(last) + + return output, nil +} diff --git a/tests/plateau_stopping_criterion/main_test.go b/tests/plateau_stopping_criterion/main_test.go new file mode 100644 index 0000000..062da7f --- /dev/null +++ b/tests/plateau_stopping_criterion/main_test.go @@ -0,0 +1,84 @@ +// © 2019-present nextmv.io inc + +package main + +import ( + "os" + "testing" + + "github.com/nextmv-io/sdk/golden" +) + +func TestMain(m *testing.M) { + golden.Setup() + code := m.Run() + golden.Teardown() + os.Exit(code) +} + +// TestPlateauIterations executes a golden file test applying the plateau +// stopping criterion using a number of iterations. +func TestPlateauIterations(t *testing.T) { + config := golden.Config{ + GoldenExtension: ".iterations.golden", + Args: []string{ + "-solve.duration", "10s", + "-format.disable.progression", + "-solve.parallelruns", "1", + "-solve.rundeterministically", + "-solve.startsolutions", "1", + "-solve.plateau.iterations", "20", + }, + TransientFields: []golden.TransientField{ + {Key: "$.version.sdk", Replacement: golden.StableVersion}, + {Key: "$.statistics.result.duration", Replacement: golden.StableFloat}, + {Key: "$.statistics.run.duration", Replacement: golden.StableFloat}, + {Key: ".solutions[0].check.duration_used", Replacement: golden.StableFloat}, + }, + Thresholds: golden.Tresholds{ + Float: 0.01, + CustomThresholds: golden.CustomThresholds{ + Int: map[string]int{ + "$.statistics.run.iterations": 0, + }, + }, + }, + } + golden.FileTest(t, "input.json", config) +} + +// TestPlateauDuration executes a golden file test applying the plateau +// stopping criterion using a duration. +func TestPlateauDuration(t *testing.T) { + config := golden.Config{ + GoldenExtension: ".duration.golden", + Args: []string{ + "-solve.duration", "10s", + "-format.disable.progression", + "-solve.parallelruns", "1", + "-solve.rundeterministically", + "-solve.startsolutions", "1", + "-solve.plateau.duration", "0.5s", + }, + TransientFields: []golden.TransientField{ + {Key: "$.version.sdk", Replacement: golden.StableVersion}, + {Key: "$.statistics.result.duration", Replacement: golden.StableFloat}, + {Key: "$.statistics.run.iterations", Replacement: golden.StableInt}, + {Key: ".solutions[0].check.duration_used", Replacement: golden.StableFloat}, + }, + OutputProcessConfig: golden.OutputProcessConfig{ + RoundingConfig: []golden.RoundingConfig{ + {Key: "$.statistics.run.duration", Precision: 1}, + }, + }, + Thresholds: golden.Tresholds{ + Float: 0.01, + CustomThresholds: golden.CustomThresholds{ + Float: map[string]float64{ + "$.statistics.run.duration": 0.2, + }, + }, + }, + } + golden.FileTest(t, "input.json", config) +}