diff --git a/factory/format.go b/factory/format.go index 200bea1..7d2bd2d 100644 --- a/factory/format.go +++ b/factory/format.go @@ -286,9 +286,14 @@ func DefaultCustomResultStatistics(solution nextroute.Solution) schema.CustomRes } } + unplannedStops := common.MapSlice( + solution.UnPlannedPlanUnits().SolutionPlanUnits(), + toSolutionOutputStops, + ) + return schema.CustomResultStatistics{ ActivatedVehicles: vehicleCount, - UnplannedStops: solution.UnPlannedPlanUnits().Size(), + UnplannedStops: len(unplannedStops), MaxTravelDuration: maxTravelDuration, MaxDuration: maxDuration, MinTravelDuration: minTravelDuration, diff --git a/model_expression_haversine.go b/model_expression_haversine.go index a27cd06..f1f5e98 100644 --- a/model_expression_haversine.go +++ b/model_expression_haversine.go @@ -59,6 +59,9 @@ func (h *haversineExpression) Value( from ModelStop, to ModelStop, ) float64 { + if from == nil || to == nil || !from.Location().IsValid() || !to.Location().IsValid() { + return 0.0 + } return haversineDistance(from.Location(), to.Location()). Value(vehicle.Model().DistanceUnit()) } diff --git a/model_expression_measure_byindex.go b/model_expression_measure_byindex.go index 403816d..5d21813 100644 --- a/model_expression_measure_byindex.go +++ b/model_expression_measure_byindex.go @@ -52,5 +52,8 @@ func (m *measureByIndexExpression) SetName(n string) { } func (m *measureByIndexExpression) Value(_ ModelVehicleType, from, to ModelStop) float64 { + if from == nil || to == nil || !from.Location().IsValid() || !to.Location().IsValid() { + return 0.0 + } return m.measure.Cost(from.(*stopImpl).measureIndex, to.(*stopImpl).measureIndex) } diff --git a/model_expression_measure_bypoint.go b/model_expression_measure_bypoint.go index 12f3cc9..7c5cd8c 100644 --- a/model_expression_measure_bypoint.go +++ b/model_expression_measure_bypoint.go @@ -52,8 +52,14 @@ func (m *measureByPointExpression) SetName(n string) { } func (m *measureByPointExpression) Value(_ ModelVehicleType, from, to ModelStop) float64 { + if from == nil || to == nil { + return 0.0 + } locFrom := from.Location() locTo := to.Location() + if !locFrom.IsValid() || !locTo.IsValid() { + return 0.0 + } value := m.measure.Cost( measure.Point{locFrom.Longitude(), locFrom.Latitude()}, measure.Point{locTo.Longitude(), locTo.Latitude()}, diff --git a/schema/input.go b/schema/input.go index b58fcf8..785f6d0 100644 --- a/schema/input.go +++ b/schema/input.go @@ -5,6 +5,8 @@ package schema import ( "time" + + "github.com/nextmv-io/sdk/measure" ) // Input is the default input schema for nextroute. @@ -226,6 +228,11 @@ type Location struct { Lat float64 `json:"lat" minimum:"-90" maximum:"90"` } +// ToPoint converts a schema.Location to a measure.Point. +func (l Location) ToPoint() measure.Point { + return measure.Point{l.Lon, l.Lat} +} + // DurationGroup represents a group of stops that get additional duration // whenever a stop of the group is approached for the first time. type DurationGroup struct { diff --git a/solve_events.go b/solve_events.go index 43780b7..624f4c9 100644 --- a/solve_events.go +++ b/solve_events.go @@ -87,3 +87,45 @@ func (e *BaseEvent2[S, T]) Trigger(payload1 S, payload2 T) { // Handler2 is a function that handles an event with two payloads. type Handler2[S any, T any] func(payload1 S, payload2 T) + +// BaseEvent3 is a base event type that can be used to implement events +// with three payloads. +type BaseEvent3[S any, T any, U any] struct { + handlers []Handler3[S, T, U] +} + +// Register adds an event handler for this event. +func (e *BaseEvent3[S, T, U]) Register(handler Handler3[S, T, U]) { + e.handlers = append(e.handlers, handler) +} + +// Trigger sends out an event with the payload. +func (e *BaseEvent3[S, T, U]) Trigger(payload1 S, payload2 T, payload3 U) { + for _, handler := range e.handlers { + handler(payload1, payload2, payload3) + } +} + +// Handler3 is a function that handles an event with three payloads. +type Handler3[S any, T any, U any] func(payload1 S, payload2 T, payload3 U) + +// BaseEvent4 is a base event type that can be used to implement events +// with four payloads. +type BaseEvent4[S any, T any, U any, V any] struct { + handlers []Handler4[S, T, U, V] +} + +// Register adds an event handler for this event. +func (e *BaseEvent4[S, T, U, V]) Register(handler Handler4[S, T, U, V]) { + e.handlers = append(e.handlers, handler) +} + +// Trigger sends out an event with the payload. +func (e *BaseEvent4[S, T, U, V]) Trigger(payload1 S, payload2 T, payload3 U, payload4 V) { + for _, handler := range e.handlers { + handler(payload1, payload2, payload3, payload4) + } +} + +// Handler4 is a function that handles an event with four payloads. +type Handler4[S any, T any, U any, V any] func(payload1 S, payload2 T, payload3 U, payload4 V) diff --git a/solve_parallel_events.go b/solve_parallel_events.go new file mode 100644 index 0000000..01087cf --- /dev/null +++ b/solve_parallel_events.go @@ -0,0 +1,35 @@ +// © 2019-present nextmv.io inc + +package nextroute + +// ParallelSolveEvents is a struct that contains events that are fired during a +// solve invocation of the parallel solver. +type ParallelSolveEvents struct { + // End is fired when the parallel solver is done. The first payload is the + // solver, the second payload is the number of iterations, the third payload + // is the best solution found. + End *BaseEvent3[ParallelSolver, int, Solution] + + // NewSolution is fired when a new solution is found. + NewSolution *BaseEvent2[ParallelSolveInformation, Solution] + + // Start is fired when the parallel solver is started. The first payload is + // the parallel solver, the second payload is the options, the third payload + // is the number of parallel runs will be invoked. + Start *BaseEvent3[ParallelSolver, ParallelSolveOptions, int] + // StartSolver is fired when one of the solver that will run in parallel is + // started. The first payload is the parallel solve information, the second + // payload is the solver, the third payload is the solve options, the fourth + // payload is the start solution. + StartSolver *BaseEvent4[ParallelSolveInformation, Solver, SolveOptions, Solution] +} + +// NewParallelSolveEvents creates a new instance of ParallelSolveEvents. +func NewParallelSolveEvents() ParallelSolveEvents { + return ParallelSolveEvents{ + End: &BaseEvent3[ParallelSolver, int, Solution]{}, + NewSolution: &BaseEvent2[ParallelSolveInformation, Solution]{}, + Start: &BaseEvent3[ParallelSolver, ParallelSolveOptions, int]{}, + StartSolver: &BaseEvent4[ParallelSolveInformation, Solver, SolveOptions, Solution]{}, + } +} diff --git a/solve_solver.go b/solve_solver.go index 9450d35..39779cb 100644 --- a/solve_solver.go +++ b/solve_solver.go @@ -10,7 +10,6 @@ import ( "time" "github.com/nextmv-io/nextroute/common" - "github.com/nextmv-io/sdk/run" ) // IntParameterOptions are the options for an integer parameter. @@ -247,7 +246,12 @@ func (s *solveImpl) Solve( s.workSolution = newWorkSolution s.random = rand.New(rand.NewSource(newWorkSolution.Random().Int63())) - start := ctx.Value(run.Start).(time.Time) + start := time.Now() + + ctx, cancel := context.WithDeadline( + ctx, + start.Add(solveOptions.Duration), + ) solveInformation := &solveInformationImpl{ iteration: 0, @@ -268,7 +272,10 @@ func (s *solveImpl) Solve( Error: nil, } go func() { - defer close(solutions) + defer func() { + close(solutions) + cancel() + }() Loop: for iteration := 0; iteration < solveOptions.Iterations; iteration++ { diff --git a/solve_solver_parallel.go b/solve_solver_parallel.go index f7188f5..a18c5b5 100644 --- a/solve_solver_parallel.go +++ b/solve_solver_parallel.go @@ -44,8 +44,10 @@ type ParallelSolver interface { // Solve starts the solving process using the given options. It returns the // solutions as a channel. Solve(context.Context, ParallelSolveOptions, ...Solution) (SolutionChannel, error) - // SolveEvents returns the solve-events used by the solver. + // SolveEvents returns the solve-events used by the individual solver instances. SolveEvents() SolveEvents + // ParallelSolveEvents returns the solve-events used by the parallel solver. + ParallelSolveEvents() ParallelSolveEvents } // SolveOptionsFactory is a factory type for creating new solve options. @@ -92,11 +94,9 @@ func NewSkeletonParallelSolver(model Model) (ParallelSolver, error) { return nil, fmt.Errorf("model cannot be nil") } parallelSolver := ¶llelSolverImpl{ - parallelSolverObservedImpl: parallelSolverObservedImpl{ - observers: make([]ParallelSolverObserver, 0), - }, - solveEvents: NewSolveEvents(), - model: model, + solveEvents: NewSolveEvents(), + parallelSolveEvents: NewParallelSolveEvents(), + model: model, } return parallelSolver, nil @@ -129,71 +129,23 @@ func (s metaSolveInformationImpl) Random() *rand.Rand { return s.random } -// ParallelSolverObserver is the interface for observing the parallel solver. -type ParallelSolverObserver interface { - // OnStart is called when the parallel solver is started. - OnStart( - solver ParallelSolver, - options ParallelSolveOptions, - parallelRuns int, - ) - // OnNewRun is called when a new run is started. - OnNewRun( - solver ParallelSolver, - ) - // OnNewSolution is called when a new solution is found. - OnNewSolution( - solver ParallelSolver, - solution Solution, - ) -} - -type parallelSolverObservedImpl struct { - observers []ParallelSolverObserver -} - -func (o *parallelSolverObservedImpl) AddMetaSearchObserver( - observer ParallelSolverObserver, -) { - o.observers = append(o.observers, observer) -} - -func (o *parallelSolverObservedImpl) OnStart( - solver ParallelSolver, - options ParallelSolveOptions, - parallelRuns int, -) { - for _, observer := range o.observers { - observer.OnStart(solver, options, parallelRuns) - } -} - -func (o *parallelSolverObservedImpl) OnNewRun( - solver ParallelSolver, -) { - for _, observer := range o.observers { - observer.OnNewRun(solver) - } -} - -func (o *parallelSolverObservedImpl) OnNewSolution( - solver ParallelSolver, - solution Solution, -) { - for _, observer := range o.observers { - observer.OnNewSolution(solver, solution) - } -} - type parallelSolverImpl struct { - parallelSolverObservedImpl model Model progression []ProgressionEntry solveEvents SolveEvents + parallelSolveEvents ParallelSolveEvents solveOptionsFactory SolveOptionsFactory solverFactory SolverFactory } +func (s *parallelSolverImpl) ParallelSolveEvents() ParallelSolveEvents { + return s.parallelSolveEvents +} + +func (s *parallelSolverImpl) SolveEvents() SolveEvents { + return s.solveEvents +} + func (s *parallelSolverImpl) Model() Model { return s.model } @@ -278,7 +230,11 @@ func (s *parallelSolverImpl) Solve( } } - start := ctx.Value(run.Start).(time.Time) + start := time.Now() + + if ctx.Value(run.Start) != nil { + start = ctx.Value(run.Start).(time.Time) + } ctx, cancel := context.WithDeadline( ctx, @@ -294,7 +250,11 @@ func (s *parallelSolverImpl) Solve( parallelRuns = runtime.NumCPU() } - s.OnStart(s, options, parallelRuns) + s.ParallelSolveEvents().Start.Trigger( + s, + options, + parallelRuns, + ) bestSolution := solutions[0] @@ -408,6 +368,14 @@ func (s *parallelSolverImpl) Solve( if updatedIterations < 0 { opt.Iterations = int(updatedIterations + int64(opt.Iterations)) } + + s.ParallelSolveEvents().StartSolver.Trigger( + metaSolveInformation, + solver, + opt, + solution, + ) + solutionChannel, err := solver.Solve( ctx, opt, @@ -417,6 +385,13 @@ func (s *parallelSolverImpl) Solve( panic(err) } for sol := range solutionChannel { + if sol.Solution != nil { + s.ParallelSolveEvents().NewSolution.Trigger( + metaSolveInformation, + sol.Solution, + ) + } + syncResultChannel <- solutionContainer{ Solution: sol, Error: sol.Error, @@ -434,11 +409,14 @@ func (s *parallelSolverImpl) Solve( go func() { defer func() { + iterations := int(totalIterations.Load()) if dataMap, ok := ctx.Value(run.Data).(*sync.Map); ok { - converted := int(totalIterations.Load()) + converted := iterations dataMap.Store(Iterations, converted) } close(resultChannel) + + s.ParallelSolveEvents().End.Trigger(s, iterations, bestSolution) }() for solverResult := range syncResultChannel { if solverResult.Error != nil { @@ -468,10 +446,6 @@ func (s *parallelSolverImpl) Solve( return resultChannel, nil } -func (s *parallelSolverImpl) SolveEvents() SolveEvents { - return s.solveEvents -} - func (s *parallelSolverImpl) RegisterEvents( events SolveEvents, ) { diff --git a/solver_parallel.go b/solver_parallel.go index 9a7387b..d1f39f1 100644 --- a/solver_parallel.go +++ b/solver_parallel.go @@ -45,6 +45,10 @@ type parallelSolverWrapperImpl struct { solver ParallelSolver } +func (p *parallelSolverWrapperImpl) ParallelSolveEvents() ParallelSolveEvents { + return p.solver.ParallelSolveEvents() +} + func (p *parallelSolverWrapperImpl) Model() Model { return p.solver.Model() } diff --git a/tests/custom_operators/main.go b/tests/custom_operators/main.go index d8b6c9a..22f1d4f 100755 --- a/tests/custom_operators/main.go +++ b/tests/custom_operators/main.go @@ -6,6 +6,7 @@ package main import ( "context" "log" + "time" "github.com/nextmv-io/nextroute" "github.com/nextmv-io/nextroute/check" @@ -76,6 +77,7 @@ func solver( ) (nextroute.SolveOptions, error) { return nextroute.SolveOptions{ Iterations: 1000, + Duration: 10 * time.Minute, }, nil }, ) diff --git a/tests/golden/testdata/alternates.json.golden b/tests/golden/testdata/alternates.json.golden index 088c610..eac78ce 100644 --- a/tests/golden/testdata/alternates.json.golden +++ b/tests/golden/testdata/alternates.json.golden @@ -261,7 +261,7 @@ "min_duration": 11, "min_stops_in_vehicle": 2, "min_travel_duration": 11, - "unplanned_stops": 2 + "unplanned_stops": 0 }, "duration": 0.123, "value": 4000925.1454996876 diff --git a/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden b/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden index 035d9c7..ecc6b78 100644 --- a/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden @@ -173,7 +173,7 @@ "min_duration": 620, "min_stops_in_vehicle": 1, "min_travel_duration": 20, - "unplanned_stops": 2 + "unplanned_stops": 4 }, "duration": 0.123, "value": 80620.0061571598 diff --git a/tests/golden/testdata/template_input.json.golden b/tests/golden/testdata/template_input.json.golden index 790588b..0d9dd72 100644 --- a/tests/golden/testdata/template_input.json.golden +++ b/tests/golden/testdata/template_input.json.golden @@ -610,7 +610,7 @@ "min_duration": 14135, "min_stops_in_vehicle": 9, "min_travel_duration": 11435, - "unplanned_stops": 3 + "unplanned_stops": 7 }, "duration": 0.123, "value": 2059665.4384450912