Custom applications¶
The best way to get started with a custom Nextroute application is to use a
community app. The community apps are pre-built applications
that can be used as a starting point for you to begin building your own
solution. The subscription app is actually just the
go-nextroute community app that has been published to the Marketplace. The
community apps work with most features out of the box.
Use the Nextmv CLI to clone the community app you are interested in:
Once you have cloned the community app, cd into the app directory and follow
the instructions in the README.md to run the application.
You are going to find an input.json file with a sample input following the
input schema. You can use defaults to set default
properties for vehicles and stops.
{
"defaults": {
"vehicles": {
"capacity": {
"bunnies": 20,
"rabbits": 10
},
"start_location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
},
"end_location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
},
"speed": 10
},
"stops": {
"duration": 300,
"quantity": {
"bunnies": -1,
"rabbits": -1
},
"unplanned_penalty": 200000,
"target_arrival_time": "2023-01-01T10:00:00Z",
"early_arrival_time_penalty": 1.5,
"late_arrival_time_penalty": 1.5
}
},
"stops": [
{
"id": "s1",
"location": {
"lon": -78.90919,
"lat": 35.72389
},
"compatibility_attributes": ["premium"]
},
{
"id": "s2",
"location": {
"lon": -78.813862,
"lat": 35.75712
},
"compatibility_attributes": ["premium"]
},
{
"id": "s3",
"location": {
"lon": -78.92996,
"lat": 35.932795
},
"compatibility_attributes": ["premium"]
},
{
"id": "s4",
"location": {
"lon": -78.505745,
"lat": 35.77772
},
"compatibility_attributes": ["premium"]
},
{
"id": "s5",
"location": {
"lon": -78.75084,
"lat": 35.732995
},
"compatibility_attributes": ["premium"]
},
{
"id": "s6",
"location": {
"lon": -78.788025,
"lat": 35.813025
},
"compatibility_attributes": ["premium"]
},
{
"id": "s7",
"location": {
"lon": -78.749391,
"lat": 35.74261
},
"compatibility_attributes": ["premium"]
},
{
"id": "s8",
"location": {
"lon": -78.94658,
"lat": 36.039135
},
"compatibility_attributes": ["basic"]
},
{
"id": "s9",
"location": {
"lon": -78.64972,
"lat": 35.64796
},
"compatibility_attributes": ["basic"]
},
{
"id": "s10",
"location": {
"lon": -78.747955,
"lat": 35.672955
},
"compatibility_attributes": ["basic"]
},
{
"id": "s11",
"location": {
"lon": -78.83403,
"lat": 35.77013
},
"compatibility_attributes": ["basic"]
},
{
"id": "s12",
"location": {
"lon": -78.864465,
"lat": 35.782855
},
"compatibility_attributes": ["basic"]
},
{
"id": "s13",
"location": {
"lon": -78.952142,
"lat": 35.88029
},
"compatibility_attributes": ["basic"]
},
{
"id": "s14",
"location": {
"lon": -78.52748,
"lat": 35.961465
},
"compatibility_attributes": ["basic"]
},
{
"id": "s15",
"location": {
"lon": -78.89832,
"lat": 35.83202
}
},
{
"id": "s16",
"location": {
"lon": -78.63216,
"lat": 35.83458
}
},
{
"id": "s17",
"location": {
"lon": -78.76063,
"lat": 35.67337
}
},
{
"id": "s18",
"location": {
"lon": -78.911485,
"lat": 36.009015
}
},
{
"id": "s19",
"location": {
"lon": -78.522705,
"lat": 35.93663
}
},
{
"id": "s20",
"location": {
"lon": -78.995162,
"lat": 35.97414
}
},
{
"id": "s21",
"location": {
"lon": -78.50509,
"lat": 35.7606
}
},
{
"id": "s22",
"location": {
"lon": -78.828547,
"lat": 35.962635
},
"precedes": ["s16", "s23"]
},
{
"id": "s23",
"location": {
"lon": -78.60914,
"lat": 35.84616
},
"start_time_window": [
"2023-01-01T09:00:00-06:00",
"2023-01-01T09:30:00-06:00"
]
},
{
"id": "s24",
"location": {
"lon": -78.65521,
"lat": 35.740605
},
"start_time_window": [
"2023-01-01T09:00:00-06:00",
"2023-01-01T09:30:00-06:00"
],
"succeeds": "s25"
},
{
"id": "s25",
"location": {
"lon": -78.92051,
"lat": 35.887575
},
"start_time_window": [
"2023-01-01T09:00:00-06:00",
"2023-01-01T09:30:00-06:00"
],
"precedes": "s26"
},
{
"id": "s26",
"location": {
"lon": -78.84058,
"lat": 35.823865
},
"start_time_window": [
"2023-01-01T09:00:00-06:00",
"2023-01-01T09:30:00-06:00"
]
}
],
"vehicles": [
{
"id": "vehicle-0",
"start_time": "2023-01-01T06:00:00-06:00",
"end_time": "2023-01-01T10:00:00-06:00",
"activation_penalty": 4000,
"compatibility_attributes": ["premium"]
},
{
"id": "vehicle-1",
"start_time": "2023-01-01T10:00:00-06:00",
"end_time": "2023-01-01T16:00:00-06:00",
"max_duration": 21000,
"compatibility_attributes": ["basic"]
}
]
}
Depending on the language of choice, the main file contains the code to solve
a Vehicle Routing Problem.
// Package main holds the implementation for the app.
package main
import (
"context"
"log"
"github.com/nextmv-io/nextroute"
"github.com/nextmv-io/nextroute/check"
"github.com/nextmv-io/nextroute/factory"
"github.com/nextmv-io/nextroute/schema"
"github.com/nextmv-io/sdk/run"
runSchema "github.com/nextmv-io/sdk/run/schema"
)
func main() {
runner := run.CLI(solver)
err := runner.Run(context.Background())
if err != nil {
log.Fatal(err)
}
}
type options struct {
Model factory.Options `json:"model,omitempty"`
Solve nextroute.ParallelSolveOptions `json:"solve,omitempty"`
Format nextroute.FormatOptions `json:"format,omitempty"`
Check check.Options `json:"check,omitempty"`
}
func solver(
ctx context.Context,
input schema.Input,
options options,
) (runSchema.Output, error) {
model, err := factory.NewModel(input, options.Model)
if err != nil {
return runSchema.Output{}, err
}
solver, err := nextroute.NewParallelSolver(model)
if err != nil {
return runSchema.Output{}, err
}
solutions, err := solver.Solve(ctx, options.Solve)
if err != nil {
return runSchema.Output{}, err
}
last, err := solutions.Last()
if err != nil {
return runSchema.Output{}, err
}
output, err := check.Format(
ctx,
options,
options.Check,
solver,
last,
)
if err != nil {
return runSchema.Output{}, err
}
output.Statistics.Result.Custom = factory.DefaultCustomResultStatistics(last)
return output, nil
}
from typing import Any
import nextmv
import nextroute
def main() -> None:
"""Entry point for the program."""
loaded_input = nextmv.load()
options = loaded_input.options
nextmv.log("Solving vehicle routing problem:")
nextmv.log(f" - stops: {len(loaded_input.data.get('stops', []))}")
nextmv.log(f" - vehicles: {len(loaded_input.data.get('vehicles', []))}")
solution, metrics = solve(loaded_input)
nextmv.write(solution=solution, metrics=metrics, options=options)
def solve(loaded_input: nextmv.Input) -> tuple[dict[str, Any], dict[str, Any]]:
"""Solves the given problem and returns the solution and metrics."""
nextroute_input = nextroute.schema.Input.from_dict(loaded_input.data)
nextroute_options = nextroute.Options.extract_from_dict(loaded_input.options.to_dict())
nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
solution = nextroute_output.solutions[0].to_dict()
metrics = nextroute_output.statistics.to_dict()
return solution, metrics
if __name__ == "__main__":
main()
Here is an example of how to run the Nextroute community app for a duration of 5 seconds.
{
"version": {
"nextroute": "v1.12.3",
"sdk": "v1.8.3-0.20241219091227-002f36a342d6"
},
"options": {...},
"solutions": [
{
"unplanned": [...],
"vehicles": [
{
"id": "vehicle-0",
"route": [
{
"stop": {
"id": "vehicle-0-start",
"location": {
"lon": -78.7401685145487,
"lat": 35.791729813680874
}
},
"travel_duration": 0,
"cumulative_travel_duration": 0,
"arrival_time": "2023-01-01T06:00:00-06:00",
"start_time": "2023-01-01T06:00:00-06:00",
"end_time": "2023-01-01T06:00:00-06:00"
},
...
{
"stop": {
"id": "vehicle-0-end",
"location": {
"lon": -78.7401685145487,
"lat": 35.791729813680874
}
},
"travel_duration": 1084,
"cumulative_travel_duration": 10047,
"travel_distance": 10842,
"cumulative_travel_distance": 100469,
"arrival_time": "2023-01-01T09:37:27-06:00",
"start_time": "2023-01-01T09:37:27-06:00",
"end_time": "2023-01-01T09:37:27-06:00"
}
],
"route_travel_duration": 10047,
"route_travel_distance": 100469,
"route_stops_duration": 3000,
"route_duration": 13047
},
{
"id": "vehicle-1",
"route": [
{
"stop": {
"id": "vehicle-1-start",
"location": {
"lon": -78.7401685145487,
"lat": 35.791729813680874
}
},
"travel_duration": 0,
"cumulative_travel_duration": 0,
"arrival_time": "2023-01-01T10:00:00-06:00",
"start_time": "2023-01-01T10:00:00-06:00",
"end_time": "2023-01-01T10:00:00-06:00"
},
...
{
"stop": {
"id": "vehicle-1-end",
"location": {
"lon": -78.7401685145487,
"lat": 35.791729813680874
}
},
"travel_duration": 2536,
"cumulative_travel_duration": 13455,
"travel_distance": 25369,
"cumulative_travel_distance": 134545,
"arrival_time": "2023-01-01T14:34:15-06:00",
"start_time": "2023-01-01T14:34:15-06:00",
"end_time": "2023-01-01T14:34:15-06:00"
}
],
"route_travel_duration": 13455,
"route_travel_distance": 134545,
"route_stops_duration": 3000,
"route_duration": 16455
}
],
"objective": {
"name": "1 * vehicle_activation_penalty + 1 * vehicles_duration + 1 * unplanned_penalty + 1 * early_arrival_penalty + 1 * late_arrival_penalty",
"objectives": [
{
"name": "vehicle_activation_penalty",
"factor": 1,
"base": 4000,
"value": 4000
},
...
{
"name": "late_arrival_penalty",
"factor": 1,
"base": 614972.6220914125,
"value": 614972.6220914125
}
],
"value": 1848475.1355384588
}
}
],
"statistics": {
"schema": "v1",
"run": {
"duration": 5.001921375,
"iterations": 260780
},
"result": {
"duration": 0.868987291,
"value": 1848475.1355384588,
"custom": {
"activated_vehicles": 2,
"unplanned_stops": 6,
"max_travel_duration": 13455,
"max_duration": 16455,
"min_travel_duration": 10047,
"min_duration": 13047,
"max_stops_in_vehicle": 10,
"min_stops_in_vehicle": 10
}
},
"series_data": {
"value": {
"name": "1 * vehicle_activation_penalty + 1 * vehicles_duration + 1 * unplanned_penalty + 1 * early_arrival_penalty + 1 * late_arrival_penalty",
"data_points": [...]
},
"custom": [...]
}
}
}
{
"options": {
"check_duration": 30,
...
"solve_startsolutions": -1
},
"solution": {
"unplanned": [...],
"vehicles": [
{
"id": "vehicle-0",
"route": [
{
"stop": {
"id": "vehicle-0-start",
"location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
}
},
"arrival_time": "2023-01-01T06:00:00-06:00",
"cumulative_travel_duration": 0.0,
"end_time": "2023-01-01T06:00:00-06:00",
"start_time": "2023-01-01T06:00:00-06:00",
"travel_duration": 0.0
},
...
{
"stop": {
"id": "vehicle-0-end",
"location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
}
},
"arrival_time": "2023-01-01T09:37:27-06:00",
"cumulative_travel_distance": 100469.0,
"cumulative_travel_duration": 10047.0,
"end_time": "2023-01-01T09:37:27-06:00",
"start_time": "2023-01-01T09:37:27-06:00",
"travel_distance": 10842.0,
"travel_duration": 1084.0
}
],
"route_duration": 13047.0,
"route_stops_duration": 3000.0,
"route_travel_distance": 100469.0,
"route_travel_duration": 10047.0
},
{
"id": "vehicle-1",
"route": [
{
"stop": {
"id": "vehicle-1-start",
"location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
}
},
"arrival_time": "2023-01-01T10:00:00-06:00",
"cumulative_travel_duration": 0.0,
"end_time": "2023-01-01T10:00:00-06:00",
"start_time": "2023-01-01T10:00:00-06:00",
"travel_duration": 0.0
},
...
{
"stop": {
"id": "vehicle-1-end",
"location": {
"lat": 35.791729813680874,
"lon": -78.7401685145487
}
},
"arrival_time": "2023-01-01T14:34:15-06:00",
"cumulative_travel_distance": 134545.0,
"cumulative_travel_duration": 13455.0,
"end_time": "2023-01-01T14:34:15-06:00",
"start_time": "2023-01-01T14:34:15-06:00",
"travel_distance": 25369.0,
"travel_duration": 2536.0
}
],
"route_duration": 16455.0,
"route_stops_duration": 3000.0,
"route_travel_distance": 134545.0,
"route_travel_duration": 13455.0
}
],
"objective": {
"name": "1 * vehicle_activation_penalty + 1 * vehicles_duration + 1 * unplanned_penalty + 1 * early_arrival_penalty + 1 * late_arrival_penalty",
"objectives": [
{
"name": "vehicle_activation_penalty",
"factor": 1,
"base": 4000,
"value": 4000
},
...
{
"name": "late_arrival_penalty",
"factor": 1,
"base": 614972.6220914125,
"value": 614972.6220914125
}
],
"value": 1848475.1355384588
}
},
"assets": [],
"metrics": {
"run": {
"duration": 5.002631792,
"iterations": 264351
},
"result": {
"duration": 0.741473917,
"value": 1848475.1355384588,
"custom": {
"activated_vehicles": 2,
"unplanned_stops": 6,
"max_travel_duration": 13455,
"max_duration": 16455,
"min_travel_duration": 10047,
"min_duration": 13047,
"max_stops_in_vehicle": 10,
"min_stops_in_vehicle": 10
}
},
"series_data": {
"value": {
"name": "1 * vehicle_activation_penalty + 1 * vehicles_duration + 1 * unplanned_penalty + 1 * early_arrival_penalty + 1 * late_arrival_penalty",
"data_points": [...]
},
"custom": [...]
},
"schema": "v1"
}
}
The output produced by the community apps is a json that follows the output
schema. The .metrics field in the output contains the run and
result metrics, including the duration of the run, the number of iterations,
and the value of the objective function. The .solution field in the output
contains the solution to the VRP, including unassigned stops, and the full stop
assignment (routes) for each vehicle.
Both applications work similarly:
- They read a
jsoninput that follows the input schema. You can use defaults to set default properties for vehicles and stops. - They can be configured with options to customize the solution.
- They produce a
jsonoutput that follows the output schema.
You can use it as any other application. For example, you can perform runs or try experiments and tests. To take full advantage of the platform, deploy your application to Nextmv Cloud.