diff --git a/tests/stop_balancing_objective/input.json b/tests/stop_balancing_objective/input.json new file mode 100644 index 0000000..297a519 --- /dev/null +++ b/tests/stop_balancing_objective/input.json @@ -0,0 +1,193 @@ +{ + "defaults": { + "vehicles": { + "speed": 10 + } + }, + "stops": [ + { + "id": "s1", + "location": { + "lon": -78.90919, + "lat": 35.72389 + } + }, + { + "id": "s2", + "location": { + "lon": -78.813862, + "lat": 35.75712 + } + }, + { + "id": "s3", + "location": { + "lon": -78.92996, + "lat": 35.932795 + } + }, + { + "id": "s4", + "location": { + "lon": -78.505745, + "lat": 35.77772 + } + }, + { + "id": "s5", + "location": { + "lon": -78.75084, + "lat": 35.732995 + } + }, + { + "id": "s6", + "location": { + "lon": -78.788025, + "lat": 35.813025 + } + }, + { + "id": "s7", + "location": { + "lon": -78.749391, + "lat": 35.74261 + } + }, + { + "id": "s8", + "location": { + "lon": -78.94658, + "lat": 36.039135 + } + }, + { + "id": "s9", + "location": { + "lon": -78.64972, + "lat": 35.64796 + } + }, + { + "id": "s10", + "location": { + "lon": -78.747955, + "lat": 35.672955 + } + }, + { + "id": "s11", + "location": { + "lon": -78.83403, + "lat": 35.77013 + } + }, + { + "id": "s12", + "location": { + "lon": -78.864465, + "lat": 35.782855 + } + }, + { + "id": "s13", + "location": { + "lon": -78.952142, + "lat": 35.88029 + } + }, + { + "id": "s14", + "location": { + "lon": -78.52748, + "lat": 35.961465 + } + }, + { + "id": "s15", + "location": { + "lon": -78.89832, + "lat": 35.83202 + } + }, + { + "id": "s16", + "location": { + "lon": -78.63216, + "lat": 35.83458 + } + }, + { + "id": "s17", + "location": { + "lon": -78.76063, + "lat": 35.67337 + } + }, + { + "id": "s18", + "location": { + "lon": -78.911485, + "lat": 36.009015 + } + }, + { + "id": "s19", + "location": { + "lon": -78.522705, + "lat": 35.93663 + } + }, + { + "id": "s20", + "location": { + "lon": -78.995162, + "lat": 35.97414 + } + }, + { + "id": "s21", + "location": { + "lon": -78.50509, + "lat": 35.7606 + } + }, + { + "id": "s22", + "location": { + "lon": -78.828547, + "lat": 35.962635 + }, + "precedes": ["s16", "s23"] + }, + { + "id": "s23", + "location": { + "lon": -78.60914, + "lat": 35.84616 + } + }, + { + "id": "s24", + "location": { + "lon": -78.65521, + "lat": 35.740605 + } + }, + { + "id": "s25", + "location": { + "lon": -78.92051, + "lat": 35.887575 + } + } + ], + "vehicles": [ + { + "id": "vehicle-0" + }, + { + "id": "vehicle-1" + } + ] +} diff --git a/tests/stop_balancing_objective/input.json.golden b/tests/stop_balancing_objective/input.json.golden new file mode 100644 index 0000000..e0e5bc9 --- /dev/null +++ b/tests/stop_balancing_objective/input.json.golden @@ -0,0 +1,464 @@ +{ + "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": 1000, + "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 + } + }, + "validate": { + "disable": { + "resources": false, + "start_time": false + }, + "enable": { + "matrix": false, + "matrix_asymmetry_tolerance": 20 + } + } + }, + "solve": { + "duration": 10000000000, + "iterations": 10000, + "parallel_runs": 1, + "run_deterministically": true, + "start_solutions": 1 + } + }, + "solutions": [ + { + "objective": { + "name": "1 * vehicles_duration + 1 * unplanned_penalty + 1000 * stop_balance", + "objectives": [ + { + "base": 15055.87046845047, + "factor": 1, + "name": "vehicles_duration", + "value": 15055.87046845047 + }, + { + "factor": 1, + "name": "unplanned_penalty", + "value": 0 + }, + { + "base": 13, + "factor": 1000, + "name": "stop_balance", + "value": 13000 + } + ], + "value": 28055.87046845047 + }, + "unplanned": [], + "vehicles": [ + { + "id": "vehicle-0", + "route": [ + { + "cumulative_travel_duration": 0, + "stop": { + "id": "s22", + "location": { + "lat": 35.962635, + "lon": -78.828547 + } + }, + "travel_duration": 0 + }, + { + "cumulative_travel_distance": 9071, + "cumulative_travel_duration": 907, + "stop": { + "id": "s18", + "location": { + "lat": 36.009015, + "lon": -78.911485 + } + }, + "travel_distance": 9071, + "travel_duration": 907 + }, + { + "cumulative_travel_distance": 13672, + "cumulative_travel_duration": 1367, + "stop": { + "id": "s8", + "location": { + "lat": 36.039135, + "lon": -78.94658 + } + }, + "travel_distance": 4601, + "travel_duration": 460 + }, + { + "cumulative_travel_distance": 22117, + "cumulative_travel_duration": 2211, + "stop": { + "id": "s20", + "location": { + "lat": 35.97414, + "lon": -78.995162 + } + }, + "travel_distance": 8445, + "travel_duration": 844 + }, + { + "cumulative_travel_distance": 29572, + "cumulative_travel_duration": 2957, + "stop": { + "id": "s3", + "location": { + "lat": 35.932795, + "lon": -78.92996 + } + }, + "travel_distance": 7455, + "travel_duration": 745 + }, + { + "cumulative_travel_distance": 34671, + "cumulative_travel_duration": 3467, + "stop": { + "id": "s25", + "location": { + "lat": 35.887575, + "lon": -78.92051 + } + }, + "travel_distance": 5099, + "travel_duration": 509 + }, + { + "cumulative_travel_distance": 37633, + "cumulative_travel_duration": 3763, + "stop": { + "id": "s13", + "location": { + "lat": 35.88029, + "lon": -78.952142 + } + }, + "travel_distance": 2962, + "travel_duration": 296 + }, + { + "cumulative_travel_distance": 44867, + "cumulative_travel_duration": 4487, + "stop": { + "id": "s15", + "location": { + "lat": 35.83202, + "lon": -78.89832 + } + }, + "travel_distance": 7234, + "travel_duration": 723 + }, + { + "cumulative_travel_distance": 55033, + "cumulative_travel_duration": 5503, + "stop": { + "id": "s6", + "location": { + "lat": 35.813025, + "lon": -78.788025 + } + }, + "travel_distance": 10166, + "travel_duration": 1016 + }, + { + "cumulative_travel_distance": 69288, + "cumulative_travel_duration": 6929, + "stop": { + "id": "s16", + "location": { + "lat": 35.83458, + "lon": -78.63216 + } + }, + "travel_distance": 14255, + "travel_duration": 1425 + }, + { + "cumulative_travel_distance": 71730, + "cumulative_travel_duration": 7173, + "stop": { + "id": "s23", + "location": { + "lat": 35.84616, + "lon": -78.60914 + } + }, + "travel_distance": 2442, + "travel_duration": 244 + }, + { + "cumulative_travel_distance": 84451, + "cumulative_travel_duration": 8445, + "stop": { + "id": "s19", + "location": { + "lat": 35.93663, + "lon": -78.522705 + } + }, + "travel_distance": 12721, + "travel_duration": 1272 + }, + { + "cumulative_travel_distance": 87245, + "cumulative_travel_duration": 8725, + "stop": { + "id": "s14", + "location": { + "lat": 35.961465, + "lon": -78.52748 + } + }, + "travel_distance": 2794, + "travel_duration": 279 + } + ], + "route_duration": 8725, + "route_travel_distance": 87245, + "route_travel_duration": 8725 + }, + { + "id": "vehicle-1", + "route": [ + { + "cumulative_travel_duration": 0, + "stop": { + "id": "s4", + "location": { + "lat": 35.77772, + "lon": -78.505745 + } + }, + "travel_duration": 0 + }, + { + "cumulative_travel_distance": 1904, + "cumulative_travel_duration": 190, + "stop": { + "id": "s21", + "location": { + "lat": 35.7606, + "lon": -78.50509 + } + }, + "travel_distance": 1904, + "travel_duration": 190 + }, + { + "cumulative_travel_distance": 15632, + "cumulative_travel_duration": 1563, + "stop": { + "id": "s24", + "location": { + "lat": 35.740605, + "lon": -78.65521 + } + }, + "travel_distance": 13728, + "travel_duration": 1372 + }, + { + "cumulative_travel_distance": 25945, + "cumulative_travel_duration": 2594, + "stop": { + "id": "s9", + "location": { + "lat": 35.64796, + "lon": -78.64972 + } + }, + "travel_distance": 10313, + "travel_duration": 1031 + }, + { + "cumulative_travel_distance": 35244, + "cumulative_travel_duration": 3524, + "stop": { + "id": "s10", + "location": { + "lat": 35.672955, + "lon": -78.747955 + } + }, + "travel_distance": 9299, + "travel_duration": 929 + }, + { + "cumulative_travel_distance": 36389, + "cumulative_travel_duration": 3639, + "stop": { + "id": "s17", + "location": { + "lat": 35.67337, + "lon": -78.76063 + } + }, + "travel_distance": 1145, + "travel_duration": 114 + }, + { + "cumulative_travel_distance": 43077, + "cumulative_travel_duration": 4308, + "stop": { + "id": "s5", + "location": { + "lat": 35.732995, + "lon": -78.75084 + } + }, + "travel_distance": 6688, + "travel_duration": 668 + }, + { + "cumulative_travel_distance": 44154, + "cumulative_travel_duration": 4415, + "stop": { + "id": "s7", + "location": { + "lat": 35.74261, + "lon": -78.749391 + } + }, + "travel_distance": 1077, + "travel_duration": 107 + }, + { + "cumulative_travel_distance": 50191, + "cumulative_travel_duration": 5019, + "stop": { + "id": "s2", + "location": { + "lat": 35.75712, + "lon": -78.813862 + } + }, + "travel_distance": 6037, + "travel_duration": 603 + }, + { + "cumulative_travel_distance": 52515, + "cumulative_travel_duration": 5252, + "stop": { + "id": "s11", + "location": { + "lat": 35.77013, + "lon": -78.83403 + } + }, + "travel_distance": 2324, + "travel_duration": 232 + }, + { + "cumulative_travel_distance": 55603, + "cumulative_travel_duration": 5560, + "stop": { + "id": "s12", + "location": { + "lat": 35.782855, + "lon": -78.864465 + } + }, + "travel_distance": 3088, + "travel_duration": 308 + }, + { + "cumulative_travel_distance": 63302, + "cumulative_travel_duration": 6330, + "stop": { + "id": "s1", + "location": { + "lat": 35.72389, + "lon": -78.90919 + } + }, + "travel_distance": 7699, + "travel_duration": 769 + } + ], + "route_duration": 6330, + "route_travel_distance": 63302, + "route_travel_duration": 6330 + } + ] + } + ], + "statistics": { + "result": { + "custom": { + "activated_vehicles": 2, + "max_duration": 8725, + "max_stops_in_vehicle": 13, + "max_travel_duration": 8725, + "min_duration": 6330, + "min_stops_in_vehicle": 12, + "min_travel_duration": 6330, + "unplanned_stops": 0 + }, + "duration": 0.123, + "value": 28055.87046845047 + }, + "run": { + "duration": 0.123, + "iterations": 10000 + }, + "schema": "v1" + }, + "version": { + "sdk": "VERSION" + } +} diff --git a/tests/stop_balancing_objective/main.go b/tests/stop_balancing_objective/main.go new file mode 100644 index 0000000..0e28415 --- /dev/null +++ b/tests/stop_balancing_objective/main.go @@ -0,0 +1,65 @@ +// © 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) { + options.Model.Objectives.StopBalance = 1000.0 + 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/stop_balancing_objective/main_test.go b/tests/stop_balancing_objective/main_test.go new file mode 100644 index 0000000..4b50910 --- /dev/null +++ b/tests/stop_balancing_objective/main_test.go @@ -0,0 +1,44 @@ +// © 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) +} + +// TestGolden executes a golden file test, where the .json input is fed and an +// output is expected. +func TestGolden(t *testing.T) { + golden.FileTests( + t, + "input.json", + golden.Config{ + Args: []string{ + "-solve.duration", "10s", + "-format.disable.progression", + "-solve.parallelruns", "1", + "-solve.iterations", "10000", + "-solve.rundeterministically", + "-solve.startsolutions", "1", + }, + TransientFields: []golden.TransientField{ + {Key: "$.version.sdk", Replacement: golden.StableVersion}, + {Key: "$.statistics.result.duration", Replacement: golden.StableFloat}, + {Key: "$.statistics.run.duration", Replacement: golden.StableFloat}, + }, + Thresholds: golden.Tresholds{ + Float: 0.01, + }, + }, + ) +}