Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
119 changes: 119 additions & 0 deletions factory/constraint_interleave.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ func getModifiersFromOptions(options Options) []modelModifier {
modifiers = append(modifiers, addInitialSolution)
}

if options.Constraints.Enable.Interleave {
modifiers = append(modifiers, addInterleaveConstraint)
}

return modifiers
}

Expand Down
3 changes: 2 additions & 1 deletion factory/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
29 changes: 28 additions & 1 deletion model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -260,6 +265,7 @@ type modelImpl struct {
mutex sync.RWMutex
isLocked bool
disallowedSuccessors [][]bool
interleaveConstraint InterleaveConstraint
}

func (m *modelImpl) Vehicles() ModelVehicles {
Expand Down Expand Up @@ -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")
Expand All @@ -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(
Expand Down
Loading