Input and output

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.08024, 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.08024, 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(),
}
}