Tutorials

Starting a new custom application

Learn how to start developing a new custom application

This tutorial assumes you already completed the steps described in the 5-minute getting started experience. To test that the Nextmv CLI is correctly configured, you can optionally run the following command on your terminal. It will get some files that are necessary to work with the Nextmv Platform. You can see the expected output as well.

nextmv install
Copy

The Nextmv Software Development Kit (SDK) lets you automate any operational decision in a way that looks and feels like writing other code. It provides the guardrails to turn your data into automated decisions and test and deploy them into production environments.

Introduction

This tutorial will walk you through our new-app template which is acts as a skeleton to help you build your own custom model from scratch.

The template contains all the key elements a decision app built with the Nextmv platform must contain as placeholders but does not implement any real functionality.

If you do not feel comfortable building your own app from scratch, our other tutorials for knapsack and shift scheduling will help you get there. In those tutorials we walk you through a working decision app, step by step.

To get the template, simply run the following command.

nextmv init -t new-app
Copy

You can check that all files are available in the newly created new-app folder. Running the tree command, you should see the file structure.

tree new-app
Copy
  • LICENSE contains the Apache License 2.0 under which we distribute this template.
  • README.md gives a short introduction to the new-app template and shows you how to run it.
  • go.mod and go.sum define a Go module and are used to manage dependencies, including the Nextmv SDK.
  • input.json describes the input data for a new-app template.
  • main.go contains the actual code of the new-app template.
  • The new-app.code-workspace file should be used to open the template in Visual Studio Code. It is pre-configured for you so that you can run and debug the template without any further steps.

Now you can run the template with the Nextmv CLI, reading from the input.json file and writing to an output.json file. The following command shows you how to specify solver limits as well. You should obtain an output similar to the one shown.

nextmv run local main.go -- \
    -hop.runner.input.path input.json \
    -hop.runner.output.path output.json \
    -hop.solver.limits.duration 5s \
    -hop.solver.diagram.expansion.limit 1
Copy

Note that transient fields are represented with "\u003c\u003cPRESENCE\u003e\u003e", which is the unicode representation of "<<PRESENCE>>", due to their dynamic nature: every time the input is run, these fields will have a different value. This representation is compliant with the jsonassert package.

Now we will show you, step by step, what the code inside the main.go achieves.

Dissecting the template

The first part of the main.go defines a package name, imports packages that are needed by the code below and a main function which is the starting point for the app. In the main function the Run function from the Nextmv run package is being called. This function executes a solver which is passed in the form of the solver function further down in the file.

package main

import (
	"github.com/nextmv-io/sdk/run"
	"github.com/nextmv-io/sdk/store"
)

// run.Run reads input data and solver options to run the solver.
func main() {
	run.Run(solver)
}
Copy

The solver function is where you actually build our model.

func solver(i input, opt store.Options) (store.Solver, error) {
Copy

It takes two parameters input and store.Options. Whilst store.Options is part of the Nextmv framework, the data struct input serves as a custom data input when running the app. For the purpose of this skeleton, input simply contains a single field, Number. We also added a json tag to automatically parse a json input data.

type input struct {
	Number int `json:"number"`
}
Copy

The central point of every decision app built with Nextmv's framework is the store which holds all of the decision variables which are created as follows:

newStore := store.New()

// Add variables that you want to track and make decisions on.
aVariable := store.NewVar(newStore, i.Number)
Copy

The last pieces that need to be implemented are a few functions: Value, Validate, Format and Generate.

The Generate function is used to branch on the search tree and traverse the search space. In this template the Lazy function with a helper variable i is used to achieve this.

newStore = newStore.Generate(func(s store.Store) store.Generator {
	i := aVariable.Get(s)
	return store.Lazy(
Copy

The Lazy function returns a new Generator of stores. It does this lazily, meaning on demand, and does this until there are no new stores to be created based on the current store. To achieve this, the Lazy function takes two functions as arguments. The first function controls if we want to generate new child stores given the current store. We use a helper variable that contains the value of aVariable of the store that the Generate function is called upon and check whether it is still greater 0.

func() bool {
	// While this condition returns true new stores will be
	// generated. Make sure that you reach a point where false is
	// returned to stop generating new stores.
	return i > 0
},
Copy

The second function takes a store and generates new child stores. Upon this child store the Generate function will be called to create new stores. We do this by creating a set of changes to be made to the store the Generate function is called on and applying the changes. By applying the changes a new store is created on which the Generate function is called upon again.

func() store.Store {
	i--
	// Create new child stores by creating and applying changes to
	// the current store.
	changes := make([]store.Change, 0)
	changes = append(
		changes,
		aVariable.Set(aVariable.Get(s)-1),
	)
	return s.Apply(changes...)
},
Copy

In the Value function the total value of a specific store is calculated. This value is used to evaluate which solution among all solutions is best. It takes a function as an input which gives us access to the store and all underlying decision variables and data. E.g., for TSP the travel time in seconds could be returned.

}).Value(func(s store.Store) int {
	// Write a term to express the solution value and return it.
	return aVariable.Get(s)
Copy

The Validate function is used to check whether the solution is feasible and satisfies the operational requirements. E.g., for a knapsack, its weight must not be exceeded.

}).Validate(func(s store.Store) bool {
	// Write a condition that checks whether the store is operationally
	// valid.
	return true
Copy

It is also recommended to implement the Format function to achieve a user-friendly output that can also be used for post-processing.

}).Format(func(s store.Store) any {
	// Define the output that you need here. E.g., you can use a map
	// like it is shown below.
	output := map[string]any{
		"new-app": "This is a skeleton example.",
		"value":   aVariable.Get(s),
	}
	return output
})
Copy

Last, in the template we return a solver using the Minimizer function.

return newStore.Minimizer(opt), nil
Copy

Page last updated

Go to on-page nav menu