diff --git a/.golangci.yml b/.golangci.yml index 25344d3..e0c0d33 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -148,6 +148,13 @@ issues: linters: - gocyclo # Next route tests and methods contain complex code hard to simplify. + - path: model_constraint_interleave\.go + linters: + - nestif + - path: solution_move_stops_generator\.go + linters: + - nestif + - gocyclo - path: model_expression_time_dependent_test\.go linters: - gocyclo diff --git a/factory/constraint_interleave.go b/factory/constraint_interleave.go new file mode 100644 index 0000000..85995c8 --- /dev/null +++ b/factory/constraint_interleave.go @@ -0,0 +1,119 @@ +// © 2019-present nextmv.io inc + +package factory + +import ( + "fmt" + + "github.com/nextmv-io/nextroute" + "github.com/nextmv-io/nextroute/schema" +) + +// addInterleaveConstraint adds a constraint which limits stop groups to be +// interleaved. +func addInterleaveConstraint( + input schema.Input, + model nextroute.Model, + options Options, +) (nextroute.Model, error) { + if input.StopGroups == nil || options.Constraints.Disable.Groups { + return model, nil + } + + data, err := getModelData(model) + if err != nil { + return nil, err + } + + planUnits := make(nextroute.ModelPlanUnits, 0, len(*input.StopGroups)) + for idx, stops := range *input.StopGroups { + if len(stops) == 0 { + return nil, fmt.Errorf("stop group %v is empty", idx) + } + stop := stops[0] + modelStop, err := model.Stop(data.stopIDToIndex[stop]) + if err != nil { + return nil, err + } + if !modelStop.HasPlanStopsUnit() { + return nil, fmt.Errorf("stop %s does not have a plan-stops unit", stop) + } + planStopsUnit := modelStop.PlanStopsUnit() + if planUnitsUnit, hasPlanUnitsUnit := planStopsUnit.PlanUnitsUnit(); hasPlanUnitsUnit { + if numberOfStops(planUnitsUnit) != len(stops) { + return nil, + fmt.Errorf( + "stop group %v starting with stop %v is not a plan unit with %v stops,"+ + " but has %v stops instead, probably because there are inter stop group precedence"+ + " relationships which are not allowed with interleave constraint", + idx, + stop, + len(stops), + numberOfStops(planUnitsUnit), + ) + } + planUnits = append(planUnits, planUnitsUnit) + } else { + if numberOfStops(planStopsUnit) != len(stops) { + return nil, + fmt.Errorf( + "stop group %v starting with stop %v is not a plan unit with %v stops,"+ + " but has %v stops instead, probably because there are inter stop group precedence"+ + " relationships which are not allowed with interleave constraint", + idx, + stop, + len(stops), + numberOfStops(planStopsUnit), + ) + } + planUnits = append(planUnits, planStopsUnit) + } + } + + if len(planUnits) <= 1 { + return model, nil + } + + interleaveConstraint, err := nextroute.NewInterleaveConstraint() + if err != nil { + return model, err + } + + for i, p1 := range planUnits { + inputPlanUnits := make(nextroute.ModelPlanUnits, 0, len(planUnits)-1) + for j, p2 := range planUnits { + if i != j { + inputPlanUnits = append(inputPlanUnits, p2) + } + } + err = interleaveConstraint.DisallowInterleaving(p1, inputPlanUnits) + if err != nil { + return model, err + } + } + + err = model.AddConstraint(interleaveConstraint) + if err != nil { + return model, err + } + + return model, nil +} + +func numberOfStops(planUnit nextroute.ModelPlanUnit) int { + if planStopsUnit, isPlanStopsUnit := planUnit.(nextroute.ModelPlanStopsUnit); isPlanStopsUnit { + return len(planStopsUnit.Stops()) + } + count := 0 + if planUnitsUnit, isPlanUnitsUnit := planUnit.(nextroute.ModelPlanUnitsUnit); isPlanUnitsUnit { + for _, unit := range planUnitsUnit.PlanUnits() { + count += numberOfStops(unit) + } + } + if planUnitsUnit, isPlanUnitsUnit := planUnit.PlanUnitsUnit(); isPlanUnitsUnit { + for _, unit := range planUnitsUnit.PlanUnits() { + count += numberOfStops(unit) + } + } + return count +} diff --git a/factory/factory.go b/factory/factory.go index 20f5667..9dab27b 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -48,6 +48,10 @@ func getModifiersFromOptions(options Options) []modelModifier { modifiers = append(modifiers, addInitialSolution) } + if options.Constraints.Enable.Interleave { + modifiers = append(modifiers, addInterleaveConstraint) + } + return modifiers } diff --git a/factory/model.go b/factory/model.go index 9935061..c1cacca 100644 --- a/factory/model.go +++ b/factory/model.go @@ -24,7 +24,8 @@ type Options struct { 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"` + Cluster bool `json:"cluster" usage:"enable the cluster constraint"` + Interleave bool `json:"interleave" usage:"enable no overlapping of stop groups"` } `json:"enable"` } `json:"constraints"` Objectives struct { diff --git a/model.go b/model.go index ef421e8..3867523 100644 --- a/model.go +++ b/model.go @@ -60,6 +60,10 @@ type Model interface { // locked after a solution has been created using the model. IsLocked() bool + // InterleaveConstraint returns the interleave constraint of the model if + // it exists, otherwise returns nil. + InterleaveConstraint() InterleaveConstraint + // NewPlanSequence creates a new plan sequence. A plan sequence is a plan // unit. A plan unit is a collection of stops which are always planned and // unplanned as a single unit. In this case they have to be planned as a @@ -213,7 +217,8 @@ func NewModel() (Model, error) { // planunit without any relationship, we will still fully explore all // permutations. To find a better number we would have to run // experiments on pathologic cases. - sequenceSampleSize: 24, + sequenceSampleSize: 24, + interleaveConstraint: nil, } if m.epoch.Second() != 0 || m.epoch.Nanosecond() != 0 { @@ -260,6 +265,7 @@ type modelImpl struct { mutex sync.RWMutex isLocked bool disallowedSuccessors [][]bool + interleaveConstraint InterleaveConstraint } func (m *modelImpl) Vehicles() ModelVehicles { @@ -391,6 +397,10 @@ func (m *modelImpl) addToCheckAt(checkAt CheckedAt, constraint ModelConstraint) m.constraintMap[checkAt] = append(m.constraintMap[checkAt], constraint) } +func (m *modelImpl) InterleaveConstraint() InterleaveConstraint { + return m.interleaveConstraint +} + func (m *modelImpl) AddConstraint(constraint ModelConstraint) error { if m.IsLocked() { return fmt.Errorf(lockErrorMessage, "constraint") @@ -403,6 +413,23 @@ func (m *modelImpl) AddConstraint(constraint ModelConstraint) error { reflect.TypeOf(constraint).String(), ) } + if _, ok := constraint.(SuccessorConstraint); ok { + if _, alreadyOneAdded := existingConstraint.(SuccessorConstraint); alreadyOneAdded { + return fmt.Errorf( + "only one SuccessorConstraint can be added to the model", + ) + } + } + if _, ok := constraint.(InterleaveConstraint); ok { + if m.interleaveConstraint != nil { + return fmt.Errorf( + "only one InterleaveConstraint can be added to the model", + ) + } + } + } + if _, ok := constraint.(InterleaveConstraint); ok { + m.interleaveConstraint = constraint.(InterleaveConstraint) } if _, ok := constraint.(ConstraintDataUpdater); ok { return fmt.Errorf( diff --git a/model_constraint_interleave.go b/model_constraint_interleave.go new file mode 100644 index 0000000..d1719d5 --- /dev/null +++ b/model_constraint_interleave.go @@ -0,0 +1,510 @@ +// © 2019-present nextmv.io inc + +package nextroute + +import ( + "fmt" + "math" + + "github.com/nextmv-io/nextroute/common" +) + +// InterleaveConstraint is a constraint that disallows certain target to be +// interleaved. +type InterleaveConstraint interface { + ModelConstraint + // DisallowInterleaving disallows the given planUnits to be interleaved. + DisallowInterleaving(source ModelPlanUnit, targets []ModelPlanUnit) error + + // DisallowedInterleaves returns the disallowed interleaves. + DisallowedInterleaves() []DisallowedInterleave + + // SourceDisallowedInterleaves returns the disallowed interleaves for the + // given source. + SourceDisallowedInterleaves(source ModelPlanUnit) []DisallowedInterleave + + // TargetDisallowedInterleaves returns the disallowed interleaves for the + // given target. + TargetDisallowedInterleaves(target ModelPlanUnit) []DisallowedInterleave + + // UpdateConstraintStopData is called when a stop has been added to a + // solution for each stop in a vehicle after the added stop. + UpdateConstraintStopData(s SolutionStop) (Copier, error) +} + +// NewInterleaveConstraint returns a new InterleaveConstraint. +func NewInterleaveConstraint() (InterleaveConstraint, error) { + return &interleaveConstraintImpl{ + modelConstraintImpl: newModelConstraintImpl( + "interleave", + ModelExpressions{}, + ), + disallowedInterleaves: make([]DisallowedInterleave, 0), + sourceDisallowedInterleaves: nil, + targetDisallowedInterleaves: nil, + }, nil +} + +// DisallowedInterleave is a disallowed interleave between a target and a set of +// sources. +type DisallowedInterleave interface { + // Target returns the target plan unit. This plan unit cannot be interleaved + // with the sources. + Target() ModelPlanUnit + + // Sources returns the sources that cannot be interleaved with the target. + Sources() []ModelPlanUnit +} + +type disallowedInterleaveImpl struct { + target ModelPlanUnit + sources []ModelPlanUnit +} + +func (d *disallowedInterleaveImpl) Target() ModelPlanUnit { + return d.target +} + +func (d *disallowedInterleaveImpl) Sources() []ModelPlanUnit { + return d.sources +} + +func newDisallowedInterleave( + target ModelPlanUnit, + sources []ModelPlanUnit, +) DisallowedInterleave { + return &disallowedInterleaveImpl{ + target: target, + sources: common.DefensiveCopy(sources), + } +} + +type interleaveConstraintImpl struct { + modelConstraintImpl + disallowedInterleaves []DisallowedInterleave + sourceDisallowedInterleaves map[ModelPlanUnit][]DisallowedInterleave + targetDisallowedInterleaves map[ModelPlanUnit][]DisallowedInterleave +} + +type interleaveSolutionStopSpan struct { + first int + last int +} + +type interleaveConstraintData struct { + solutionPlanStopUnits map[int]interleaveSolutionStopSpan +} + +func (i *interleaveConstraintData) Copy() Copier { + solutionPlanStopUnits := make(map[int]interleaveSolutionStopSpan, len(i.solutionPlanStopUnits)) + + for k := range i.solutionPlanStopUnits { + solutionPlanStopUnits[k] = interleaveSolutionStopSpan{ + first: i.solutionPlanStopUnits[k].first, + last: i.solutionPlanStopUnits[k].last, + } + } + + return &interleaveConstraintData{ + solutionPlanStopUnits: solutionPlanStopUnits, + } +} + +func (l *interleaveConstraintImpl) UpdateConstraintStopData(s SolutionStop) (Copier, error) { + if !s.IsLast() { + return nil, nil + } + + data := interleaveConstraintData{ + solutionPlanStopUnits: make(map[int]interleaveSolutionStopSpan), + } + + modelPlanStopsUnits := map[SolutionPlanStopsUnit]struct{}{} + + stop := s.Vehicle().First().Next() + for !stop.IsLast() { + planStopsUnit := stop.PlanStopsUnit() + + stop = stop.Next() + + if _, ok := modelPlanStopsUnits[planStopsUnit]; ok { + continue + } + + if modelPlanUnitsUnit, ok := planStopsUnit.ModelPlanStopsUnit().PlanUnitsUnit(); ok { + modelPlanStopsUnits[planStopsUnit] = struct{}{} + + planUnitsUnitFirst := math.MaxInt64 + planUnitsUnitLast := math.MinInt64 + + for _, planUnit := range modelPlanUnitsUnit.PlanUnits() { + if modelPlanStopsUnit, ok := planUnit.(ModelPlanStopsUnit); ok { + solutionPlanStopsUnit := s.Solution().SolutionPlanStopsUnit(modelPlanStopsUnit) + if solutionPlanStopsUnit.IsPlanned() { + solutionStops := solutionPlanStopsUnit.SolutionStops() + first := solutionStops[0].Position() + last := solutionStops[len(solutionStops)-1].Position() + + data.solutionPlanStopUnits[modelPlanStopsUnit.Index()] = interleaveSolutionStopSpan{ + first: first, + last: last, + } + + if first < planUnitsUnitFirst { + planUnitsUnitFirst = first + } + if last > planUnitsUnitLast { + planUnitsUnitLast = last + } + } + } + } + data.solutionPlanStopUnits[modelPlanUnitsUnit.Index()] = interleaveSolutionStopSpan{ + first: planUnitsUnitFirst, + last: planUnitsUnitLast, + } + } + } + + return &data, nil +} + +func (l *interleaveConstraintImpl) SourceDisallowedInterleaves( + source ModelPlanUnit, +) []DisallowedInterleave { + if l.sourceDisallowedInterleaves != nil { + if disallowedInterleaves, ok := l.sourceDisallowedInterleaves[source]; ok { + return disallowedInterleaves + } + return []DisallowedInterleave{} + } + + found := make([]DisallowedInterleave, 0) + for _, disallowedInterleave := range l.disallowedInterleaves { + for _, s := range disallowedInterleave.Sources() { + if source == s { + found = append(found, disallowedInterleave) + } + } + } + return found +} + +func (l *interleaveConstraintImpl) TargetDisallowedInterleaves( + target ModelPlanUnit, +) []DisallowedInterleave { + if l.targetDisallowedInterleaves != nil { + if disallowedInterleaves, ok := l.targetDisallowedInterleaves[target]; ok { + return disallowedInterleaves + } + return []DisallowedInterleave{} + } + + found := make([]DisallowedInterleave, 0) + for _, disallowedInterleave := range l.disallowedInterleaves { + if disallowedInterleave.Target() == target { + found = append(found, disallowedInterleave) + } + } + return found +} + +func (l *interleaveConstraintImpl) DisallowedInterleaves() []DisallowedInterleave { + return l.disallowedInterleaves +} + +func addToMap( + planUnit ModelPlanUnit, + mapUnit map[ModelPlanUnit][]DisallowedInterleave, + disallowedInterleave DisallowedInterleave, +) { + if modelPlanUnitsUnit, ok := planUnit.(ModelPlanUnitsUnit); ok { + for _, pu := range modelPlanUnitsUnit.PlanUnits() { + addToMap(pu, mapUnit, disallowedInterleave) + } + return + } + + if _, ok := mapUnit[planUnit]; !ok { + mapUnit[planUnit] = []DisallowedInterleave{} + } + + disallowedInterleaves := mapUnit[planUnit] + disallowedInterleaves = append(disallowedInterleaves, disallowedInterleave) + mapUnit[planUnit] = disallowedInterleaves +} + +func (l *interleaveConstraintImpl) Lock(_ Model) error { + l.sourceDisallowedInterleaves = make(map[ModelPlanUnit][]DisallowedInterleave) + l.targetDisallowedInterleaves = make(map[ModelPlanUnit][]DisallowedInterleave) + + for _, disallowedInterleave := range l.disallowedInterleaves { + addToMap(disallowedInterleave.Target(), l.targetDisallowedInterleaves, disallowedInterleave) + + for _, source := range disallowedInterleave.Sources() { + addToMap(source, l.sourceDisallowedInterleaves, disallowedInterleave) + } + } + return nil +} + +func verifyPlanUnitAllOnSameVehicle(planUnit ModelPlanUnit, preFix string) error { + if modelPlanUnitsUnit, isModelPlanUnitsUnit := planUnit.(ModelPlanUnitsUnit); isModelPlanUnitsUnit { + if modelPlanUnitsUnit.PlanAll() && !modelPlanUnitsUnit.SameVehicle() { + return fmt.Errorf( + "%s, all plan units in a conjunction must be on the same vehicle", + preFix, + ) + } + for _, planUnit := range modelPlanUnitsUnit.PlanUnits() { + if err := verifyPlanUnitAllOnSameVehicle(planUnit, preFix); err != nil { + return err + } + } + } + return nil +} + +func (l *interleaveConstraintImpl) DisallowInterleaving(target ModelPlanUnit, sources []ModelPlanUnit) error { + if target == nil { + return fmt.Errorf("target cannot be nil") + } + + if sources == nil { + return fmt.Errorf("sources cannot be nil") + } + + if len(sources) == 0 { + return nil + } + + if _, hasPlanUnitsUnit := target.PlanUnitsUnit(); hasPlanUnitsUnit { + return fmt.Errorf("target cannot be a plan unit part of a PlanUnitsUnit") + } + + if modelPlanStopsUnit, ok := target.(ModelPlanStopsUnit); ok { + if modelPlanStopsUnit.Stops()[0].Model().IsLocked() { + return fmt.Errorf(lockErrorMessage, "DisallowInterleaving") + } + } + + for idx, source := range sources { + if source == nil { + return fmt.Errorf("source[%v] cannot be nil", idx) + } + if _, hasPlanUnitsUnit := source.PlanUnitsUnit(); hasPlanUnitsUnit { + return fmt.Errorf("source at index %v cannot be a plan unit part of a PlanUnitsUnit", idx) + } + if source == target { + return fmt.Errorf("target is also in a source") + } + } + uniqueSources := common.UniqueDefined(sources, func(t ModelPlanUnit) int { + return t.Index() + }) + if len(uniqueSources) != len(sources) { + return fmt.Errorf("sources cannot have duplicate plan units") + } + // check the type of planUnit and + err := verifyPlanUnitAllOnSameVehicle(target, "target") + if err != nil { + return err + } + for idx, unit := range sources { + err = verifyPlanUnitAllOnSameVehicle(unit, fmt.Sprintf("sources[%v]", idx)) + if err != nil { + return err + } + } + + index := common.FindIndex(l.disallowedInterleaves, func(disallowedInterleave DisallowedInterleave) bool { + return disallowedInterleave.Target() == target + }) + if index < 0 { + l.disallowedInterleaves = append(l.disallowedInterleaves, newDisallowedInterleave(target, sources)) + } else { + l.disallowedInterleaves[index].(*disallowedInterleaveImpl).sources = append( + l.disallowedInterleaves[index].(*disallowedInterleaveImpl).sources, + sources..., + ) + l.disallowedInterleaves[index].(*disallowedInterleaveImpl).sources = common.UniqueDefined( + l.disallowedInterleaves[index].(*disallowedInterleaveImpl).sources, + func(t ModelPlanUnit) int { + return t.Index() + }, + ) + } + + return nil +} + +func (l *interleaveConstraintImpl) String() string { + return l.name +} + +func (l *interleaveConstraintImpl) EstimationCost() Cost { + return LinearStop +} + +func isViolatedPositions(sourceFirstPosition, sourceLastPosition, targetFirstPosition, targetLastPosition int) bool { + // S===S + // T=========T + if sourceFirstPosition > targetFirstPosition && + sourceLastPosition < targetLastPosition { + return true + } + + // S=====S + // T=========T + if sourceFirstPosition < targetFirstPosition && + sourceLastPosition > targetFirstPosition && + sourceLastPosition < targetLastPosition { + return true + } + // S=====S + // T=========T + if sourceFirstPosition > targetFirstPosition && + sourceFirstPosition < targetLastPosition && + sourceLastPosition > targetLastPosition { + return true + } + + return false +} +func (l *interleaveConstraintImpl) EstimateIsViolated( + move SolutionMoveStops, +) (isViolated bool, stopPositionsHint StopPositionsHint) { + solution := move.Solution() + + moveImpl := move.(*solutionMoveStopsImpl) + vehicle := moveImpl.vehicle() + data := vehicle.last().ConstraintData(l).(*interleaveConstraintData) + + solutionMoveStops := move.(*solutionMoveStopsImpl) + + generator := newSolutionStopGenerator(*solutionMoveStops, true, true) + defer generator.release() + + newPositions := make(map[SolutionStop]int) + oldPositions := make([]SolutionStop, 0, vehicle.NumberOfStops()+2) + + position := 0 + for solutionStop, ok := generator.next(); ok; solutionStop, ok = generator.next() { + newPositions[solutionStop] = position + if solutionStop.IsPlanned() { + oldPositions = append(oldPositions, solutionStop) + } + position++ + } + + newPlanUnitSpanFirstPosition := move.Previous().Position() + 1 + newPlanUnitSpanLastPosition := move.Next().Position() + len(move.PlanStopsUnit().SolutionStops()) - 1 + + // Check if the plan unit we are moving is a plan units unit, if it is we + // need to check if the plan units that are already are planned of this unit + // spans the new positions of the plan unit we are moving, if so we are good + if modelPlanUnitsUnit, hasModelPlanUnitsUnit := + move.PlanStopsUnit().ModelPlanUnit().PlanUnitsUnit(); hasModelPlanUnitsUnit { + if _, ok := data.solutionPlanStopUnits[modelPlanUnitsUnit.Index()]; ok { + firstSolutionStop := oldPositions[data.solutionPlanStopUnits[modelPlanUnitsUnit.Index()].first] + if newPositions[firstSolutionStop] < newPlanUnitSpanFirstPosition { + newPlanUnitSpanFirstPosition = newPositions[firstSolutionStop] + } + + lastSolutionStop := oldPositions[data.solutionPlanStopUnits[modelPlanUnitsUnit.Index()].last] + if newPositions[lastSolutionStop] > newPlanUnitSpanLastPosition { + newPlanUnitSpanLastPosition = newPositions[lastSolutionStop] + } + } + } + + // Check if the plan unit we are moving is a target + if targetDisallowedInterleaves, isTargetPlanUnit := + l.targetDisallowedInterleaves[move.PlanStopsUnit().ModelPlanUnit()]; isTargetPlanUnit { + for _, disallowedInterleave := range targetDisallowedInterleaves { + for _, sourcePlanUnit := range disallowedInterleave.Sources() { + sourceSolutionPlanUnit := move.Solution().SolutionPlanUnit(sourcePlanUnit) + if sourceSolutionPlanUnit.IsPlanned() { + var sourceSpanFirst, sourceSpanLast SolutionStop + solutionPlanStopsUnits := sourceSolutionPlanUnit.PlannedPlanStopsUnits() + for _, solutionPlanStopsUnit := range solutionPlanStopsUnits { + if solutionPlanStopsUnit.SolutionStops()[0].Vehicle() != move.Vehicle() { + continue + } + + sourceSpanFirst, sourceSpanLast = determineFirstLastSolutionStops( + sourceSpanFirst, + sourceSpanLast, + solutionPlanStopsUnit, + ) + newSourceSpanFirstPosition := newPositions[sourceSpanFirst] + newSourceSpanLastPosition := newPositions[sourceSpanLast] + + if isViolatedPositions( + newSourceSpanFirstPosition, + newSourceSpanLastPosition, + newPlanUnitSpanFirstPosition, + newPlanUnitSpanLastPosition, + ) { + return true, noPositionsHint() + } + } + } + } + } + } + + // check if plan unit is a source + if sourceDisallowedInterleaves, isSourcePlanUnit := + l.sourceDisallowedInterleaves[move.PlanStopsUnit().ModelPlanUnit()]; isSourcePlanUnit { + for _, disallowedInterleave := range sourceDisallowedInterleaves { + targetSolutionPlanUnit := solution.SolutionPlanUnit(disallowedInterleave.Target()) + if targetSolutionPlanUnit.IsPlanned() { + var targetSpanFirst, targetSpanLast SolutionStop + for _, plannedSolutionStops := range targetSolutionPlanUnit.PlannedPlanStopsUnits() { + if plannedSolutionStops.SolutionStops()[0].Vehicle() != move.Vehicle() { + continue + } + targetSpanFirst, targetSpanLast = determineFirstLastSolutionStops( + targetSpanFirst, + targetSpanLast, + plannedSolutionStops, + ) + newTargetSpanFirstPosition := newPositions[targetSpanFirst] + newTargetSpanLastPosition := newPositions[targetSpanLast] + + if isViolatedPositions( + newPlanUnitSpanFirstPosition, + newPlanUnitSpanLastPosition, + newTargetSpanFirstPosition, + newTargetSpanLastPosition, + ) { + return true, noPositionsHint() + } + } + } + } + } + return false, noPositionsHint() +} + +// determineFirstLastSolutionStops determines the first and last solution stops +// of the given solution plan stops unit. +func determineFirstLastSolutionStops( + first, last SolutionStop, + solutionPlanStopUnit SolutionPlanStopsUnit, +) (SolutionStop, SolutionStop) { + if !solutionPlanStopUnit.IsPlanned() { + return nil, nil + } + solutionStops := solutionPlanStopUnit.SolutionStops() + f := solutionStops[0] + if f != nil && (first == nil || f.Position() < first.Position()) { + first = f + } + l := solutionStops[len(solutionStops)-1] + if l != nil && (last == nil || l.Position() > last.Position()) { + last = l + } + return first, last +} diff --git a/model_constraint_interleave_test.go b/model_constraint_interleave_test.go new file mode 100644 index 0000000..f7fe05b --- /dev/null +++ b/model_constraint_interleave_test.go @@ -0,0 +1,1431 @@ +// © 2019-present nextmv.io inc + +package nextroute_test + +import ( + "context" + "testing" + + "github.com/nextmv-io/nextroute" + "github.com/nextmv-io/nextroute/common" +) + +func TestNewInterleaveConstraint(t *testing.T) { + model, err := createModel( + input( + vehicleTypes("truck"), + []Vehicle{ + vehicles( + "truck", + depot(), + 1, + )[0], + }, + nil, + planPairSequences(), + ), + ) + if err != nil { + t.Error(err) + } + + interleaveConstraint, err := nextroute.NewInterleaveConstraint() + if err != nil { + t.Error(err) + } + + if interleaveConstraint == nil { + t.Error("interleave constraint should not be nil") + } + + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + []nextroute.ModelPlanUnit{ + model.PlanUnits()[1], + }, + ) + if err != nil { + t.Error(err) + } + + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + []nextroute.ModelPlanUnit{}, + ) + if err != nil { + t.Error(err) + } + err = interleaveConstraint.DisallowInterleaving( + nil, + []nextroute.ModelPlanUnit{ + model.PlanUnits()[1], + }, + ) + if err == nil { + t.Error("expected error, target cannot be nil") + } + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + nil, + ) + if err == nil { + t.Error("expected error, sources cannot be nil") + } + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + []nextroute.ModelPlanUnit{ + model.PlanUnits()[0], + nil, + }, + ) + if err == nil { + t.Error("expected error, target cannot be nil") + } + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + []nextroute.ModelPlanUnit{ + model.PlanUnits()[0], + }, + ) + if err == nil { + t.Error("expected error, target is also in a target") + } + + err = interleaveConstraint.DisallowInterleaving( + model.PlanUnits()[0], + []nextroute.ModelPlanUnit{ + model.PlanUnits()[1], + model.PlanUnits()[1], + }, + ) + if err == nil { + t.Error("expected error, sources has duplicates") + } + + err = model.AddConstraint(interleaveConstraint) + if err != nil { + t.Error(err) + } + + solution, err := nextroute.NewSolution(model) + if err != nil { + t.Error(err) + } + + if solution == nil { + t.Error("solution should not be nil") + } +} + +func TestInterleaveConstraint0(t *testing.T) { + model, planUnits, modelStops := createModel1(t, false) + xPlanUnit := planUnits[0] + aPlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + + yPlanUnit := planUnits[1] + cPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + dPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + + a := modelStops[0] + c := modelStops[2] + d := modelStops[3] + + solution, err := nextroute.NewSolution(model) + if err != nil { + t.Error(err) + } + + solutionVehicle := solution.Vehicles()[0] + + aSolutionStop := solution.SolutionStop(a) + cSolutionStop := solution.SolutionStop(c) + dSolutionStop := solution.SolutionStop(d) + + aSolutionPlanUnit := solution.SolutionPlanUnit(aPlanUnit).(nextroute.SolutionPlanStopsUnit) + cSolutionPlanUnit := solution.SolutionPlanUnit(cPlanUnit).(nextroute.SolutionPlanStopsUnit) + dSolutionPlanUnit := solution.SolutionPlanUnit(dPlanUnit).(nextroute.SolutionPlanStopsUnit) + + // F - c - L + stopPositionc, err := nextroute.NewStopPosition( + solutionVehicle.First(), + cSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err := nextroute.NewMoveStops( + cSolutionPlanUnit, + nextroute.StopPositions{stopPositionc}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err := move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - c - d - L + stopPositiond, err := nextroute.NewStopPosition( + cSolutionStop, + dSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + dSolutionPlanUnit, + nextroute.StopPositions{stopPositiond}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - c - a - d - L + stopPositiona, err := nextroute.NewStopPosition( + cSolutionStop, + aSolutionStop, + dSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + aSolutionPlanUnit, + nextroute.StopPositions{stopPositiona}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } +} + +func TestInterleaveConstraint1(t *testing.T) { + model, planUnits, modelStops := createModel1(t, true) + + xPlanUnit := planUnits[0] + aPlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + bPlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + + yPlanUnit := planUnits[1] + cPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + dPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + + zPlanUnit := planUnits[2] + ePlanUnit := zPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + fPlanUnit := zPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + + a := modelStops[0] + b := modelStops[1] + c := modelStops[2] + d := modelStops[3] + e := modelStops[4] + f := modelStops[5] + + solution, err := nextroute.NewSolution(model) + if err != nil { + t.Error(err) + } + + solutionVehicle := solution.Vehicles()[0] + + aSolutionStop := solution.SolutionStop(a) + bSolutionStop := solution.SolutionStop(b) + cSolutionStop := solution.SolutionStop(c) + dSolutionStop := solution.SolutionStop(d) + eSolutionStop := solution.SolutionStop(e) + fSolutionStop := solution.SolutionStop(f) + + aSolutionPlanUnit := solution.SolutionPlanUnit(aPlanUnit).(nextroute.SolutionPlanStopsUnit) + bSolutionPlanUnit := solution.SolutionPlanUnit(bPlanUnit).(nextroute.SolutionPlanStopsUnit) + + cSolutionPlanUnit := solution.SolutionPlanUnit(cPlanUnit).(nextroute.SolutionPlanStopsUnit) + dSolutionPlanUnit := solution.SolutionPlanUnit(dPlanUnit).(nextroute.SolutionPlanStopsUnit) + + eSolutionPlanUnit := solution.SolutionPlanUnit(ePlanUnit).(nextroute.SolutionPlanStopsUnit) + fSolutionPlanUnit := solution.SolutionPlanUnit(fPlanUnit).(nextroute.SolutionPlanStopsUnit) + + // F - a - L + stopPositiona, err := nextroute.NewStopPosition( + solutionVehicle.First(), + aSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err := nextroute.NewMoveStops( + aSolutionPlanUnit, + nextroute.StopPositions{stopPositiona}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err := move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - a - b - L + stopPositionb, err := nextroute.NewStopPosition( + aSolutionStop, + bSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + bSolutionPlanUnit, + nextroute.StopPositions{stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - a - b - c - L + stopPositionc, err := nextroute.NewStopPosition( + bSolutionStop, + cSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + cSolutionPlanUnit, + nextroute.StopPositions{stopPositionc}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - a - b - c - d - L + stopPositiond, err := nextroute.NewStopPosition( + cSolutionStop, + dSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + dSolutionPlanUnit, + nextroute.StopPositions{stopPositiond}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // F - e - a - b - c - d - L allowed + stopPositione, err := nextroute.NewStopPosition( + solutionVehicle.First(), + eSolutionStop, + aSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit, + nextroute.StopPositions{stopPositione}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + // F - a - e - b - c - d - L not allowed + stopPositione, err = nextroute.NewStopPosition( + aSolutionStop, + eSolutionStop, + bSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit, + nextroute.StopPositions{stopPositione}, + ) + if err != nil { + t.Fatal(err) + } + + if move.IsExecutable() { + t.Fatal("expected move not to be executable") + } + + // F - a - b - e - c - d - L allowed + stopPositione, err = nextroute.NewStopPosition( + bSolutionStop, + eSolutionStop, + cSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit, + nextroute.StopPositions{stopPositione}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + // F - a - b - c - e - d - L not allowed + stopPositione, err = nextroute.NewStopPosition( + cSolutionStop, + eSolutionStop, + dSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit, + nextroute.StopPositions{stopPositione}, + ) + if err != nil { + t.Fatal(err) + } + + if move.IsExecutable() { + t.Fatal("expected move to be not executable") + } + + // F - a - b - c - d - e - L allowed + stopPositione, err = nextroute.NewStopPosition( + dSolutionStop, + eSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit, + nextroute.StopPositions{stopPositione}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + _ = fSolutionStop + _ = fSolutionPlanUnit +} + +func TestInterleaveConstraint2(t *testing.T) { + model, planUnits, modelStops := createModel2(t) + xPlanUnit := planUnits[0] + yPlanUnit := planUnits[1] + iPlanUnit := planUnits[2] + + a := modelStops[0] + b := modelStops[1] + c := modelStops[2] + d := modelStops[3] + e := modelStops[4] + f := modelStops[5] + g := modelStops[6] + h := modelStops[7] + i := modelStops[8] + + solution, err := nextroute.NewSolution(model) + if err != nil { + t.Fatal(err) + } + + solutionVehicle := solution.Vehicles()[0] + + iSolutionStop := solution.SolutionStop(i) + iSolutionPlanUnit := solution.SolutionPlanUnit(iPlanUnit) + + stopPosition, err := nextroute.NewStopPosition( + solutionVehicle.First(), + iSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err := nextroute.NewMoveStops( + iSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPosition}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err := move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // we now have: F - i - L + + eSolutionStop := solution.SolutionStop(e) + + ePlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[2] + if len(ePlanUnit.(nextroute.ModelPlanStopsUnit).Stops()) != 1 { + t.Fatal("expected plan unit to have 1 stop (e)") + } + eSolutionPlanUnit := solution.SolutionPlanUnit(ePlanUnit) + + stopPosition, err = nextroute.NewStopPosition( + solutionVehicle.First(), + eSolutionStop, + solutionVehicle.First().Next(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + eSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPosition}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // we now have: F - e - i - L + aSolutionStop := solution.SolutionStop(a) + bSolutionStop := solution.SolutionStop(b) + + abPlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + if len(abPlanUnit.(nextroute.ModelPlanStopsUnit).Stops()) != 2 { + t.Fatal("expected plan unit to have 2 stops (a, b)") + } + abSolutionPlanUnit := solution.SolutionPlanUnit(abPlanUnit) + + // Check F - a - b - e - i - L is accepted + stopPositiona, err := nextroute.NewStopPosition( + solutionVehicle.First(), + aSolutionStop, + bSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositionb, err := nextroute.NewStopPosition( + aSolutionStop, + bSolutionStop, + solutionVehicle.First().Next(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + abSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositiona, stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + unplanned, err := abSolutionPlanUnit.UnPlan() + if err != nil { + t.Fatal(err) + } + if !unplanned { + t.Fatal("expected plan unit to be unplanned") + } + // Check F - a - e - b - i - L is accepted + stopPositiona, err = nextroute.NewStopPosition( + solutionVehicle.First(), + aSolutionStop, + eSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositionb, err = nextroute.NewStopPosition( + eSolutionStop, + bSolutionStop, + iSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + abSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositiona, stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + unplanned, err = abSolutionPlanUnit.UnPlan() + if err != nil { + t.Fatal(err) + } + if !unplanned { + t.Fatal("expected plan unit to be unplanned") + } + // Check F - a - e - i - b - L is rejected + stopPositiona, err = nextroute.NewStopPosition( + solutionVehicle.First(), + aSolutionStop, + eSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositionb, err = nextroute.NewStopPosition( + iSolutionStop, + bSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + abSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositiona, stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal( + "expected move to be not executable, i can not be interleaved with a-b" + + ", a before i and b after i is not allowed", + ) + } + // Check F - e - i - a - b - L is rejected + stopPositiona, err = nextroute.NewStopPosition( + solutionVehicle.Last().Previous(), + aSolutionStop, + bSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositionb, err = nextroute.NewStopPosition( + aSolutionStop, + bSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + abSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositiona, stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal( + "expected move to be not executable, i can not be interleaved with a-b" + + ", a-b not allowed after i as e is already before i", + ) + } + // Check F - e - a - b - i - L is accepted + stopPositiona, err = nextroute.NewStopPosition( + eSolutionStop, + aSolutionStop, + bSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositionb, err = nextroute.NewStopPosition( + aSolutionStop, + bSolutionStop, + iSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + abSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositiona, stopPositionb}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + // We now have F - e - a - b - i - L + cSolutionStop := solution.SolutionStop(c) + dSolutionStop := solution.SolutionStop(d) + + cdPlanUnit := xPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + if len(cdPlanUnit.(nextroute.ModelPlanStopsUnit).Stops()) != 2 { + t.Fatal("expected plan unit to have 2 stops (c, d)") + } + cdSolutionPlanUnit := solution.SolutionPlanUnit(cdPlanUnit) + + // Check F - c - e - a - b - i - d - L is rejected + stopPositionc, err := nextroute.NewStopPosition( + solutionVehicle.First(), + cSolutionStop, + eSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiond, err := nextroute.NewStopPosition( + iSolutionStop, + dSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + cdSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionc, stopPositiond}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal("expected move not to be executable, d not allowed after i as b is already before i") + } + // Check F - e - a - b - i - c - d - L is rejected + stopPositionc, err = nextroute.NewStopPosition( + iSolutionStop, + cSolutionStop, + dSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiond, err = nextroute.NewStopPosition( + cSolutionStop, + dSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + cdSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionc, stopPositiond}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal("expected move not to be executable, c - d not allowed after i as b is already before i") + } + // Check F - e - a - b - c - d - i - L is accepted + stopPositionc, err = nextroute.NewStopPosition( + bSolutionStop, + cSolutionStop, + dSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiond, err = nextroute.NewStopPosition( + cSolutionStop, + dSolutionStop, + iSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + cdSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionc, stopPositiond}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable, c - d is before i") + } + planned, err = move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("expected move to be planned") + } + + // We now have F - e - a - b - c - d - i - L + + fSolutionStop := solution.SolutionStop(f) + gSolutionStop := solution.SolutionStop(g) + + fgPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + if len(fgPlanUnit.(nextroute.ModelPlanStopsUnit).Stops()) != 2 { + t.Fatal("expected plan unit to have 2 stops (f, g)") + } + fgSolutionPlanUnit := solution.SolutionPlanUnit(fgPlanUnit) + + // Check F - f - e - a - b - c - d - i - g - L is rejected + stopPositionf, err := nextroute.NewStopPosition( + solutionVehicle.First(), + fSolutionStop, + eSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiong, err := nextroute.NewStopPosition( + iSolutionStop, + gSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + fgSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionf, stopPositiong}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal("expected move not to be executable, x not allowed to interleave f - g") + } + // Check F - e - a - b - c - d - f - i - g - L is accepted + stopPositionf, err = nextroute.NewStopPosition( + dSolutionStop, + fSolutionStop, + iSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiong, err = nextroute.NewStopPosition( + iSolutionStop, + gSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + fgSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionf, stopPositiong}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable, i can interleave f - g") + } + // Check F - f - g - e - a - b - c - d - i - L is accepted + stopPositionf, err = nextroute.NewStopPosition( + solutionVehicle.First(), + fSolutionStop, + gSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + stopPositiong, err = nextroute.NewStopPosition( + fSolutionStop, + gSolutionStop, + solutionVehicle.First().Next(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + fgSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionf, stopPositiong}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable, i can interleave f - g") + } + + hSolutionStop := solution.SolutionStop(h) + hPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + if len(hPlanUnit.(nextroute.ModelPlanStopsUnit).Stops()) != 1 { + t.Fatal("expected plan unit to have 1 stop (h)") + } + hSolutionPlanUnit := solution.SolutionPlanUnit(hPlanUnit) + + // Check F - h - e - a - b - c - d - i - L is accepted + stopPositionh, err := nextroute.NewStopPosition( + solutionVehicle.First(), + hSolutionStop, + eSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + hSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionh}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable, h is allowed first") + } + // Check F - e - h - a - b - c - d - i - L is rejected + stopPositionh, err = nextroute.NewStopPosition( + eSolutionStop, + hSolutionStop, + aSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + hSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionh}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal("expected move not to be executable, h cannot be interleaved with e and a-b") + } + // Check F - e - a - h - b - c - d - i - L is rejected + stopPositionh, err = nextroute.NewStopPosition( + aSolutionStop, + hSolutionStop, + bSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + hSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionh}, + ) + if err != nil { + t.Fatal(err) + } + if move.IsExecutable() { + t.Fatal("expected move not to be executable, h cannot be interleaved with a-b") + } + + // Check F - e - a - b - c - d - h - i - L is accepted + stopPositionh, err = nextroute.NewStopPosition( + dSolutionStop, + hSolutionStop, + iSolutionStop, + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + hSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionh}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } + // Check F - e - a - b - c - d - i - h - L is accepted + stopPositionh, err = nextroute.NewStopPosition( + iSolutionStop, + hSolutionStop, + solutionVehicle.Last(), + ) + if err != nil { + t.Fatal(err) + } + move, err = nextroute.NewMoveStops( + hSolutionPlanUnit.(nextroute.SolutionPlanStopsUnit), + nextroute.StopPositions{stopPositionh}, + ) + if err != nil { + t.Fatal(err) + } + if !move.IsExecutable() { + t.Fatal("expected move to be executable") + } +} + +// createModel2 creates a model with 10 stops, 4 plan units, 2 vehicles. +// Creates 10 stops A, B, C, D, E, F, G, H, I, J +// Creates 4 plan units: +// 1. Plan units unit x consisting out of 3 plan stops units: +// - Plan stops unit x1 consisting out of stops A, B, A has to go before B +// - Plan stops unit x2 consisting out of stops C, D, C has to go before D +// - Plan stops unit x3 consisting out of stop E +// +// 2. Plan units unit y consisting out of 2 plan stops units: +// - Plan stops unit y1 consisting out of stops F, G, F has to go before G +// - Plan stops unit y2 consisting out of stop H +// +// 3. Plan stops unit i consisting out of stop I +// 4. Plan stops unit j consisting out of stop J +// +// Creates an interleave constraint that: +// - Disallows interleaving of plan unit y and i with plan unit x +// - Disallows interleaving of plan unit x and j with plan unit y +// Adds the interleave constraint to the model +// Returns the model, plan units [x,y, i, j], and stops [A, B, C, D, E, F, G, H, I, J]. +func createModel2(t *testing.T) ( + nextroute.Model, + nextroute.ModelPlanUnits, + nextroute.ModelStops, +) { + model, err := nextroute.NewModel() + if err != nil { + t.Fatal(err) + } + center, err := common.NewLocation(0, 0) + if err != nil { + t.Fatal(err) + } + a, err := model.NewStop(center) + a.SetID("a") + if err != nil { + t.Fatal(err) + } + b, err := model.NewStop(center) + b.SetID("b") + if err != nil { + t.Fatal(err) + } + c, err := model.NewStop(center) + c.SetID("c") + if err != nil { + t.Fatal(err) + } + d, err := model.NewStop(center) + d.SetID("d") + if err != nil { + t.Fatal(err) + } + e, err := model.NewStop(center) + e.SetID("e") + if err != nil { + t.Fatal(err) + } + x1DAG := nextroute.NewDirectedAcyclicGraph() + err = x1DAG.AddArc(a, b) + if err != nil { + t.Fatal(err) + } + x1, err := model.NewPlanMultipleStops([]nextroute.ModelStop{a, b}, x1DAG) + if err != nil { + t.Fatal(err) + } + x2DAG := nextroute.NewDirectedAcyclicGraph() + err = x2DAG.AddArc(c, d) + if err != nil { + t.Fatal(err) + } + x2, err := model.NewPlanMultipleStops([]nextroute.ModelStop{c, d}, x2DAG) + if err != nil { + t.Fatal(err) + } + x3, err := model.NewPlanSingleStop(e) + if err != nil { + t.Fatal(err) + } + xPlanUnit, err := model.NewPlanAllPlanUnits(true, x1, x2, x3) + if err != nil { + t.Fatal(err) + } + + f, err := model.NewStop(center) + f.SetID("f") + if err != nil { + t.Fatal(err) + } + g, err := model.NewStop(center) + g.SetID("g") + if err != nil { + t.Fatal(err) + } + h, err := model.NewStop(center) + h.SetID("h") + if err != nil { + t.Fatal(err) + } + y1DAG := nextroute.NewDirectedAcyclicGraph() + err = y1DAG.AddArc(f, g) + if err != nil { + t.Fatal(err) + } + y1, err := model.NewPlanMultipleStops([]nextroute.ModelStop{f, g}, y1DAG) + if err != nil { + t.Fatal(err) + } + y2, err := model.NewPlanSingleStop(h) + if err != nil { + t.Fatal(err) + } + yPlanUnit, err := model.NewPlanAllPlanUnits(true, y1, y2) + if err != nil { + t.Fatal(err) + } + + i, err := model.NewStop(center) + i.SetID("i") + if err != nil { + t.Fatal(err) + } + j, err := model.NewStop(center) + j.SetID("j") + if err != nil { + t.Fatal(err) + } + iPlanUnit, err := model.NewPlanSingleStop(i) + if err != nil { + t.Fatal(err) + } + jPlanUnit, err := model.NewPlanSingleStop(j) + if err != nil { + t.Fatal(err) + } + + interleaveConstraint, err := nextroute.NewInterleaveConstraint() + if err != nil { + t.Fatal(err) + } + err = interleaveConstraint.DisallowInterleaving(xPlanUnit, []nextroute.ModelPlanUnit{yPlanUnit, iPlanUnit}) + if err != nil { + t.Fatal(err) + } + err = interleaveConstraint.DisallowInterleaving(yPlanUnit, []nextroute.ModelPlanUnit{xPlanUnit, jPlanUnit}) + if err != nil { + t.Fatal(err) + } + + err = model.AddConstraint(interleaveConstraint) + if err != nil { + t.Fatal(err) + } + + vt, err := model.NewVehicleType( + nextroute.NewTimeIndependentDurationExpression( + nextroute.NewTravelDurationExpression( + nextroute.NewHaversineExpression(), + common.NewSpeed( + 10.0, + common.MetersPerSecond, + ), + ), + ), + nextroute.NewDurationExpression( + "travelDuration", + nextroute.NewStopDurationExpression("serviceDuration", 0.0), + common.Second, + ), + ) + if err != nil { + t.Fatal(err) + } + warehouse, err := model.NewStop(center) + if err != nil { + t.Fatal(err) + } + warehouse.SetID("warehouse") + + v, err := model.NewVehicle(vt, model.Epoch(), warehouse, warehouse) + v.SetID("v1") + if err != nil { + t.Fatal(err) + } + v, err = model.NewVehicle(vt, model.Epoch(), warehouse, warehouse) + v.SetID("v2") + if err != nil { + t.Fatal(err) + } + + return model, + nextroute.ModelPlanUnits{xPlanUnit, yPlanUnit, iPlanUnit, jPlanUnit}, + nextroute.ModelStops{a, b, c, d, e, f, g, h, i, j} +} + +func createModel1(t *testing.T, postAll bool) ( + nextroute.Model, + nextroute.ModelPlanUnits, + nextroute.ModelStops, +) { + model, err := nextroute.NewModel() + if err != nil { + t.Fatal(err) + } + center, err := common.NewLocation(0, 0) + if err != nil { + t.Fatal(err) + } + a, err := model.NewStop(center) + a.SetID("a") + if err != nil { + t.Fatal(err) + } + b, err := model.NewStop(center) + b.SetID("b") + if err != nil { + t.Fatal(err) + } + c, err := model.NewStop(center) + c.SetID("c") + if err != nil { + t.Fatal(err) + } + d, err := model.NewStop(center) + d.SetID("d") + if err != nil { + t.Fatal(err) + } + e, err := model.NewStop(center) + e.SetID("e") + if err != nil { + t.Fatal(err) + } + f, err := model.NewStop(center) + f.SetID("f") + if err != nil { + t.Fatal(err) + } + + aPlanUnit, err := model.NewPlanSingleStop(a) + if err != nil { + t.Fatal(err) + } + bPlanUnit, err := model.NewPlanSingleStop(b) + if err != nil { + t.Fatal(err) + } + xPlanUnit, err := model.NewPlanAllPlanUnits(true, aPlanUnit, bPlanUnit) + if err != nil { + t.Fatal(err) + } + cPlanUnit, err := model.NewPlanSingleStop(c) + if err != nil { + t.Fatal(err) + } + dPlanUnit, err := model.NewPlanSingleStop(d) + if err != nil { + t.Fatal(err) + } + yPlanUnit, err := model.NewPlanAllPlanUnits(true, cPlanUnit, dPlanUnit) + if err != nil { + t.Fatal(err) + } + ePlanUnit, err := model.NewPlanSingleStop(e) + if err != nil { + t.Fatal(err) + } + fPlanUnit, err := model.NewPlanSingleStop(f) + if err != nil { + t.Fatal(err) + } + zPlanUnit, err := model.NewPlanAllPlanUnits(true, ePlanUnit, fPlanUnit) + if err != nil { + t.Fatal(err) + } + interleaveConstraint, err := nextroute.NewInterleaveConstraint() + if err != nil { + t.Fatal(err) + } + err = interleaveConstraint.DisallowInterleaving(xPlanUnit, []nextroute.ModelPlanUnit{yPlanUnit, zPlanUnit}) + if err != nil { + t.Fatal(err) + } + + if postAll { + err = interleaveConstraint.DisallowInterleaving(yPlanUnit, []nextroute.ModelPlanUnit{xPlanUnit, zPlanUnit}) + if err != nil { + t.Fatal(err) + } + err = interleaveConstraint.DisallowInterleaving(zPlanUnit, []nextroute.ModelPlanUnit{xPlanUnit, yPlanUnit}) + if err != nil { + t.Fatal(err) + } + } + err = model.AddConstraint(interleaveConstraint) + if err != nil { + t.Fatal(err) + } + + vt, err := model.NewVehicleType( + nextroute.NewTimeIndependentDurationExpression( + nextroute.NewTravelDurationExpression( + nextroute.NewHaversineExpression(), + common.NewSpeed( + 10.0, + common.MetersPerSecond, + ), + ), + ), + nextroute.NewDurationExpression( + "travelDuration", + nextroute.NewStopDurationExpression("serviceDuration", 0.0), + common.Second, + ), + ) + if err != nil { + t.Fatal(err) + } + warehouse, err := model.NewStop(center) + if err != nil { + t.Fatal(err) + } + warehouse.SetID("warehouse") + + v, err := model.NewVehicle(vt, model.Epoch(), warehouse, warehouse) + v.SetID("v1") + if err != nil { + t.Fatal(err) + } + + return model, + nextroute.ModelPlanUnits{xPlanUnit, yPlanUnit, zPlanUnit}, + nextroute.ModelStops{a, b, c, d, e, f} +} diff --git a/model_constraint_successor_test.go b/model_constraint_successors_test.go similarity index 99% rename from model_constraint_successor_test.go rename to model_constraint_successors_test.go index 89fc654..9afd1db 100644 --- a/model_constraint_successor_test.go +++ b/model_constraint_successors_test.go @@ -9,7 +9,7 @@ import ( "github.com/nextmv-io/nextroute" ) -func TestNewSuccessorConstraintConstraint_EstimateIsViolated(t *testing.T) { +func TestNewSuccessorConstraint_EstimateIsViolated(t *testing.T) { model, err := createModel( input( vehicleTypes("truck"), diff --git a/solution.go b/solution.go index 145846c..9904b3f 100644 --- a/solution.go +++ b/solution.go @@ -125,8 +125,7 @@ func NewSolution( } solution := &solutionImpl{ - model: m, - + model: m, vehicleIndices: make([]int, 0, len(model.vehicles)), vehicles: make([]solutionVehicleImpl, 0, len(model.vehicles)), solutionVehicles: make([]SolutionVehicle, 0, len(model.vehicles)), @@ -1055,9 +1054,9 @@ func (s *solutionImpl) UnPlannedPlanUnits() ImmutableSolutionPlanUnitCollection // PreAllocatedMoveContainer is used to reduce allocations. // It contains objects that can be used instead of allocating new ones. type PreAllocatedMoveContainer struct { - // singleStopPosSolutionMoveStop has the underlying type *solutionMoveStopsImpl. + // solutionMoveStops has the underlying type *solutionMoveStopsImpl. // and has a length 1 stopPositions slice. - singleStopPosSolutionMoveStop SolutionMoveStops + solutionMoveStops SolutionMoveStops } // NewPreAllocatedMoveContainer creates a new PreAllocatedMoveContainer. @@ -1068,7 +1067,7 @@ func NewPreAllocatedMoveContainer(planUnit SolutionPlanUnit) *PreAllocatedMoveCo case SolutionPlanStopsUnit: m := newNotExecutableSolutionMoveStops(planUnit.(*solutionPlanStopsUnitImpl)) m.stopPositions = make([]stopPositionImpl, 1, 2) - allocations.singleStopPosSolutionMoveStop = m + allocations.solutionMoveStops = m case SolutionPlanUnitsUnit: } return &allocations diff --git a/solution_move_stops_generator.go b/solution_move_stops_generator.go index eff108e..1392810 100644 --- a/solution_move_stops_generator.go +++ b/solution_move_stops_generator.go @@ -3,6 +3,9 @@ package nextroute import ( + "fmt" + "math" + "github.com/nextmv-io/nextroute/common" ) @@ -101,38 +104,55 @@ func SolutionMoveStopsGenerator( preAllocatedMoveContainer *PreAllocatedMoveContainer, shouldStop func() bool, ) { - source := common.Map(stops, func(stop SolutionStop) solutionStopImpl { - return stop.(solutionStopImpl) - }) - target := common.Map(vehicle.SolutionStops(), func(stop SolutionStop) solutionStopImpl { - return stop.(solutionStopImpl) - }) - m := preAllocatedMoveContainer.singleStopPosSolutionMoveStop - m.(*solutionMoveStopsImpl).reset() + solution := vehicle.solution + nrStops := len(stops) + + m := preAllocatedMoveContainer.solutionMoveStops m.(*solutionMoveStopsImpl).planUnit = planUnit - m.(*solutionMoveStopsImpl).allowed = false - if len(source) == 0 { + m.(*solutionMoveStopsImpl).reset() + + if nrStops == 0 { yield(m) return } - // TODO: we can reuse the stopPositions slice from m - positions := make([]stopPositionImpl, len(source)) - for idx := range source { - positions[idx].stopIndex = source[idx].index - positions[idx].solution = source[idx].solution + if cap(m.(*solutionMoveStopsImpl).stopPositions) < nrStops { + m.(*solutionMoveStopsImpl).stopPositions = make([]stopPositionImpl, nrStops) } + m.(*solutionMoveStopsImpl).stopPositions = m.(*solutionMoveStopsImpl).stopPositions[:nrStops] - locations := make([]int, 0, len(source)) + for idx, stop := range stops { + m.(*solutionMoveStopsImpl).stopPositions[idx].stopIndex = stop.(solutionStopImpl).index + m.(*solutionMoveStopsImpl).stopPositions[idx].solution = solution + } - generate(positions, locations, source, target, func() { - m.(*solutionMoveStopsImpl).reset() - m.(*solutionMoveStopsImpl).planUnit = planUnit - m.(*solutionMoveStopsImpl).stopPositions = positions - m.(*solutionMoveStopsImpl).allowed = false - m.(*solutionMoveStopsImpl).valueSeen = 1 - yield(m) - }, shouldStop) + target := common.Map(vehicle.SolutionStops(), func(stop SolutionStop) solutionStopImpl { + return stop.(solutionStopImpl) + }) + + combination := make([]int, 0, nrStops) + + generate( + vehicle, + planUnit, + m.(*solutionMoveStopsImpl).stopPositions, combination, target, func() { + m.(*solutionMoveStopsImpl).allowed = false + m.(*solutionMoveStopsImpl).valueSeen = 1 + + ids := common.Map(m.StopPositions(), func(stopPosition StopPosition) string { + return stopPosition.Stop().ModelStop().ID() + }, + ) + + fmt.Println("yielding move", m.Vehicle().ModelVehicle().ID(), + m.Previous().ModelStop().ID(), + ids, + m.Next().ModelStop().ID(), + ) + yield(m) + }, + shouldStop, + ) } func isNotAllowed(from, to solutionStopImpl) bool { @@ -154,10 +174,18 @@ func mustBeNeighbours(from, to solutionStopImpl) bool { HasDirectArc(from.ModelStop(), to.ModelStop()) } +// - combination[i] will define before which stop in target the stop at +// stopPositions[i].stop should be inserted in target. +// - if len(combination) == len(stopPositions) then we have a complete +// definition of where all the stop positions should be inserted. +// - combination[i] >= combination[i-1] should be true for all i > 0, this is +// because we want to preserve the order of the stops associated with the +// stop positions. func generate( + vehicle SolutionVehicle, + solutionPlanStopsUnit SolutionPlanStopsUnit, stopPositions []stopPositionImpl, combination []int, - source []solutionStopImpl, target []solutionStopImpl, yield func(), shouldStop func() bool, @@ -166,53 +194,183 @@ func generate( return } - if len(combination) == len(source) { + if len(combination) == len(stopPositions) { yield() return } start := 0 - if len(combination) > 0 { + + if len(combination) != 0 { start = combination[len(combination)-1] - 1 } - for i := start; i < len(target)-1; i++ { + // can we be more precise on where we can end? + end := len(target) - 1 + + excludePositions := map[int]struct{}{} + + interleaveConstraint := vehicle.ModelVehicle().Model().InterleaveConstraint() + + if interleaveConstraint != nil { + solution := vehicle.SolutionStops()[0].Solution() + + modelPlanUnit := solutionPlanStopsUnit.ModelPlanUnit() + + // what is the first planned target position for this plan unit if + // it is a composition of plan units (plan units unit)? + firstPlannedTargetPosition := math.MaxInt64 + + if planUnitsUnit, hasPlanUnitsUnit := modelPlanUnit.PlanUnitsUnit(); hasPlanUnitsUnit { + data := vehicle.Last().ConstraintData(interleaveConstraint).(*interleaveConstraintData) + if solutionStopSpan, ok := data.solutionPlanStopUnits[planUnitsUnit.Index()]; ok { + firstPlannedTargetPosition = solutionStopSpan.first + } + } + + sourceDisallowedInterleaves := interleaveConstraint.SourceDisallowedInterleaves(modelPlanUnit) + + for _, sourceDisallowedInterleave := range sourceDisallowedInterleaves { + targetSolutionPlanUnit := solution.SolutionPlanUnit(sourceDisallowedInterleave.Target()) + if targetSolutionPlanUnit.IsPlanned() { + var first SolutionStop + var last SolutionStop + for _, plannedPlanStopsUnit := range targetSolutionPlanUnit.PlannedPlanStopsUnits() { + if plannedPlanStopsUnit.SolutionStops()[0].Vehicle() == vehicle { + // the source is not allowed to be interleaved into these positions + for _, solutionStop := range plannedPlanStopsUnit.SolutionStops() { + if first == nil || solutionStop.Position() < first.Position() { + first = solutionStop + } + if last == nil || solutionStop.Position() > last.Position() { + last = solutionStop + } + } + } + } + if first != nil { + for s := first.Next(); s != last; s = s.Next() { + excludePositions[s.Position()-1] = struct{}{} + } + excludePositions[last.Position()-1] = struct{}{} + } + } + } + + targetDisallowedInterleaves := interleaveConstraint.TargetDisallowedInterleaves(modelPlanUnit) + + TargetDisallowedInterleavesLoop: + for _, targetDisallowedInterleave := range targetDisallowedInterleaves { + for _, source := range targetDisallowedInterleave.Sources() { + sourceSolutionPlanUnit := solution.SolutionPlanUnit(source) + if sourceSolutionPlanUnit.IsPlanned() { + // Each source can not be interleaved with the to be planned + // plan unit (target). + for _, plannedPlanStopsUnit := range sourceSolutionPlanUnit.PlannedPlanStopsUnits() { + if plannedPlanStopsUnit.SolutionStops()[0].Vehicle() == vehicle { + firstStopOfPlanUnit := plannedPlanStopsUnit.SolutionStops()[0] + + // If we already decided to be before or after this planned source all position must be + // before or after. We force before by setting end, we do not have to force after as the + // sequence of stops already does that. + if len(combination) > 0 && + combination[len(combination)-1] <= firstStopOfPlanUnit.Position() { + end = firstStopOfPlanUnit.Position() + 1 + break TargetDisallowedInterleavesLoop + } + + // If we already planned part of the plan units unit target we must plan this target also + // before or after the planned source. + if firstPlannedTargetPosition != math.MaxInt64 { + lastStopOfPlanUnit := + plannedPlanStopsUnit.SolutionStops()[len(plannedPlanStopsUnit.SolutionStops())-1] + + if firstPlannedTargetPosition > lastStopOfPlanUnit.Position() { + start = lastStopOfPlanUnit.Position() + 1 + } + + if firstPlannedTargetPosition < firstStopOfPlanUnit.Position() { + end = firstStopOfPlanUnit.Position() + 1 + } + } + } + } + } + } + } + } + + positions := make([]int, 0, end-start) + for i := start; i < end; i++ { + if _, excludePosition := excludePositions[i]; !excludePosition { + positions = append(positions, i) + } + } + + for _, i := range positions { + // if the stops on the existing vehicle must be neighbours, we can only + // explore combinations where the stops are neighbours. In other words + // we can not add something between these two stops. if i > 0 && mustBeNeighbours(target[i], target[i+1]) { continue } - combination = append(combination, i+1) - positionIdx := len(combination) - 1 + stopPositionIdx := len(combination) - stopPositions[positionIdx].previousStopIndex = target[combination[positionIdx]-1].index - stopPositions[positionIdx].nextStopIndex = target[combination[positionIdx]].index + combination = append(combination, i+1) - if positionIdx > 0 { - if combination[positionIdx] == combination[positionIdx-1] { - stopPositions[positionIdx].previousStopIndex = stopPositions[positionIdx-1].stopIndex - stopPositions[positionIdx-1].nextStopIndex = stopPositions[positionIdx].stopIndex + stopPositions[stopPositionIdx].previousStopIndex = target[combination[stopPositionIdx]-1].index + stopPositions[stopPositionIdx].nextStopIndex = target[combination[stopPositionIdx]].index + + if stopPositionIdx > 0 { + previousStopPositionIdx := stopPositionIdx - 1 + if combination[stopPositionIdx] == combination[stopPositionIdx-1] { + stopPositions[stopPositionIdx].previousStopIndex = stopPositions[previousStopPositionIdx].stopIndex + stopPositions[previousStopPositionIdx].nextStopIndex = stopPositions[stopPositionIdx].stopIndex } else { - stopPositions[positionIdx-1].nextStopIndex = target[combination[positionIdx-1]].index - if mustBeNeighbours(stopPositions[positionIdx-1].stop(), stopPositions[positionIdx].stop()) { + stopPositions[previousStopPositionIdx].nextStopIndex = + target[combination[previousStopPositionIdx]].index + if mustBeNeighbours( + stopPositions[previousStopPositionIdx].stop(), + stopPositions[stopPositionIdx].stop(), + ) { + // if the previous stop and this stop must be neighbours, we can break immediately and only explore + // them as neighbours (no need to try to position positionIdx at any other position). break } } - if isNotAllowed(stopPositions[positionIdx-1].stop(), stopPositions[positionIdx-1].next()) { - combination = combination[:positionIdx] - if stopPositions[positionIdx-1].nextStopIndex != stopPositions[positionIdx].previousStopIndex { + if isNotAllowed( + stopPositions[previousStopPositionIdx].stop(), + stopPositions[previousStopPositionIdx].next(), + ) { + // undo the last combination (undo where we position stop at stopPositionIdx) + combination = combination[:stopPositionIdx] + // if selecting a new position for the stop at stopPositionIdx does not influence the previous stop next + // stop it does not matter where we position the stop at stopPositionIdx, therefor we trigger a back + // track immediately and go for the next position for the previous stop position. + if stopPositions[previousStopPositionIdx].nextStopIndex != + stopPositions[stopPositionIdx].previousStopIndex { break } continue } } - if isNotAllowed(stopPositions[positionIdx].previous(), stopPositions[positionIdx].stop()) { - combination = combination[:positionIdx] + if isNotAllowed(stopPositions[stopPositionIdx].previous(), stopPositions[stopPositionIdx].stop()) { + combination = combination[:stopPositionIdx] continue } - generate(stopPositions, combination, source, target, yield, shouldStop) + generate( + vehicle, + solutionPlanStopsUnit, + stopPositions, + combination, + target, + yield, + shouldStop, + ) combination = combination[:len(combination)-1] } diff --git a/solution_move_stops_generator_test.go b/solution_move_stops_generator_test.go index 6bdaf68..ecdf348 100644 --- a/solution_move_stops_generator_test.go +++ b/solution_move_stops_generator_test.go @@ -877,3 +877,232 @@ func testMoves( ) } } + +func TestSolutionMoveStopsGeneratorInterleaved( + t *testing.T, +) { + model, planUnits, _ := createModel2(t) + xPlanUnit := planUnits[0] + yPlanUnit := planUnits[1] + iPlanUnit := planUnits[2] + jPlanUnit := planUnits[3] + + solution, err := nextroute.NewSolution(model) + if err != nil { + t.Fatal(err) + } + + v := solution.Vehicles()[0] + + xSolutionPlanUnit := solution.SolutionPlanUnit(xPlanUnit) + + move := v.BestMove(context.Background(), xSolutionPlanUnit) + if move == nil { + t.Fatal("move should not be nil") + } + if !move.IsExecutable() { + t.Fatal("move should be executable") + } + planned, err := move.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("move should be planned") + } + + // i can not be interleaved anywhere in 'warehouse - a - b - c - d - e - warehouse' + // but can go first or last + iSolutionPlanUnit := solution.SolutionPlanUnit(iPlanUnit).(nextroute.SolutionPlanStopsUnit) + + moveCount := 0 + + alloc := nextroute.NewPreAllocatedMoveContainer(iSolutionPlanUnit) + nextroute.SolutionMoveStopsGeneratorTest( + v, + iSolutionPlanUnit, + func(move nextroute.SolutionMoveStops) { + moveCount++ + if moveCount > 2 { + t.Fatal("move count should not exceed 2") + } + if moveCount == 1 { + if move.StopPositions()[0].Previous() != v.First() { + t.Fatal("previous stop should be the first stop") + } + } + if moveCount == 2 { + if move.StopPositions()[0].Next() != v.Last() { + t.Fatal("next stop should be the last stop") + } + } + }, + iSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + + // j can go anywhere in 'warehouse - a - b - c - d - e - warehouse' + jSolutionPlanUnit := solution.SolutionPlanUnit(jPlanUnit).(nextroute.SolutionPlanStopsUnit) + + moveCount = 0 + + alloc = nextroute.NewPreAllocatedMoveContainer(jSolutionPlanUnit) + nextroute.SolutionMoveStopsGeneratorTest( + v, + jSolutionPlanUnit, + func(_ nextroute.SolutionMoveStops) { + moveCount++ + }, + jSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + if moveCount != 6 { + t.Fatal("move count should be 6") + } + + hPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[1] + + hSolutionPlanUnit := solution.SolutionPlanUnit(hPlanUnit).(nextroute.SolutionPlanStopsUnit) + + // h can go only at start and end of 'warehouse - a - b - c - d - e - warehouse' + // h is part of Y and Y can not be interleaved with X and a,b,c,d and e are + // part of X so h can only go at start and end + moveCount = 0 + + alloc = nextroute.NewPreAllocatedMoveContainer(hSolutionPlanUnit) + nextroute.SolutionMoveStopsGeneratorTest( + v, + hSolutionPlanUnit, + func(_ nextroute.SolutionMoveStops) { + moveCount++ + }, + hSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + if moveCount != 2 { + t.Fatal("move for h count should be 2, it is", moveCount) + } + + fgPlanUnit := yPlanUnit.(nextroute.ModelPlanUnitsUnit).PlanUnits()[0] + + fgSolutionPlanUnit := solution.SolutionPlanUnit(fgPlanUnit).(nextroute.SolutionPlanStopsUnit) + + // fg can can not interleave a-..- e in in 'warehouse - a - b - c - d - e - warehouse' + // and fg can not be interleaved by any a-..-e, so 2 moves + + moveCount = 0 + + alloc = nextroute.NewPreAllocatedMoveContainer(fgSolutionPlanUnit) + nextroute.SolutionMoveStopsGeneratorTest( + v, + fgSolutionPlanUnit, + func(_ nextroute.SolutionMoveStops) { + moveCount++ + }, + fgSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + if moveCount != 2 { + t.Fatal("move for fg count should be 2, it is", moveCount) + } + + hFirstPosition, err := nextroute.NewStopPosition(v.First(), hSolutionPlanUnit.SolutionStops()[0], v.First().Next()) + if err != nil { + t.Fatal(err) + } + + moveH, err := nextroute.NewMoveStops( + hSolutionPlanUnit, + nextroute.StopPositions{ + hFirstPosition, + }, + ) + if err != nil { + t.Fatal(err) + } + planned, err = moveH.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("move should be planned") + } + + moveCount = 0 + + nextroute.SolutionMoveStopsGeneratorTest( + v, + fgSolutionPlanUnit, + func(_ nextroute.SolutionMoveStops) { + moveCount++ + }, + fgSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + if moveCount != 3 { + t.Fatal("move for fg count should be 3, it is", moveCount) + } + + unplanned, err := hSolutionPlanUnit.UnPlan() + if err != nil { + t.Fatal(err) + } + if !unplanned { + t.Fatal("h should be unplanned") + } + + hLastPosition, err := nextroute.NewStopPosition(v.Last().Previous(), hSolutionPlanUnit.SolutionStops()[0], v.Last()) + if err != nil { + t.Fatal(err) + } + + moveH, err = nextroute.NewMoveStops( + hSolutionPlanUnit, + nextroute.StopPositions{ + hLastPosition, + }, + ) + if err != nil { + t.Fatal(err) + } + planned, err = moveH.Execute(context.Background()) + if err != nil { + t.Fatal(err) + } + if !planned { + t.Fatal("move should be planned") + } + + moveCount = 0 + + nextroute.SolutionMoveStopsGeneratorTest( + v, + fgSolutionPlanUnit, + func(_ nextroute.SolutionMoveStops) { + moveCount++ + }, + fgSolutionPlanUnit.SolutionStops(), + alloc, + func() bool { + return false + }, + ) + if moveCount != 4 { + t.Fatal("move for fg count should be 4, it is", moveCount) + } +} diff --git a/solution_vehicle.go b/solution_vehicle.go index 7b2f92d..6ea641f 100644 --- a/solution_vehicle.go +++ b/solution_vehicle.go @@ -241,7 +241,7 @@ func (v solutionVehicleImpl) bestMovePlanSingleStop( preAllocatedMoveContainer *PreAllocatedMoveContainer, ) SolutionMoveStops { candidateStop := planUnit.solutionStops[0] - move := preAllocatedMoveContainer.singleStopPosSolutionMoveStop + move := preAllocatedMoveContainer.solutionMoveStops move.(*solutionMoveStopsImpl).reset() // ensure that stopPositions is a length 1 slice move.(*solutionMoveStopsImpl).stopPositions = append( diff --git a/tests/check/input.json.golden b/tests/check/input.json.golden index 97dcd4b..dde1a04 100644 --- a/tests/check/input.json.golden +++ b/tests/check/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/custom_constraint/input.json.golden b/tests/custom_constraint/input.json.golden index 277bec4..b2b4406 100644 --- a/tests/custom_constraint/input.json.golden +++ b/tests/custom_constraint/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/custom_matrices/input.json.golden b/tests/custom_matrices/input.json.golden index 4f3abc2..7e590ec 100644 --- a/tests/custom_matrices/input.json.golden +++ b/tests/custom_matrices/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/custom_objective/input.json.golden b/tests/custom_objective/input.json.golden index 9967889..e150984 100644 --- a/tests/custom_objective/input.json.golden +++ b/tests/custom_objective/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/custom_operators/input.json.golden b/tests/custom_operators/input.json.golden index a539e88..6517c99 100644 --- a/tests/custom_operators/input.json.golden +++ b/tests/custom_operators/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/activation_penalty.json.golden b/tests/golden/testdata/activation_penalty.json.golden index c571835..bec876b 100644 --- a/tests/golden/testdata/activation_penalty.json.golden +++ b/tests/golden/testdata/activation_penalty.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/alternates.json.golden b/tests/golden/testdata/alternates.json.golden index eac78ce..a414c78 100644 --- a/tests/golden/testdata/alternates.json.golden +++ b/tests/golden/testdata/alternates.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/basic.json.golden b/tests/golden/testdata/basic.json.golden index a73489a..5696f7a 100644 --- a/tests/golden/testdata/basic.json.golden +++ b/tests/golden/testdata/basic.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/capacity.json.golden b/tests/golden/testdata/capacity.json.golden index 10d8b2c..ba2ad39 100644 --- a/tests/golden/testdata/capacity.json.golden +++ b/tests/golden/testdata/capacity.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/compatibility_attributes.json.golden b/tests/golden/testdata/compatibility_attributes.json.golden index c7cf69f..96533bf 100644 --- a/tests/golden/testdata/compatibility_attributes.json.golden +++ b/tests/golden/testdata/compatibility_attributes.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/complex_precedence.json.golden b/tests/golden/testdata/complex_precedence.json.golden index 12b81ba..ca87f1b 100644 --- a/tests/golden/testdata/complex_precedence.json.golden +++ b/tests/golden/testdata/complex_precedence.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/custom_data.json.golden b/tests/golden/testdata/custom_data.json.golden index 11a2723..2853e93 100644 --- a/tests/golden/testdata/custom_data.json.golden +++ b/tests/golden/testdata/custom_data.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/defaults.json.golden b/tests/golden/testdata/defaults.json.golden index 7329cf3..66b3368 100644 --- a/tests/golden/testdata/defaults.json.golden +++ b/tests/golden/testdata/defaults.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/direct_precedence.json.golden b/tests/golden/testdata/direct_precedence.json.golden index ab29c0d..6b908b8 100644 --- a/tests/golden/testdata/direct_precedence.json.golden +++ b/tests/golden/testdata/direct_precedence.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/direct_precedence_linked.json.golden b/tests/golden/testdata/direct_precedence_linked.json.golden index d319218..11f302e 100644 --- a/tests/golden/testdata/direct_precedence_linked.json.golden +++ b/tests/golden/testdata/direct_precedence_linked.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/distance_matrix.json.golden b/tests/golden/testdata/distance_matrix.json.golden index 52b5f63..b426bf2 100644 --- a/tests/golden/testdata/distance_matrix.json.golden +++ b/tests/golden/testdata/distance_matrix.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/duration_groups.json.golden b/tests/golden/testdata/duration_groups.json.golden index 58b73c2..cfd9e84 100644 --- a/tests/golden/testdata/duration_groups.json.golden +++ b/tests/golden/testdata/duration_groups.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/duration_groups_with_stop_multiplier.json.golden b/tests/golden/testdata/duration_groups_with_stop_multiplier.json.golden index d8422d7..4213c05 100644 --- a/tests/golden/testdata/duration_groups_with_stop_multiplier.json.golden +++ b/tests/golden/testdata/duration_groups_with_stop_multiplier.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/duration_matrix.json.golden b/tests/golden/testdata/duration_matrix.json.golden index b465359..4d31687 100644 --- a/tests/golden/testdata/duration_matrix.json.golden +++ b/tests/golden/testdata/duration_matrix.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/early_arrival_penalty.json.golden b/tests/golden/testdata/early_arrival_penalty.json.golden index 61518bc..af5826d 100644 --- a/tests/golden/testdata/early_arrival_penalty.json.golden +++ b/tests/golden/testdata/early_arrival_penalty.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops.json.golden b/tests/golden/testdata/initial_stops.json.golden index 08c9c18..0e0137c 100644 --- a/tests/golden/testdata/initial_stops.json.golden +++ b/tests/golden/testdata/initial_stops.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops_infeasible_compatibility.json.golden b/tests/golden/testdata/initial_stops_infeasible_compatibility.json.golden index ebcd026..6471897 100644 --- a/tests/golden/testdata/initial_stops_infeasible_compatibility.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_compatibility.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops_infeasible_max_duration.json.golden b/tests/golden/testdata/initial_stops_infeasible_max_duration.json.golden index 8bddde4..01bb23f 100644 --- a/tests/golden/testdata/initial_stops_infeasible_max_duration.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_max_duration.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops_infeasible_remove_all.json.golden b/tests/golden/testdata/initial_stops_infeasible_remove_all.json.golden index 1d6dfe0..a4a3c72 100644 --- a/tests/golden/testdata/initial_stops_infeasible_remove_all.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_remove_all.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops_infeasible_temporal.json.golden b/tests/golden/testdata/initial_stops_infeasible_temporal.json.golden index 5dbbc60..6317533 100644 --- a/tests/golden/testdata/initial_stops_infeasible_temporal.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_temporal.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden b/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden index ecc6b78..fcd0b12 100644 --- a/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden +++ b/tests/golden/testdata/initial_stops_infeasible_tuple.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/late_arrival_penalty.json.golden b/tests/golden/testdata/late_arrival_penalty.json.golden index 4e93c70..e230ccf 100644 --- a/tests/golden/testdata/late_arrival_penalty.json.golden +++ b/tests/golden/testdata/late_arrival_penalty.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/max_distance.json.golden b/tests/golden/testdata/max_distance.json.golden index 4bd92cb..9c85fca 100644 --- a/tests/golden/testdata/max_distance.json.golden +++ b/tests/golden/testdata/max_distance.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { @@ -75,10 +76,10 @@ "name": "1 * vehicles_duration + 1 * unplanned_penalty", "objectives": [ { - "base": 148.90959499292012, + "base": 148.9095949929201, "factor": 1, "name": "vehicles_duration", - "value": 148.90959499292012 + "value": 148.9095949929201 }, { "base": 6000000, diff --git a/tests/golden/testdata/max_duration.json.golden b/tests/golden/testdata/max_duration.json.golden index f5feb00..f71e17f 100644 --- a/tests/golden/testdata/max_duration.json.golden +++ b/tests/golden/testdata/max_duration.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/max_stops.json.golden b/tests/golden/testdata/max_stops.json.golden index a61a3b6..e39a3f2 100644 --- a/tests/golden/testdata/max_stops.json.golden +++ b/tests/golden/testdata/max_stops.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/max_wait_stop.json.golden b/tests/golden/testdata/max_wait_stop.json.golden index b3ff703..ee78e76 100644 --- a/tests/golden/testdata/max_wait_stop.json.golden +++ b/tests/golden/testdata/max_wait_stop.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/max_wait_vehicle.json.golden b/tests/golden/testdata/max_wait_vehicle.json.golden index 576d260..76615fd 100644 --- a/tests/golden/testdata/max_wait_vehicle.json.golden +++ b/tests/golden/testdata/max_wait_vehicle.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/min_stops.json.golden b/tests/golden/testdata/min_stops.json.golden index ea7b1ba..80e0405 100644 --- a/tests/golden/testdata/min_stops.json.golden +++ b/tests/golden/testdata/min_stops.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { @@ -75,10 +76,10 @@ "name": "1 * vehicles_duration + 1 * unplanned_penalty + 1 * min_stops", "objectives": [ { - "base": 909.0466359667603, + "base": 909.04663596676, "factor": 1, "name": "vehicles_duration", - "value": 909.0466359667603 + "value": 909.04663596676 }, { "factor": 1, @@ -91,7 +92,7 @@ "value": 0 } ], - "value": 909.0466359667603 + "value": 909.04663596676 }, "unplanned": [], "vehicles": [ @@ -214,7 +215,7 @@ "unplanned_stops": 0 }, "duration": 0.123, - "value": 909.0466359667603 + "value": 909.04663596676 }, "run": { "duration": 0.123, diff --git a/tests/golden/testdata/multi_window.json.golden b/tests/golden/testdata/multi_window.json.golden index 87b4b6d..b28c93c 100644 --- a/tests/golden/testdata/multi_window.json.golden +++ b/tests/golden/testdata/multi_window.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/no_mix.json.golden b/tests/golden/testdata/no_mix.json.golden index 2de194c..da10501 100644 --- a/tests/golden/testdata/no_mix.json.golden +++ b/tests/golden/testdata/no_mix.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/precedence pathologic.json.golden b/tests/golden/testdata/precedence pathologic.json.golden index 9b6a7b1..7e43092 100644 --- a/tests/golden/testdata/precedence pathologic.json.golden +++ b/tests/golden/testdata/precedence pathologic.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/precedence.json.golden b/tests/golden/testdata/precedence.json.golden index b9d1f66..bea2730 100644 --- a/tests/golden/testdata/precedence.json.golden +++ b/tests/golden/testdata/precedence.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/start_level.json.golden b/tests/golden/testdata/start_level.json.golden index 5864c38..6ee53b0 100644 --- a/tests/golden/testdata/start_level.json.golden +++ b/tests/golden/testdata/start_level.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/start_time_window.json.golden b/tests/golden/testdata/start_time_window.json.golden index 16c6643..a0267dc 100644 --- a/tests/golden/testdata/start_time_window.json.golden +++ b/tests/golden/testdata/start_time_window.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/stop_duration.json.golden b/tests/golden/testdata/stop_duration.json.golden index 3314ad3..240aeee 100644 --- a/tests/golden/testdata/stop_duration.json.golden +++ b/tests/golden/testdata/stop_duration.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/stop_duration_multiplier.json.golden b/tests/golden/testdata/stop_duration_multiplier.json.golden index b91363a..c75db77 100644 --- a/tests/golden/testdata/stop_duration_multiplier.json.golden +++ b/tests/golden/testdata/stop_duration_multiplier.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/stop_groups.json.golden b/tests/golden/testdata/stop_groups.json.golden index f1d092a..f8ffcc7 100644 --- a/tests/golden/testdata/stop_groups.json.golden +++ b/tests/golden/testdata/stop_groups.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/template_input.json.golden b/tests/golden/testdata/template_input.json.golden index 0d9dd72..7c06c4b 100644 --- a/tests/golden/testdata/template_input.json.golden +++ b/tests/golden/testdata/template_input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/unplanned_penalty.json.golden b/tests/golden/testdata/unplanned_penalty.json.golden index afb418e..7f67943 100644 --- a/tests/golden/testdata/unplanned_penalty.json.golden +++ b/tests/golden/testdata/unplanned_penalty.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/vehicle_start_end_location.json.golden b/tests/golden/testdata/vehicle_start_end_location.json.golden index 4924d73..a7574c3 100644 --- a/tests/golden/testdata/vehicle_start_end_location.json.golden +++ b/tests/golden/testdata/vehicle_start_end_location.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { @@ -75,10 +76,10 @@ "name": "1 * vehicles_duration + 1 * unplanned_penalty", "objectives": [ { - "base": 375.4720230604402, + "base": 375.47202306044016, "factor": 1, "name": "vehicles_duration", - "value": 375.4720230604402 + "value": 375.47202306044016 }, { "factor": 1, @@ -86,7 +87,7 @@ "value": 0 } ], - "value": 375.4720230604402 + "value": 375.47202306044016 }, "unplanned": [], "vehicles": [ @@ -255,7 +256,7 @@ "unplanned_stops": 0 }, "duration": 0.123, - "value": 375.4720230604402 + "value": 375.47202306044016 }, "run": { "duration": 0.123, diff --git a/tests/golden/testdata/vehicle_start_end_time.json.golden b/tests/golden/testdata/vehicle_start_end_time.json.golden index 896a660..409af4c 100644 --- a/tests/golden/testdata/vehicle_start_end_time.json.golden +++ b/tests/golden/testdata/vehicle_start_end_time.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/golden/testdata/vehicles_duration_objective.json.golden b/tests/golden/testdata/vehicles_duration_objective.json.golden index c4f3078..1f09f6a 100644 --- a/tests/golden/testdata/vehicles_duration_objective.json.golden +++ b/tests/golden/testdata/vehicles_duration_objective.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/inline_options/input.json.golden b/tests/inline_options/input.json.golden index 8abf09d..ac79ff4 100644 --- a/tests/inline_options/input.json.golden +++ b/tests/inline_options/input.json.golden @@ -28,7 +28,8 @@ "vehicle_start_time": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": { diff --git a/tests/output_options/main.sh.golden b/tests/output_options/main.sh.golden index c34ef90..21734ed 100644 --- a/tests/output_options/main.sh.golden +++ b/tests/output_options/main.sh.golden @@ -18,7 +18,8 @@ "start_time_windows": false }, "enable": { - "cluster": false + "cluster": false, + "interleave": false } }, "objectives": {