You are viewing Nextmv legacy docs. ⚡️ Go to latest docs ⚡️

Router

Custom Constraints

You will learn how to use the Constraint option with a practical example.

The router engine works with common constraints in vehicle routing problems (VRPs) out of the box, such as capacity or limits. But some problems may need very specific constraints.

The router engine provides the Constraint option to add a custom constraint. This is done by defining a custom type that implements the Constraint interface of the vehicle engine. An instance of this type is then passed into the Constraint option with the vehicle IDs to which this constraint should be applied. It is possible to use the Constraint option multiple times to add multiple custom constraints.

Constraints make sure that when stops are being inserted into a route, the route is still feasible. Filters, however, are used to prevent assigning stops to a vehicle that they are incompatible with under any circumstance. It is often good practice to also make use of a filter when using a custom constraint.

Example

The aim of this example is to define a custom type to create a custom constraint. The introductory router example is used as a base, where routes are created to visit seven landmarks in Kyoto using two vehicles.

Save the following information in an input.json file (see input and output for more information on working with input files).

{
  "stops": [
    {
      "id": "Fushimi Inari Taisha",
      "position": { "lon": 135.772695, "lat": 34.967146 }
    },
    {
      "id": "Kiyomizu-dera",
      "position": { "lon": 135.78506, "lat": 34.994857 }
    },
    {
      "id": "Nijō Castle",
      "position": { "lon": 135.748134, "lat": 35.014239 }
    },
    {
      "id": "Kyoto Imperial Palace",
      "position": { "lon": 135.762057, "lat": 35.025431 }
    },
    {
      "id": "Gionmachi",
      "position": { "lon": 135.775682, "lat": 35.002457 }
    },
    {
      "id": "Kinkaku-ji",
      "position": { "lon": 135.728898, "lat": 35.039705 }
    },
    {
      "id": "Arashiyama Bamboo Forest",
      "position": { "lon": 135.672009, "lat": 35.017209 }
    }
  ],
  "vehicles": ["v1", "v2"]
}
Copy

Code

The following program uses the CLI Runner to obtain a solution and requires access to the Nextmv code repository on GitHub. To request access, please contact support@nextmv.io.

To proceed with running the example, create a main.go file and use the code snippet below. We create a custom type that implements the Constraint interface, which we then pass into the Constraint option together with a slice of vehicle IDs.

package main

import (
    "crypto/sha1"
    "encoding/base64"
    "fmt"
    "io"

    "github.com/nextmv-io/code/engines/route"
    vehicleEngine "github.com/nextmv-io/code/engines/route/vehicle"
    "github.com/nextmv-io/code/hop/run/cli"
    "github.com/nextmv-io/code/hop/solve"
)

// Struct to read from JSON in.
type input struct {
    Stops    []route.Stop `json:"stops,omitempty"`
    Vehicles []string     `json:"vehicles,omitempty"`
}

// A custom type that implements Violated to fulfill the Constraint interface
type CustomConstraint struct {
    count int
}

// Violated is one of the two methods that must be implemented to be used as a
// Constraint
func (c CustomConstraint) Violated(state vehicleEngine.State) (vehicleEngine.Constraint, bool) {
    // Count only assigned stops, do not include start/end stops in count.
    violated := len(state.Route())-2 > c.count
    if violated {
        return nil, true
    }
    return c, false
}

// Use the CLI runner to solve a Vehicle Routing Problem.
func main() {
    f := func(i input, opt solve.Options) (solve.Solver, error) {
        // Create a custom constraint.
        constraint := CustomConstraint{count: 4}
        router, err := route.NewRouter(
            i.Stops,
            i.Vehicles,
            route.Constraint(constraint, []string{"v1", "v2"}),
        )
        if err != nil {
            return nil, err
        }

        return router.Solver(opt)
    }

    cli.Run(f)
}
Copy

To execute the example, specify the path to the input.json file using command-line flags and use jq to extract the solution state (see runners for more information on building and running programs).

go run main.go -hop.runner.input.path input.json | jq .state
Copy

Solution

The solution should look similar to this one:

{
  "unassigned": [],
  "vehicles": [
    {
      "id": "v1",
      "route": [
        {
          "id": "Arashiyama Bamboo Forest",
          "position": {
            "lon": 135.672009,
            "lat": 35.017209
          }
        },
        {
          "id": "Kinkaku-ji",
          "position": {
            "lon": 135.728898,
            "lat": 35.039705
          }
        },
        {
          "id": "Nijō Castle",
          "position": {
            "lon": 135.748134,
            "lat": 35.014239
          }
        },
        {
          "id": "Kyoto Imperial Palace",
          "position": {
            "lon": 135.762057,
            "lat": 35.025431
          }
        }
      ],
      "route_duration": 1085
    },
    {
      "id": "v2",
      "route": [
        {
          "id": "Fushimi Inari Taisha",
          "position": {
            "lon": 135.772695,
            "lat": 34.967146
          }
        },
        {
          "id": "Kiyomizu-dera",
          "position": {
            "lon": 135.78506,
            "lat": 34.994857
          }
        },
        {
          "id": "Gionmachi",
          "position": {
            "lon": 135.775682,
            "lat": 35.002457
          }
        }
      ],
      "route_duration": 448
    }
  ]
}
Copy

You can see that none of the given routes has more than four stops assigned due to the constraint which was applied to both vehicles v1 and v2.

Page last updated

Go to on-page nav menu