diff --git a/factory/constraint_max_travel_duration.go b/factory/constraint_max_travel_duration.go new file mode 100644 index 0000000..0e98b77 --- /dev/null +++ b/factory/constraint_max_travel_duration.go @@ -0,0 +1,52 @@ +// © 2019-present nextmv.io inc + +package factory + +import ( + "time" + + "github.com/nextmv-io/nextroute" + "github.com/nextmv-io/nextroute/schema" +) + +// addMaximumTravelDurationConstraint +// TODO +func addMaximumTravelDurationConstraint( + input schema.Input, + model nextroute.Model, + _ Options, +) (nextroute.Model, error) { + maximumWaitPerVehicle := nextroute.NewVehicleTypeDurationExpression("vehicle-travel-max", model.MaxDuration()) + maximumTravelPresent := addMaximumTravelDurationVehicles(input, model, maximumWaitPerVehicle) + + if maximumTravelPresent { + cnstr, err := nextroute.NewMaximumDurationConstraint(maximumWaitPerVehicle) + if err != nil { + return nil, err + } + err = model.AddConstraint(cnstr) + if err != nil { + return nil, err + } + } + + return model, nil +} + +func addMaximumTravelDurationVehicles( + input schema.Input, + model nextroute.Model, + vehicleLimit nextroute.VehicleTypeDurationExpression, +) bool { + present := false + modelVehicles := model.Vehicles() + for v, inputVehicle := range input.Vehicles { + if inputVehicle.MaxTravelDuration == nil { + continue + } + present = true + + vehicleLimit.SetDuration(modelVehicles[v].VehicleType(), time.Duration(*inputVehicle.MaxTravelDuration)*time.Second) + } + return present +} diff --git a/factory/factory.go b/factory/factory.go index 20f5667..581266b 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -75,6 +75,10 @@ func appendConstraintModifiers( modifiers = append(modifiers, addMaximumDurationConstraint) } + if !options.Constraints.Disable.MaximumTravelDuration { + modifiers = append(modifiers, addMaximumTravelDurationConstraint) + } + if !options.Constraints.Disable.Precedence { modifiers = append(modifiers, addPrecedenceInformation) } diff --git a/factory/model.go b/factory/model.go index 9935061..652d20e 100644 --- a/factory/model.go +++ b/factory/model.go @@ -8,20 +8,21 @@ package factory type Options struct { Constraints struct { Disable struct { - Attributes bool `json:"attributes" usage:"ignore the compatibility attributes constraint"` - Capacity bool `json:"capacity" usage:"ignore the capacity constraint for all resources"` - Capacities []string `json:"capacities" usage:"ignore the capacity constraint for the given resource names"` - DistanceLimit bool `json:"distance_limit" usage:"ignore the distance limit constraint"` - Groups bool `json:"groups" usage:"ignore the groups constraint"` - MaximumDuration bool `json:"maximum_duration" usage:"ignore the maximum duration constraint"` - MaximumStops bool `json:"maximum_stops" usage:"ignore the maximum stops constraint"` - MaximumWaitStop bool `json:"maximum_wait_stop" usage:"ignore the maximum stop wait constraint"` - MaximumWaitVehicle bool `json:"maximum_wait_vehicle" usage:"ignore the maximum vehicle wait constraint"` - MixingItems bool `json:"mixing_items" usage:"ignore the do not mix items constraint"` - Precedence bool `json:"precedence" usage:"ignore the precedence (pickups & deliveries) constraint"` - VehicleStartTime bool `json:"vehicle_start_time" usage:"ignore the vehicle start time constraint"` - VehicleEndTime bool `json:"vehicle_end_time" usage:"ignore the vehicle end time constraint"` - StartTimeWindows bool `json:"start_time_windows" usage:"ignore the start time windows constraint"` + Attributes bool `json:"attributes" usage:"ignore the compatibility attributes constraint"` + Capacity bool `json:"capacity" usage:"ignore the capacity constraint for all resources"` + Capacities []string `json:"capacities" usage:"ignore the capacity constraint for the given resource names"` + DistanceLimit bool `json:"distance_limit" usage:"ignore the distance limit constraint"` + Groups bool `json:"groups" usage:"ignore the groups constraint"` + MaximumDuration bool `json:"maximum_duration" usage:"ignore the maximum duration constraint"` + MaximumTravelDuration bool `json:"maximum_travel_duration" usage:"ignore the maximum travel duration constraint"` + MaximumStops bool `json:"maximum_stops" usage:"ignore the maximum stops constraint"` + MaximumWaitStop bool `json:"maximum_wait_stop" usage:"ignore the maximum stop wait constraint"` + MaximumWaitVehicle bool `json:"maximum_wait_vehicle" usage:"ignore the maximum vehicle wait constraint"` + MixingItems bool `json:"mixing_items" usage:"ignore the do not mix items constraint"` + Precedence bool `json:"precedence" usage:"ignore the precedence (pickups & deliveries) constraint"` + VehicleStartTime bool `json:"vehicle_start_time" usage:"ignore the vehicle start time constraint"` + VehicleEndTime bool `json:"vehicle_end_time" usage:"ignore the vehicle end time constraint"` + StartTimeWindows bool `json:"start_time_windows" usage:"ignore the start time windows constraint"` } `json:"disable"` Enable struct { Cluster bool `json:"cluster" usage:"enable the cluster constraint"` diff --git a/model_constraint_maximum_duration.go b/model_constraint_maximum_duration.go index bff13cc..e715adb 100644 --- a/model_constraint_maximum_duration.go +++ b/model_constraint_maximum_duration.go @@ -54,13 +54,18 @@ func (l *maximumDurationConstraintImpl) EstimateIsViolated( ) (isViolated bool, stopPositionsHint StopPositionsHint) { moveImpl := move.(*solutionMoveStopsImpl) vehicle := moveImpl.vehicle() + + if vehicle.IsEmpty() { + return false, constNoPositionsHint + } + vehicleType := vehicle.ModelVehicle().VehicleType() dependentOnTime := vehicleType.TravelDurationExpression().IsDependentOnTime() maximumValue := l.maximum.Value(vehicleType, nil, nil) - startValue := vehicle.first().StartValue() + startValue := vehicle.first().next().StartValue() previous, _ := moveImpl.previous() endValue := previous.EndValue() @@ -97,8 +102,14 @@ func (l *maximumDurationConstraintImpl) EstimateIsViolated( } func (l *maximumDurationConstraintImpl) DoesVehicleHaveViolations(vehicle SolutionVehicle) bool { - return vehicle.DurationValue() > - l.maximum.Value(vehicle.ModelVehicle().VehicleType(), nil, nil) + if vehicle.IsEmpty() { + return false + } + // TODO: improve + endValue := vehicle.Last().EndValue() - vehicle.First().Next().StartValue() + max := l.maximum.Value(vehicle.ModelVehicle().VehicleType(), nil, nil) + return endValue > max + } func (l *maximumDurationConstraintImpl) IsTemporal() bool { diff --git a/schema/input.go b/schema/input.go index 785f6d0..5d14f47 100644 --- a/schema/input.go +++ b/schema/input.go @@ -127,8 +127,12 @@ type Vehicle struct { MaxStops *int `json:"max_stops,omitempty"` // Speed of the vehicle in meters per second. Speed *float64 `json:"speed,omitempty" minimumExclusive:"0"` - // MaxDuration maximum duration in seconds that the vehicle can travel. + // MaxDuration maximum duration in seconds that the vehicle can travel after + // its start time. MaxDuration *int `json:"max_duration,omitempty" minimum:"0"` + // MaxTravelDuration maximum duration in seconds that the vehicle can + // travel between first and last stop. + MaxTravelDuration *int `json:"max_travel_duration,omitempty" minimum:"0"` // MaxWait maximum aggregated waiting time that the vehicle can wait across route stops. MaxWait *int `json:"max_wait,omitempty" minimum:"0"` // ActivationPenalty penalty of using the vehicle. diff --git a/tests/golden/testdata/max_duration_early_start.json b/tests/golden/testdata/max_duration_early_start.json new file mode 100644 index 0000000..99b66f8 --- /dev/null +++ b/tests/golden/testdata/max_duration_early_start.json @@ -0,0 +1,25 @@ +{ + "defaults": { + "vehicles": { + "speed": 5, + "start_time": "2023-01-01T12:00:00Z" + }, + "stops": { + "unplanned_penalty": 2000000000, + "duration": 300 + } + }, + "stops": [ + { + "id": "Fushimi Inari Taisha", + "location": { "lon": 135.772695, "lat": 34.967146 }, + "start_time_window": ["2023-01-01T18:00:00Z", "2023-01-01T18:05:00Z"] + } + ], + "vehicles": [ + { + "id": "v1", + "max_travel_duration": 3600 + } + ] +}