Input and output schema
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/v1finds 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.