Input and output schema

Last updatedAugust 12, 2021

Traditional modeling and optimization tools require translating decision data (i.e. inputs) and business logic to some other form before calling some optimization algorithm. In contrast, Nextmv's tools are designed to operate directly on business data and produce decisions that are actionable by software systems. This makes decisions more interpretable and easier to test. It also makes integration with data warehouses and business intelligence platforms significantly easier.

When using Nextmv Cloud, you must follow the cloud input schema when writing your input files. Following the input schema enables Nextmv runners to automatically convert input JSON into Go types. They do this using Go's standard rules for JSON unmarshaling (the same applies to marshaling output states).

Using Nextmv Enterprise, you can write a custom input schema to use the Nextmv with any input data. See below for more information on writing custom input and output schema.

Hop solvers

Say we have the following JSON data we need to make a decision about. It has a driver's name, their capacity and location, and a vector of pickup and delivery requests to route for them.

{
  "Driver": "Mooney",
  "Capacity": 3,
  "Location": [-122.066889, 37.386274],
  "Requests": [
    [[-122.080240, 37.393735], [-122.094032, 37.393879]],
    [[-122.060621, 37.399051], [-122.066038, 37.396881]],
    [[-122.058902, 37.394507], [-122.055139, 37.390682]]
  ]
}

We define a Go struct to read this into a Hop model.

type input struct {
    Driver   string
    Capacity int
    Location [2]float64
    Requests [][2][2]float64
}

Now we need to define a decision, or system, state. In this case it probably makes sense to keep the current path as a slice of integers to refer to the locations by index.

type state struct {
    Path []int
    input input
}

There may be other data we store on our state, such as the final location of the driver or the cost of the route, but we don't show that here. What you store on your model state is up to you.

We add Feasible, Next and other methods in the [example app][dispatch-app]. Since Path is exported, Hop will automatically serialize it as the model output. All we must do is hook up the input type to a runner, construct a state, and return a solver operating on that state.

Once we go build our model into a binary, it will read input structures from standard input and write state structures to standard output. An alternative to using standard input and output is to use the -hop.runner.input.path and -hop.runner.output.path command-line flags.

Dash

Let's look at an example in a Dash simulation. The following JSON is an excerpt of an example file named input.json located in the code/dash/examples/queue directory:

[
  { "Number": 0, "Arrival":  0, "Service":  3 },
  { "Number": 1, "Arrival":  1, "Service": 10 },
  { "Number": 2, "Arrival":  1, "Service":  7 }
]

Here, we have a JSON array representing a queue of three customers waiting for some kind of service. Each has a Number, Arrival, and Service attribute. Number serves as a unique identifier in the simulation, Arrival is the number of minutes since the beginning of the simulation that the customer enters the queue and Service is the number of minutes required to service that customer.

We define a Go struct so we can read (or unmarshal) this data into our simulation:

type customer struct {
    Number  int
    Arrival int
    Service int
}

To use a customer struct as an actor in our simulation, it must have a Run method which takes a time.Time value as an argument and returns a new time.Time value and a boolean: true if the Run was successful, and false if it was not. The specifics for what happens in the Run method are up to you. Refer to the Single-Server Queue example for inspiration.

Customizing JSON formatting

Say we really don't like the uppercase names Go structures use by default for map keys. We can change the way they are read in and written out using json annotations on our structs.

type input struct {
    Driver   string       `json:"driver`
    Capacity int          `json:"capacity"`
    Location [2]float64   `json:"location"`
    Requests [][2][2]float64 `json:"requests"`
}

Our model can now read data that looks like this:

{
  "driver": "Mooney",
  "capacity": 3,
  "location": [-122.066889, 37.386274],
  "requests": [
    [[-122.080240, 37.393735], [-122.094032, 37.393879]],
    [[-122.060621, 37.399051], [-122.066038, 37.396881]],
    [[-122.058902, 37.394507], [-122.055139, 37.390682]]
  ]
}

If you need finer-grained control over how data are read in and decisions are written, you can provide UnmarshalJSON and MarshalJSON methods for the types as shown in the Go docs.

Let's also change the way our state type is written when solutions are found. Say we implement a duration method for state that estimates the time to complete a path and returns a time.Duration. We add that to our model output with a MarshalJSON method. We also add the request list from the input.

func (s state) MarshalJSON() ([]byte, error) {
    m := map[string]interface{}{
        "path":     s.Path,
        "requests": s.input.requests,
        "time":     s.duration().String(),
    }
}

Distance / duration matrix

By default our models calculate relative distances using the Haversine formula. While this works well for development, production models generally rely on data provided by a mapping service. In many cases users opt for Open Source Routing Machine (OSRM), but Hop can be used with the mapping service of your choosing. We have customers using OSRM, Google Maps API, GraphHopper and proprietary internal mapping services. Hop is agnostic to provider and can interface with any via json input.

Of note when using OSRM: OSRM can be hosted as a REST service within your infrastructure using a docker image. This will expose two endpoints:

  • /table/v1/ provides a matrix containing the distances and durations between all points within a set of coordinates.
  • /route/v1 finds the fastest route for a set of coordinates, while preserving the order.

The matrix provdied by /table/v1/ can be stripped from the response and added to the input file. When the model calculates distances it will reference the matrix.

There is a known issue where hitting each of these endpoints will return slightly different values. OSRM uses different distance calculation algorithms for /table/v1/ and /route/v1. Our recommendation is that users apply the continue_straight=false option when making requests to /route/v1. More information on this issue can be found here.

Was this helpful?