Skip to content

Connect your model to Nextmv with the CLI

⌛️ Approximate time to complete: 20 min.

In this tutorial you will learn how to use the Nextmv CLI to bring your own decision model to the Nextmv Platform, from scratch. Complete this tutorial if you:

  • Have a pre-existing decision model and you want to explore the Nextmv Platform.
  • Are fluent using a language of your preference, such as C++, Java, Python, etc.

To complete this tutorial, we will use three external examples, working under the principle that they are not Nextmv-created decision models. You can, and should, use your own decision model, or follow along with the examples provided:

At a high level, this tutorial will go through the following steps using the examples:

  1. Nextmv-ify the decision model.
  2. Push the model to Nextmv Cloud.
  3. Run the model remotely.
  4. Perform scenario testing.

You may follow along with the full tutorial code. Let’s dive right in 🤿.

1. Prepare the executable code

Info

If you are working with your own decision model and already know that it executes, feel free to skip this step.

The decision model is composed of executable code that solves an optimization problem. Copy the desired example code to a script named:

  • main.py for the Xpress example.
  • main.cpp for the HiGHS example.
  • src/main/java/com/google/ortools/constraintsolver/samples/VrpPickupDelivery.java for the OR-Tools example.
main.py
import xpress as xp

q = 3

starting_grid = [
    [8, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 6, 0, 0, 0, 0, 0],
    [0, 7, 0, 0, 9, 0, 2, 0, 0],
    [0, 5, 0, 0, 0, 7, 0, 0, 0],
    [0, 0, 0, 0, 4, 5, 7, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 3, 0],
    [0, 0, 1, 0, 0, 0, 0, 6, 8],
    [0, 0, 8, 5, 0, 0, 0, 1, 0],
    [0, 9, 0, 0, 0, 0, 4, 0, 0],
]

n = q**2  # the size must be the square of the size of the subgrids
N = range(n)

p = xp.problem()

x = p.addVariables(N, N, N, vartype=xp.binary)

# define all q^2 subgrids
subgrids = {
    (h, l): [(i, j) for i in range(q * h, q * h + q) for j in range(q * l, q * l + q)]
    for h in range(q)
    for l in range(q)
}

vertical = [xp.Sum(x[i, j, k] for i in N) == 1 for j in N for k in N]
horizontal = [xp.Sum(x[i, j, k] for j in N) == 1 for i in N for k in N]
subgrid = [
    xp.Sum(x[i, j, k] for (i, j) in subgrids[h, l]) == 1
    for (h, l) in subgrids.keys()
    for k in N
]

# Assign exactly one number to each cell

assign = [xp.Sum(x[i, j, k] for k in N) == 1 for i in N for j in N]

init = [
    x[i, j, k] == 1 for k in N for i in N for j in N if starting_grid[i][j] == k + 1
]

p.addConstraint(vertical, horizontal, subgrid, assign, init)

p.optimize()

print("Solution:")

for i in N:
    for j in N:
        l = [k for k in N if p.getSolution(x[i, j, k]) >= 0.5]
        assert len(l) == 1
        print("{0:2d}".format(1 + l[0]), end="", sep="")
    print("")
main.cpp
// HiGHS is designed to solve linear optimization problems of the form
//
// Min (1/2)x^TQx + c^Tx + d subject to L <= Ax <= U; l <= x <= u
//
// where A is a matrix with m rows and n columns, and Q is either zero
// or positive definite. If Q is zero, HiGHS can determine the optimal
// integer-valued solution.
//
// The scalar n is num_col_
// The scalar m is num_row_
//
// The vector c is col_cost_
// The scalar d is offset_
// The vector l is col_lower_
// The vector u is col_upper_
// The vector L is row_lower_
// The vector U is row_upper_
//
// The matrix A is represented in packed vector form, either
// row-wise or column-wise: only its nonzeros are stored
//
// * The number of nonzeros in A is num_nz
//
// * The indices of the nonnzeros in the vectors of A are stored in a_index
//
// * The values of the nonnzeros in the vectors of A are stored in a_value
//
// * The position in a_index/a_value of the index/value of the first
// nonzero in each vector is stored in a_start
//
// Note that a_start[0] must be zero
//
// The matrix Q is represented in packed column form
//
// * The dimension of Q is dim_
//
// * The number of nonzeros in Q is hessian_num_nz
//
// * The indices of the nonnzeros in the vectors of A are stored in q_index
//
// * The values of the nonnzeros in the vectors of A are stored in q_value
//
// * The position in q_index/q_value of the index/value of the first
// nonzero in each column is stored in q_start
//
// Note
//
// * By default, Q is zero. This is indicated by dim_ being initialised to zero.
//
// * q_start[0] must be zero
//
#include <cassert>

#include "Highs.h"

using std::cout;
using std::endl;

int main() {
  // Create and populate a HighsModel instance for the LP
  //
  // Min    f  =  x_0 +  x_1 + 3
  // s.t.                x_1 <= 7
  //        5 <=  x_0 + 2x_1 <= 15
  //        6 <= 3x_0 + 2x_1
  // 0 <= x_0 <= 4; 1 <= x_1
  //
  // Although the first constraint could be expressed as an upper
  // bound on x_1, it serves to illustrate a non-trivial packed
  // column-wise matrix.
  //
  HighsModel model;
  model.lp_.num_col_ = 2;
  model.lp_.num_row_ = 3;
  model.lp_.sense_ = ObjSense::kMinimize;
  model.lp_.offset_ = 3;
  model.lp_.col_cost_ = {1.0, 1.0};
  model.lp_.col_lower_ = {0.0, 1.0};
  model.lp_.col_upper_ = {4.0, 1.0e30};
  model.lp_.row_lower_ = {-1.0e30, 5.0, 6.0};
  model.lp_.row_upper_ = {7.0, 15.0, 1.0e30};
  //
  // Here the orientation of the matrix is column-wise
  model.lp_.a_matrix_.format_ = MatrixFormat::kColwise;
  // a_start_ has num_col_1 entries, and the last entry is the number
  // of nonzeros in A, allowing the number of nonzeros in the last
  // column to be defined
  model.lp_.a_matrix_.start_ = {0, 2, 5};
  model.lp_.a_matrix_.index_ = {1, 2, 0, 1, 2};
  model.lp_.a_matrix_.value_ = {1.0, 3.0, 1.0, 2.0, 2.0};
  //
  // Create a Highs instance
  Highs highs;
  HighsStatus return_status;
  //
  // Pass the model to HiGHS
  return_status = highs.passModel(model);
  assert(return_status == HighsStatus::kOk);
  // If a user passes a model with entries in
  // model.lp_.a_matrix_.value_ less than (the option)
  // small_matrix_value in magnitude, they will be ignored. A logging
  // message will indicate this, and passModel will return
  // HighsStatus::kWarning
  //
  // Get a const reference to the LP data in HiGHS
  const HighsLp& lp = highs.getLp();
  //
  // Solve the model
  return_status = highs.run();
  assert(return_status == HighsStatus::kOk);
  //
  // Get the model status
  const HighsModelStatus& model_status = highs.getModelStatus();
  assert(model_status == HighsModelStatus::kOptimal);
  cout << "Model status: " << highs.modelStatusToString(model_status) << endl;
  //
  // Get the solution information
  const HighsInfo& info = highs.getInfo();
  cout << "Simplex iteration count: " << info.simplex_iteration_count << endl;
  cout << "Objective function value: " << info.objective_function_value << endl;
  cout << "Primal  solution status: "
       << highs.solutionStatusToString(info.primal_solution_status) << endl;
  cout << "Dual    solution status: "
       << highs.solutionStatusToString(info.dual_solution_status) << endl;
  cout << "Basis: " << highs.basisValidityToString(info.basis_validity) << endl;
  const bool has_values = info.primal_solution_status;
  const bool has_duals = info.dual_solution_status;
  const bool has_basis = info.basis_validity;
  //
  // Get the solution values and basis
  const HighsSolution& solution = highs.getSolution();
  const HighsBasis& basis = highs.getBasis();
  //
  // Report the primal and solution values and basis
  for (int col = 0; col < lp.num_col_; col++) {
    cout << "Column " << col;
    if (has_values) cout << "; value = " << solution.col_value[col];
    if (has_duals) cout << "; dual = " << solution.col_dual[col];
    if (has_basis)
      cout << "; status: " << highs.basisStatusToString(basis.col_status[col]);
    cout << endl;
  }
  for (int row = 0; row < lp.num_row_; row++) {
    cout << "Row    " << row;
    if (has_values) cout << "; value = " << solution.row_value[row];
    if (has_duals) cout << "; dual = " << solution.row_dual[row];
    if (has_basis)
      cout << "; status: " << highs.basisStatusToString(basis.row_status[row]);
    cout << endl;
  }

  // Now indicate that all the variables must take integer values
  model.lp_.integrality_.resize(lp.num_col_);
  for (int col = 0; col < lp.num_col_; col++)
    model.lp_.integrality_[col] = HighsVarType::kInteger;

  highs.passModel(model);
  // Solve the model
  return_status = highs.run();
  assert(return_status == HighsStatus::kOk);
  // Report the primal solution values
  for (int col = 0; col < lp.num_col_; col++) {
    cout << "Column " << col;
    if (info.primal_solution_status)
      cout << "; value = " << solution.col_value[col];
    cout << endl;
  }
  for (int row = 0; row < lp.num_row_; row++) {
    cout << "Row    " << row;
    if (info.primal_solution_status)
      cout << "; value = " << solution.row_value[row];
    cout << endl;
  }

  highs.resetGlobalScheduler(true);

  return 0;
}
src/main/java/com/google/ortools/constraintsolver/samples/VrpPickupDelivery.java
package com.google.ortools.constraintsolver.samples;

import com.google.ortools.Loader;
import com.google.ortools.constraintsolver.Assignment;
import com.google.ortools.constraintsolver.FirstSolutionStrategy;
import com.google.ortools.constraintsolver.RoutingDimension;
import com.google.ortools.constraintsolver.RoutingIndexManager;
import com.google.ortools.constraintsolver.RoutingModel;
import com.google.ortools.constraintsolver.RoutingSearchParameters;
import com.google.ortools.constraintsolver.Solver;
import com.google.ortools.constraintsolver.main;
import java.util.logging.Logger;

/** Minimal Pickup & Delivery Problem (PDP).*/
public class VrpPickupDelivery {
  private static final Logger logger = Logger.getLogger(VrpPickupDelivery.class.getName());

  static class DataModel {
    public final long[][] distanceMatrix = {
        {0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662},
        {548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210},
        {776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754},
        {696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358},
        {582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244},
        {274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708},
        {502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480},
        {194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856},
        {308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514},
        {194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468},
        {536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354},
        {502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844},
        {388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730},
        {354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536},
        {468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194},
        {776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798},
        {662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0},
    };
    public final int[][] pickupsDeliveries = {
        {1, 6},
        {2, 10},
        {4, 3},
        {5, 9},
        {7, 8},
        {15, 11},
        {13, 12},
        {16, 14},
    };
    public final int vehicleNumber = 4;
    public final int depot = 0;
  }

  /// @brief Print the solution.
  static void printSolution(
      DataModel data, RoutingModel routing, RoutingIndexManager manager, Assignment solution) {
    // Solution cost.
    logger.info("Objective : " + solution.objectiveValue());
    // Inspect solution.
    long totalDistance = 0;
    for (int i = 0; i < data.vehicleNumber; ++i) {
      if (!routing.isVehicleUsed(solution, i)) {
        continue;
      }
      long index = routing.start(i);
      logger.info("Route for Vehicle " + i + ":");
      long routeDistance = 0;
      String route = "";
      while (!routing.isEnd(index)) {
        route += manager.indexToNode(index) + " -> ";
        long previousIndex = index;
        index = solution.value(routing.nextVar(index));
        routeDistance += routing.getArcCostForVehicle(previousIndex, index, i);
      }
      logger.info(route + manager.indexToNode(index));
      logger.info("Distance of the route: " + routeDistance + "m");
      totalDistance += routeDistance;
    }
    logger.info("Total Distance of all routes: " + totalDistance + "m");
  }

  public static void main(String[] args) throws Exception {
    Loader.loadNativeLibraries();
    // Instantiate the data problem.
    final DataModel data = new DataModel();

    // Create Routing Index Manager
    RoutingIndexManager manager =
        new RoutingIndexManager(data.distanceMatrix.length, data.vehicleNumber, data.depot);

    // Create Routing Model.
    RoutingModel routing = new RoutingModel(manager);

    // Create and register a transit callback.
    final int transitCallbackIndex =
        routing.registerTransitCallback((long fromIndex, long toIndex) -> {
          // Convert from routing variable Index to user NodeIndex.
          int fromNode = manager.indexToNode(fromIndex);
          int toNode = manager.indexToNode(toIndex);
          return data.distanceMatrix[fromNode][toNode];
        });

    // Define cost of each arc.
    routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);

    // Add Distance constraint.
    boolean unused = routing.addDimension(transitCallbackIndex, // transit callback index
        0, // no slack
        3000, // vehicle maximum travel distance
        true, // start cumul to zero
        "Distance");
    RoutingDimension distanceDimension = routing.getMutableDimension("Distance");
    distanceDimension.setGlobalSpanCostCoefficient(100);

    // Define Transportation Requests.
    Solver solver = routing.solver();
    for (int[] request : data.pickupsDeliveries) {
      long pickupIndex = manager.nodeToIndex(request[0]);
      long deliveryIndex = manager.nodeToIndex(request[1]);
      routing.addPickupAndDelivery(pickupIndex, deliveryIndex);
      solver.addConstraint(
          solver.makeEquality(routing.vehicleVar(pickupIndex), routing.vehicleVar(deliveryIndex)));
      solver.addConstraint(solver.makeLessOrEqual(
          distanceDimension.cumulVar(pickupIndex), distanceDimension.cumulVar(deliveryIndex)));
    }

    // Setting first solution heuristic.
    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION)
            .build();

    // Solve the problem.
    Assignment solution = routing.solveWithParameters(searchParameters);

    // Print solution on console.
    printSolution(data, routing, manager, solution);
  }
}

2. Set up requirements

Info

If you are working with your own decision model and already have all requirements ready for it, feel free to skip this step.

The examples require different setups. These are the requirements that you need before compiling and running the examples.

2.1. Xpress

If you are following along with the full tutorial code, you should be using the original directory. In that case, run the following command in the root of your project.

uv sync

On the other hand, if you are completing this tutorial from scratch, run the following command in the root of your project.

uv init --bare # Execute if you don't have a pyproject.toml file in your project.
uv add xpress

2.2. HiGHS

Stand at the root of the project where you placed the main.cpp file.

  • Make sure you can run g++ (C++ compiler).

  • Make sure you can run cmake (CMake build system).

  • Clone HiGHS from GitHub to build the solver.

    git clone https://github.com/ERGO-Code/HiGHS.git
    
  • Build HiGHS, the solver itself.

    cd HiGHS
    cmake -S. -B build 
    cmake --build build --parallel
    cd ..
    
  • After building, a ./HiGHS/build/lib directory should exist with the necessary libraries.

2.3. OR-Tools

Stand at the root of the project where you placed the src directory.

  • Make sure you can run java (JDK 11 or higher).

  • Make sure you can run mvn (Maven).

  • Create a pom.xml file to manage dependencies.

    pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.google.ortools</groupId>
        <artifactId>vrp-pickup-delivery</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <name>VRP Pickup Delivery</name>
        <description>Vehicle Routing Problem with Pickup and Delivery using OR-Tools</description>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>11</maven.compiler.source>
            <maven.compiler.target>11</maven.compiler.target>
            <ortools.version>9.8.3296</ortools.version>
        </properties>
    
        <dependencies>
            <!-- OR-Tools dependency -->
            <dependency>
                <groupId>com.google.ortools</groupId>
                <artifactId>ortools-java</artifactId>
                <version>${ortools.version}</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <!-- Maven Compiler Plugin -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>11</source>
                        <target>11</target>
                    </configuration>
                </plugin>
    
                <!-- Maven Exec Plugin for running the application -->
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <mainClass>com.google.ortools.constraintsolver.samples.VrpPickupDelivery</mainClass>
                    </configuration>
                </plugin>
    
                <!-- Maven Shade Plugin for creating an executable JAR -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>3.5.1</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <createDependencyReducedPom>false</createDependencyReducedPom>
                                <shadedArtifactAttached>false</shadedArtifactAttached>
                                <outputDirectory>.</outputDirectory>
                                <finalName>main</finalName>
                                <transformers>
                                    <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                        <mainClass>
                                            com.google.ortools.constraintsolver.samples.VrpPickupDelivery</mainClass>
                                    </transformer>
                                </transformers>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

3. Compile and run the executable code

Info

If you are working with your own decision model and already know that it executes, feel free to skip this step.

These are the steps for compiling and running the examples.

3.1. Xpress

Stand at the root of the project where you placed the main.py file. Execute the following command to run the code.

uv run main.py
FICO Xpress v9.9.1, Community, solve started 11:02:39, Jul 9, 2026
Heap usage: 755KB (peak 755KB, 58KB system)
Minimizing MILP noname using up to 10 threads and up to 32GB memory, with these control settings:
OUTPUTLOG = 1
NLPPOSTSOLVE = 1
XSLP_DELETIONCONTROL = 0
XSLP_OBJSENSE = 1
Original problem has:
      345 rows          729 cols         2937 elements       729 entities
Presolved problem has:
      141 rows          201 cols          595 elements       201 entities
LP relaxation tightened
Presolve finished in 0 seconds
Heap usage: 2355KB (peak 2355KB, 58KB system)

Coefficient range                    original                 solved        
  Coefficients   [min,max] : [ 1.00e+00,  1.00e+00] / [ 1.00e+00,  1.00e+00]
  RHS and bounds [min,max] : [ 1.00e+00,  1.00e+00] / [ 1.00e+00,  1.00e+00]
  Objective      [min,max] : [      0.0,       0.0] / [      0.0,       0.0]
Autoscaling applied standard scaling

Will try to keep branch and bound tree memory usage below 30.3GB
Starting concurrent solve with dual (1 thread)

Concurrent-Solve,   0s
            Dual        
    objective   dual inf

------- optimal --------
Concurrent statistics:
          Dual: 207 simplex iterations, 0.00s
Optimal solution found

  Its         Obj Value      S   Ninf  Nneg        Sum Inf  Time
  207           .000000      P      0     0        .000000     0
Dual solved problem
  207 simplex iterations in 0.00 seconds at time 0

Final objective                       : 0.000000000000000e+00
  Max primal violation      (abs/rel) :       0.0 /       0.0
  Max dual violation        (abs/rel) :       0.0 /       0.0
  Max complementarity viol. (abs/rel) :       0.0 /       0.0

Starting root cutting & heuristics
Deterministic mode with up to 4 additional threads

Its Type    BestSoln    BestBound   Sols    Add    Del     Gap     GInf   Time
P             .000000      .000000      1               0.0e+00        0      0
STOPPING - MIPRELSTOP target reached (MIPRELSTOP=0.0001  gap=0).
*** Search completed ***
Uncrunching matrix
Final MIP objective                   : 0.000000000000000e-01
Final MIP bound                       : 0.000000000000000e-01
  Solution time / primaldual integral :      0.02s/ 98.601964%
  Work / work units per second        :      0.02 /      1.20
  Number of solutions found / nodes   :         1 /         0
  Max primal violation      (abs/rel) :       0.0 /       0.0
  Max integer violation     (abs    ) :       0.0
Solution:
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2

3.2. HiGHS

Stand at the root of the project where you placed the main.cpp file.

  • Execute the following command to compile the code.

    g++ -std=c++11 main.cpp -o main \
      -I./HiGHS/highs \
      -I./HiGHS/build \
      -L./HiGHS/build/lib -lhighs
    
  • A ./main binary should have been created.

  • Execute the following command to run the code.

DYLD_LIBRARY_PATH=./HiGHS/build/lib ./main
LD_LIBRARY_PATH=./HiGHS/build/lib ./main
Running HiGHS 1.12.0 (git hash: 869cbd4df): Copyright (c) 2025 HiGHS under MIT licence terms
Cols:           1 upper bounds greater than or equal to        1e+20 are treated as +Infinity
Rows:           1 lower bounds    less than or equal to       -1e+20 are treated as -Infinity
Rows:           1 upper bounds greater than or equal to        1e+20 are treated as +Infinity
LP has 3 rows; 2 cols; 5 nonzeros
Coefficient ranges:
  Matrix  [1e+00, 3e+00]
  Cost    [1e+00, 1e+00]
  Bound   [1e+00, 4e+00]
  RHS     [5e+00, 2e+01]
Presolving model
2 rows, 2 cols, 4 nonzeros  0s
2 rows, 2 cols, 4 nonzeros  0s
Presolve reductions: rows 2(-1); columns 2(-0); nonzeros 4(-1)
Solving the presolved LP
Using EKK dual simplex solver - serial
  Iteration        Objective     Infeasibilities num(sum)
          0     4.0000013886e+00 Pr: 2(7) 0s
          2     5.7500000000e+00 Pr: 0(0) 0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model status        : Optimal
Simplex   iterations: 2
Objective value     :  5.7500000000e+00
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
Model status: Optimal
Simplex iteration count: 2
Objective function value: 5.75
Primal  solution status: Feasible
Dual    solution status: Feasible
Basis: Valid
Column 0; value = 0.5; dual = 0; status: Basic
Column 1; value = 2.25; dual = 0; status: Basic
Row    0; value = 2.25; dual = -0; status: Basic
Row    1; value = 5; dual = 0.25; status: At lower/fixed bound
Row    2; value = 6; dual = 0.25; status: At lower/fixed bound
Cols:           1 upper bounds greater than or equal to        1e+20 are treated as +Infinity
Rows:           1 lower bounds    less than or equal to       -1e+20 are treated as -Infinity
Rows:           1 upper bounds greater than or equal to        1e+20 are treated as +Infinity
MIP has 3 rows; 2 cols; 5 nonzeros; 2 integer variables (0 binary)
Coefficient ranges:
  Matrix  [1e+00, 3e+00]
  Cost    [1e+00, 1e+00]
  Bound   [1e+00, 4e+00]
  RHS     [5e+00, 2e+01]
Presolving model
2 rows, 2 cols, 4 nonzeros  0s
2 rows, 2 cols, 4 nonzeros  0s
Presolve reductions: rows 2(-1); columns 2(-0); nonzeros 4(-1)
Objective function is integral with scale 1

Solving MIP model with:
  2 rows
  2 cols (0 binary, 2 integer, 0 implied int., 0 continuous, 0 domain fixed)
  4 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
    I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
    S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
    Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

u       0       0         0   0.00%   -inf            9                  Large        0      0      0         0     0.0s
J       0       0         0 100.00%   -inf            6                  Large        0      0      0         0     0.0s
        1       0         1 100.00%   6               6                  0.00%        0      0      0         0     0.0s

Solving report
  Status            Optimal
  Primal bound      6
  Dual bound        6
  Gap               0% (tolerance: 0.01%)
  P-D integral      2.22166818276e-07
  Solution status   feasible
                    6 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.01
  Max sub-MIP depth 0
  Nodes             1
  Repair LPs        0
  LP iterations     0
Column 0; value = -0
Column 1; value = 3
Row    0; value = 3
Row    1; value = 6
Row    2; value = 6

3.3. OR-Tools

Stand at the root of the project where you placed the src directory.

  • Execute the following command to compile the code.

    mvn clean package
    
  • A ./main.jar file should have been created.

  • Execute the following command to run the code.

java -jar main.jar
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Objective : 226116
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 0:
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 13 -> 15 -> 11 -> 12 -> 0
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1552m
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 1:
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 5 -> 2 -> 10 -> 16 -> 14 -> 9 -> 0
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 2192m
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 2:
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 4 -> 3 -> 0
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1392m
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 3:
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 7 -> 1 -> 6 -> 8 -> 0
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1780m
Nov 26, 2025 1:26:43 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Total Distance of all routes: 6916m

4. Nextmv-ify the decision model

We are going to turn the executable decision model into a Nextmv application.

Tip

Go to the Applications section to learn more about Nextmv applications.

We are going to adapt the examples so that they can follow the conventions of a Nextmv application, such as:

  • The app receives one, or more, inputs (problem data) through stdin or files.
  • The app run can be configured through options that are received as CLI arguments.
  • The app processes the inputs, and executes the decision model.
  • The app produces one, or more, outputs (solutions) and prints to stdout or files.
  • The app optionally produces metrics and assets (can be visual, like charts).

Start by adding the app.yaml file, which is known as the app manifest, to the root of the project. This file contains the configuration of the app.

app.yaml
type: python
runtime: ghcr.io/nextmv-io/runtime/python:3.11
files:
  - main.py
python:
  pip-requirements: pyproject.toml # Can be a requirements.txt
configuration:
  content:
    format: json
  options:
    items:
      - name: duration
        description: Duration for the solver, in seconds.
        required: false
        option_type: float
        default: 1
        additional_attributes:
          min: 0
          max: 10
          step: 1
        ui:
          control_type: slider
app.yaml
type: binary
runtime: ghcr.io/nextmv-io/runtime/default:latest
files:
  - main
  - HiGHS/build/lib/
build:
  command: bash ./build.sh
execution:
  entrypoint: main
configuration:
  content:
    format: json
  options:
    items:
      - name: duration
        description: Duration for the solver, in seconds.
        required: false
        option_type: float
        default: 1
        additional_attributes:
          min: 0
          max: 10
          step: 1
        ui:
          control_type: slider
app.yaml
type: java
runtime: ghcr.io/nextmv-io/runtime/java:latest
files:
  - main.jar
build:
  command: mvn clean package
execution:
  entrypoint: main.jar
configuration:
  content:
    format: multi-file
    multi-file:
      input:
        path: inputs
      output:
        solutions: outputs/solutions
        metrics: outputs/metrics.json
        assets: outputs/assets.json
  options:
    items:
      - name: vehicle_maximum_travel_distance
        description: Maximum travel distance for each vehicle, in meters.
        required: false
        option_type: float
        default: 3000
        additional_attributes:
          min: 10
          max: 10000
          step: 100
        ui:
          control_type: slider
      - name: global_span_cost_coefficient
        description: Coefficient for the global span cost in the objective function.
        required: false
        option_type: float
        default: 100
        additional_attributes:
          min: 1
          max: 10000
          step: 10
        ui:
          control_type: slider

This tutorial is not meant to discuss the app manifest in-depth, for that you can go to the manifest docs. However, these are the main attributes shown in the manifests:

  • type: the examples demonstrate different types of applications.
  • runtime: Xpress uses the python runtime, HiGHS uses default to execute binaries, and OR-Tools uses the java runtime to execute Java applications.
  • build.command: specifies in some cases the command to build the executable code. We are going to compile an artifact for HiGHS and OR-Tools.
  • execution.entrypoint: specifies the executable code to run.
  • files: contains files that make up the executable code of the app. For Xpress we need the main.py, for HiGHS we are including the main binary and the required libraries, and finally for OR-Tools, we are including the main.jar file.
  • python.pip-requirements: specifies for Xpress the file with the Python packages that need to be installed for the application.
  • configuration.content: Xpress and HiGHS will use the json format, so they do not need additional configurations. OR-Tools will use multi-file, so additional configurations are needed. As you complete this tutorial, the difference between the two formats will become clearer.
  • configuration.options: for the examples we are adding options to the application, which allow you to configure runs, with parameters such as solver duration.

Now, you can overwrite your decision model files with the Nextmv-ified version.

main.py
import nextmv
import xpress as xp

input = nextmv.load()
q = input.data["q"]
starting_grid = input.data["starting_grid"]

n = q**2  # the size must be the square of the size of the subgrids
N = range(n)

nextmv.redirect_stdout()  # Solver chatter is logged to stderr.

p = xp.problem()
p.setControl("timelimit", input.options.duration)

x = p.addVariables(N, N, N, vartype=xp.binary)

# define all q^2 subgrids
subgrids = {
    (h, l): [(i, j) for i in range(q * h, q * h + q) for j in range(q * l, q * l + q)]
    for h in range(q)
    for l in range(q)
}

vertical = [xp.Sum(x[i, j, k] for i in N) == 1 for j in N for k in N]
horizontal = [xp.Sum(x[i, j, k] for j in N) == 1 for i in N for k in N]
subgrid = [
    xp.Sum(x[i, j, k] for (i, j) in subgrids[h, l]) == 1
    for (h, l) in subgrids.keys()
    for k in N
]

# Assign exactly one number to each cell

assign = [xp.Sum(x[i, j, k] for k in N) == 1 for i in N for j in N]

init = [
    x[i, j, k] == 1 for k in N for i in N for j in N if starting_grid[i][j] == k + 1
]

p.addConstraint(vertical, horizontal, subgrid, assign, init)

p.optimize()

print("Solution:")
solution = {}
rows = []
for i in N:
    row = []
    for j in N:
        l = [k for k in N if p.getSolution(x[i, j, k]) >= 0.5]
        assert len(l) == 1
        value = 1 + l[0]
        row.append(value)
        print("{0:2d}".format(value), end="", sep="")
    rows.append(row)
    print("")

for i, row in enumerate(rows):
    solution[f"row_{i}"] = row

metrics = {
    "duration": p.attributes.time,
    "objective": p.attributes.objval,
    "num_variables": p.attributes.cols,
    "num_constraints": p.attributes.rows,
}

nextmv.write(solution=solution, metrics=metrics, options=input.options)
main.cpp
// HiGHS is designed to solve linear optimization problems of the form
//
// Min (1/2)x^TQx + c^Tx + d subject to L <= Ax <= U; l <= x <= u
//
// where A is a matrix with m rows and n columns, and Q is either zero
// or positive definite. If Q is zero, HiGHS can determine the optimal
// integer-valued solution.
//
// The scalar n is num_col_
// The scalar m is num_row_
//
// The vector c is col_cost_
// The scalar d is offset_
// The vector l is col_lower_
// The vector u is col_upper_
// The vector L is row_lower_
// The vector U is row_upper_
//
// The matrix A is represented in packed vector form, either
// row-wise or column-wise: only its nonzeros are stored
//
// * The number of nonzeros in A is num_nz
//
// * The indices of the nonnzeros in the vectors of A are stored in a_index
//
// * The values of the nonnzeros in the vectors of A are stored in a_value
//
// * The position in a_index/a_value of the index/value of the first
// nonzero in each vector is stored in a_start
//
// Note that a_start[0] must be zero
//
// The matrix Q is represented in packed column form
//
// * The dimension of Q is dim_
//
// * The number of nonzeros in Q is hessian_num_nz
//
// * The indices of the nonnzeros in the vectors of A are stored in q_index
//
// * The values of the nonnzeros in the vectors of A are stored in q_value
//
// * The position in q_index/q_value of the index/value of the first
// nonzero in each column is stored in q_start
//
// Note
//
// * By default, Q is zero. This is indicated by dim_ being initialised to zero.
//
// * q_start[0] must be zero
//
#include <cassert>
#include <chrono>
#include <fstream>
#include <iostream>
#include <string>

#include "Highs.h"
#include "nlohmann/json.hpp"

#include <sstream>
#include <vector>

using std::cout;
using std::endl;
using json = nlohmann::json;

int main(int argc, char* argv[]) {
  // Start timing
  auto start_time = std::chrono::high_resolution_clock::now();

  // Parse command line arguments for time_limit
  double time_limit = 0.0;  // 0 means no limit
  for (int i = 1; i < argc; i++) {
    std::string arg = argv[i];
    if (arg.find("--time_limit=") == 0) {
      time_limit = std::stod(arg.substr(13));
    } else if (arg == "--time_limit" && i + 1 < argc) {
      time_limit = std::stod(argv[++i]);
    }
  }

  // Read and parse JSON from stdin
  json j;
  try {
    std::cin >> j;
  } catch (const json::parse_error& e) {
    std::cerr << "Error: Failed to parse JSON input: " << e.what() << std::endl;
    return 1;
  }

  if (j.empty()) {
    std::cerr << "Error: No input provided on stdin" << std::endl;
    return 1;
  }

  // Parse the JSON and create the model
  HighsModel model;
  model.lp_.num_col_ = j["num_col"].get<int>();
  model.lp_.num_row_ = j["num_row"].get<int>();
  model.lp_.sense_ = ObjSense::kMinimize;
  model.lp_.offset_ = j.value("offset", 0.0);
  model.lp_.col_cost_ = j["col_cost"].get<std::vector<double>>();
  model.lp_.col_lower_ = j["col_lower"].get<std::vector<double>>();
  model.lp_.col_upper_ = j["col_upper"].get<std::vector<double>>();
  model.lp_.row_lower_ = j["row_lower"].get<std::vector<double>>();
  model.lp_.row_upper_ = j["row_upper"].get<std::vector<double>>();

  // Parse the a_matrix object
  const auto& matrix = j["a_matrix"];

  // Set matrix format from JSON
  std::string format = matrix["format"].get<std::string>();
  if (format == "rowwise") {
    model.lp_.a_matrix_.format_ = MatrixFormat::kRowwise;
  } else {
    model.lp_.a_matrix_.format_ = MatrixFormat::kColwise;
  }
  model.lp_.a_matrix_.start_ = matrix["start"].get<std::vector<int>>();
  model.lp_.a_matrix_.index_ = matrix["index"].get<std::vector<int>>();
  model.lp_.a_matrix_.value_ = matrix["value"].get<std::vector<double>>();
  //
  // Create a Highs instance
  Highs highs;
  HighsStatus return_status;

  // Suppress HiGHS output
  highs.setOptionValue("output_flag", false);
  highs.setOptionValue("log_to_console", false);

  // Set time limit if provided
  if (time_limit > 0.0) {
    highs.setOptionValue("time_limit", time_limit);
    std::cerr << "Time limit set to " << time_limit << " seconds" << endl;
  }
  //
  // Pass the model to HiGHS
  return_status = highs.passModel(model);
  assert(return_status == HighsStatus::kOk);
  // If a user passes a model with entries in
  // model.lp_.a_matrix_.value_ less than (the option)
  // small_matrix_value in magnitude, they will be ignored. A logging
  // message will indicate this, and passModel will return
  // HighsStatus::kWarning
  //
  // Get a const reference to the LP data in HiGHS
  const HighsLp& lp = highs.getLp();
  //
  // Solve the model
  return_status = highs.run();
  assert(return_status == HighsStatus::kOk);
  //
  // Get the model status
  const HighsModelStatus& model_status = highs.getModelStatus();
  assert(model_status == HighsModelStatus::kOptimal);
  std::cerr << "Model status: " << highs.modelStatusToString(model_status) << endl;
  //
  // Get the solution information
  const HighsInfo& info = highs.getInfo();
  std::cerr << "Simplex iteration count: " << info.simplex_iteration_count << endl;
  std::cerr << "Objective function value: " << info.objective_function_value << endl;
  std::cerr << "Primal  solution status: "
       << highs.solutionStatusToString(info.primal_solution_status) << endl;
  std::cerr << "Dual    solution status: "
       << highs.solutionStatusToString(info.dual_solution_status) << endl;
  std::cerr << "Basis: " << highs.basisValidityToString(info.basis_validity) << endl;
  const bool has_values = info.primal_solution_status;
  const bool has_duals = info.dual_solution_status;
  const bool has_basis = info.basis_validity;
  //
  // Get the solution values and basis
  const HighsSolution& solution = highs.getSolution();
  const HighsBasis& basis = highs.getBasis();
  //
  // Report the primal and solution values and basis to stderr
  for (int col = 0; col < lp.num_col_; col++) {
    std::cerr << "Column " << col;
    if (has_values) std::cerr << "; value = " << solution.col_value[col];
    if (has_duals) std::cerr << "; dual = " << solution.col_dual[col];
    if (has_basis)
      std::cerr << "; status: " << highs.basisStatusToString(basis.col_status[col]);
    std::cerr << endl;
  }
  for (int row = 0; row < lp.num_row_; row++) {
    std::cerr << "Row    " << row;
    if (has_values) std::cerr << "; value = " << solution.row_value[row];
    if (has_duals) std::cerr << "; dual = " << solution.row_dual[row];
    if (has_basis)
      std::cerr << "; status: " << highs.basisStatusToString(basis.row_status[row]);
    std::cerr << endl;
  }

  // Now indicate that all the variables must take integer values from JSON
  if (j.contains("integrality")) {
    std::vector<int> integrality = j["integrality"].get<std::vector<int>>();
    if (!integrality.empty()) {
      model.lp_.integrality_.resize(lp.num_col_);
      for (int col = 0; col < lp.num_col_; col++)
        model.lp_.integrality_[col] = integrality[col] == 1 ? HighsVarType::kInteger : HighsVarType::kContinuous;
    }
  }

  highs.passModel(model);
  // Solve the model
  return_status = highs.run();
  assert(return_status == HighsStatus::kOk);

  // Calculate duration
  auto end_time = std::chrono::high_resolution_clock::now();
  std::chrono::duration<double> duration = end_time - start_time;

  // Build output JSON
  json output;

  // Add solution columns
  json columns = json::array();
  for (int col = 0; col < lp.num_col_; col++) {
    json column_obj;
    column_obj["index"] = col;
    if (info.primal_solution_status) {
      column_obj["value"] = solution.col_value[col];
    }
    columns.push_back(column_obj);
  }

  // Add solution rows
  json rows = json::array();
  for (int row = 0; row < lp.num_row_; row++) {
    json row_obj;
    row_obj["index"] = row;
    if (info.primal_solution_status) {
      row_obj["value"] = solution.row_value[row];
    }
    rows.push_back(row_obj);
  }

  // Build complete output structure
  output["solution"]["columns"] = columns;
  output["solution"]["rows"] = rows;
  output["metrics"]["objective_value"] = info.objective_function_value;
  output["metrics"]["duration"] = duration.count();
  output["metrics"]["simplex_iteration_count"] = info.simplex_iteration_count;
  output["metrics"]["status"] = highs.modelStatusToString(model_status);

  // Output formatted JSON to stdout
  cout << output.dump(2) << endl;

  highs.resetGlobalScheduler(true);

  return 0;
}
src/main/java/com/google/ortools/constraintsolver/samples/VrpPickupDelivery.java
package com.google.ortools.constraintsolver.samples;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.ortools.Loader;
import com.google.ortools.constraintsolver.Assignment;
import com.google.ortools.constraintsolver.FirstSolutionStrategy;
import com.google.ortools.constraintsolver.RoutingDimension;
import com.google.ortools.constraintsolver.RoutingIndexManager;
import com.google.ortools.constraintsolver.RoutingModel;
import com.google.ortools.constraintsolver.RoutingSearchParameters;
import com.google.ortools.constraintsolver.Solver;
import com.google.ortools.constraintsolver.main;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/** Minimal Pickup & Delivery Problem (PDP).*/
public class VrpPickupDelivery {
  private static final Logger logger = Logger.getLogger(VrpPickupDelivery.class.getName());

  static class DataModel {
    public long[][] distanceMatrix;
    public int[][] pickupsDeliveries;
    public int vehicleNumber;
    public int depot;

    public static DataModel loadFromFiles(String distanceFile, String pickupsDeliveriesFile, String problemFile) throws IOException {
      DataModel data = new DataModel();
      Gson gson = new Gson();

      // Load distance matrix
      try (FileReader reader = new FileReader(distanceFile)) {
        data.distanceMatrix = gson.fromJson(reader, long[][].class);
      }

      // Load pickups and deliveries
      try (FileReader reader = new FileReader(pickupsDeliveriesFile)) {
        data.pickupsDeliveries = gson.fromJson(reader, int[][].class);
      }

      // Load problem configuration
      try (FileReader reader = new FileReader(problemFile)) {
        JsonObject problemConfig = gson.fromJson(reader, JsonObject.class);
        data.vehicleNumber = problemConfig.get("vehicleNumber").getAsInt();
        data.depot = problemConfig.get("depot").getAsInt();
      }

      return data;
    }
  }

  static class Route {
    public int vehicle;
    public List<Integer> stops;
    public long distance;

    public Route(int vehicle, List<Integer> stops, long distance) {
      this.vehicle = vehicle;
      this.stops = stops;
      this.distance = distance;
    }
  }

  static class Solution {
    public List<Route> routes;

    public Solution(List<Route> routes) {
      this.routes = routes;
    }
  }

  static class SolutionOutput {
    public Solution solution;

    public SolutionOutput(Solution solution) {
      this.solution = solution;
    }
  }

  /// @brief Save the solution to a JSON file.
  static long saveSolutionToFile(
      DataModel data, RoutingModel routing, RoutingIndexManager manager, Assignment solution, String outputFile) throws IOException {
    List<Route> routes = new ArrayList<>();
    long totalDistance = 0;

    for (int i = 0; i < data.vehicleNumber; ++i) {
      if (!routing.isVehicleUsed(solution, i)) {
        continue;
      }

      List<Integer> stops = new ArrayList<>();
      long index = routing.start(i);
      long routeDistance = 0;

      while (!routing.isEnd(index)) {
        stops.add(manager.indexToNode(index));
        long previousIndex = index;
        index = solution.value(routing.nextVar(index));
        routeDistance += routing.getArcCostForVehicle(previousIndex, index, i);
      }
      stops.add(manager.indexToNode(index)); // Add final stop

      routes.add(new Route(i, stops, routeDistance));
      totalDistance += routeDistance;
    }

    Solution solutionData = new Solution(routes);
    SolutionOutput output = new SolutionOutput(solutionData);

    // Create output directory if it doesn't exist
    File file = new File(outputFile);
    file.getParentFile().mkdirs();

    // Write to JSON file
    Gson gson = new Gson();
    try (FileWriter writer = new FileWriter(outputFile)) {
      gson.toJson(output, writer);
    }

    logger.info("Solution saved to " + outputFile);
    return totalDistance;
  }

  /// @brief Save the metrics to a JSON file.
  static void saveMetricsToFile(
      double objectiveValue, double durationSeconds, long totalDistance, int totalRoutes, String outputFile) throws IOException {
    java.util.Map<String, Object> metrics = new java.util.LinkedHashMap<>();
    metrics.put("objective_value", objectiveValue);
    metrics.put("duration_seconds", durationSeconds);
    metrics.put("total_distance", totalDistance);
    metrics.put("total_routes", totalRoutes);

    // Create output directory if it doesn't exist
    File file = new File(outputFile);
    file.getParentFile().mkdirs();

    // Write to JSON file
    Gson gson = new Gson();
    try (FileWriter writer = new FileWriter(outputFile)) {
      gson.toJson(metrics, writer);
    }

    logger.info("Metrics saved to " + outputFile);
  }

  /// @brief Print the solution.
  static void printSolution(
      DataModel data, RoutingModel routing, RoutingIndexManager manager, Assignment solution) {
    // Solution cost.
    logger.info("Objective : " + solution.objectiveValue());
    // Inspect solution.
    long totalDistance = 0;
    for (int i = 0; i < data.vehicleNumber; ++i) {
      if (!routing.isVehicleUsed(solution, i)) {
        continue;
      }
      long index = routing.start(i);
      logger.info("Route for Vehicle " + i + ":");
      long routeDistance = 0;
      String route = "";
      while (!routing.isEnd(index)) {
        route += manager.indexToNode(index) + " -> ";
        long previousIndex = index;
        index = solution.value(routing.nextVar(index));
        routeDistance += routing.getArcCostForVehicle(previousIndex, index, i);
      }
      logger.info(route + manager.indexToNode(index));
      logger.info("Distance of the route: " + routeDistance + "m");
      totalDistance += routeDistance;
    }
    logger.info("Total Distance of all routes: " + totalDistance + "m");
  }

  public static void main(String[] args) throws Exception {
    Loader.loadNativeLibraries();

    // Parse command line arguments
    int vehicleMaximumTravelDistance = 3000; // default value
    int globalSpanCostCoefficient = 100; // default value
    for (int i = 0; i < args.length; i++) {
      if (args[i].equals("--vehicle_maximum_travel_distance") && i + 1 < args.length) {
        vehicleMaximumTravelDistance = Integer.parseInt(args[i + 1]);
      } else if (args[i].equals("--global_span_cost_coefficient") && i + 1 < args.length) {
        globalSpanCostCoefficient = Integer.parseInt(args[i + 1]);
      }
    }

    // Define input file paths
    String distanceFile = "inputs/distance.json";
    String pickupsDeliveriesFile = "inputs/pickups_deliveries.json";
    String problemFile = "inputs/problem.json";

    // Load the data from JSON files
    final DataModel data = DataModel.loadFromFiles(distanceFile, pickupsDeliveriesFile, problemFile);

    // Create Routing Index Manager
    RoutingIndexManager manager =
        new RoutingIndexManager(data.distanceMatrix.length, data.vehicleNumber, data.depot);

    // Create Routing Model.
    RoutingModel routing = new RoutingModel(manager);

    // Create and register a transit callback.
    final int transitCallbackIndex =
        routing.registerTransitCallback((long fromIndex, long toIndex) -> {
          // Convert from routing variable Index to user NodeIndex.
          int fromNode = manager.indexToNode(fromIndex);
          int toNode = manager.indexToNode(toIndex);
          return data.distanceMatrix[fromNode][toNode];
        });

    // Define cost of each arc.
    routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);

    // Add Distance constraint.
    boolean unused = routing.addDimension(transitCallbackIndex, // transit callback index
        0, // no slack
        vehicleMaximumTravelDistance, // vehicle maximum travel distance
        true, // start cumul to zero
        "Distance");
    RoutingDimension distanceDimension = routing.getMutableDimension("Distance");
    distanceDimension.setGlobalSpanCostCoefficient(globalSpanCostCoefficient);

    // Define Transportation Requests.
    Solver solver = routing.solver();
    for (int[] request : data.pickupsDeliveries) {
      long pickupIndex = manager.nodeToIndex(request[0]);
      long deliveryIndex = manager.nodeToIndex(request[1]);
      routing.addPickupAndDelivery(pickupIndex, deliveryIndex);
      solver.addConstraint(
          solver.makeEquality(routing.vehicleVar(pickupIndex), routing.vehicleVar(deliveryIndex)));
      solver.addConstraint(solver.makeLessOrEqual(
          distanceDimension.cumulVar(pickupIndex), distanceDimension.cumulVar(deliveryIndex)));
    }

    // Setting first solution heuristic.
    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION)
            .build();

    // Solve the problem and track duration.
    long startTime = System.nanoTime();
    Assignment solution = routing.solveWithParameters(searchParameters);
    long endTime = System.nanoTime();
    double durationSeconds = (endTime - startTime) / 1_000_000_000.0;

    // Print solution on console.
    printSolution(data, routing, manager, solution);

    // Save solution to JSON file and get total distance.
    String outputFile = "outputs/solutions/solution.json";
    long totalDistance = saveSolutionToFile(data, routing, manager, solution, outputFile);

    // Count total routes used.
    int totalRoutes = 0;
    for (int i = 0; i < data.vehicleNumber; ++i) {
      if (routing.isVehicleUsed(solution, i)) {
        totalRoutes++;
      }
    }

    // Save metrics to JSON file.
    String metricsFile = "outputs/metrics.json";
    double objectiveValue = solution.objectiveValue();
    saveMetricsToFile(objectiveValue, durationSeconds, totalDistance, totalRoutes, metricsFile);
  }
}

This is a short summary of the changes introduced for each of the examples:

  • Added a dependency for nextmv, the Python SDK for Nextmv.
  • Load the app manifest from the app.yaml file.
  • Extract options (configurations) from the manifest.
  • The input data is no longer in the Python file itself. We will move it to a file under inputs/input.json. In a single json file we will define the complete input. Given that we are working with the json content format, we use the Python SDK to load the input data from stdin.
  • Write the solution to the problem, and solver metrics, to stdout, given that we are working with the json content format.
  • Added parsing of command line arguments to extract options.
  • Added reading of input data from stdin, in json format.
  • Modified the model definition to use the loaded input data.
  • Store the solution to the problem, and solver metrics (statistics), in an output.
  • Write the output to stdout, given that we are working with the json content format.
  • Added parsing of command line arguments to extract options.
  • The input data is no longer in the Java file itself. We are representing the problem with several files under the inputs directory. In inputs/distance.json we are going to write the distance matrix. In inputs/pickups_deliveries.json we are going to set the information about the precedence of stops, defining pickup-delivery pairs. In inputs/problem.json we are storing additional information about the problem. When working with more than one file, the multi-file content format is ideal.
  • Modified the model definition to use the loaded input data.
  • Write the output to several files, under the outputs directory, given that we are working with the multi-file content format.

Here are the data files that you need to place in an inputs directory.

inputs/input.json
{
  "starting_grid": [
    [8, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 3, 6, 0, 0, 0, 0, 0],
    [0, 7, 0, 0, 9, 0, 2, 0, 0],
    [0, 5, 0, 0, 0, 7, 0, 0, 0],
    [0, 0, 0, 0, 4, 5, 7, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 3, 0],
    [0, 0, 1, 0, 0, 0, 0, 6, 8],
    [0, 0, 8, 5, 0, 0, 0, 1, 0],
    [0, 9, 0, 0, 0, 0, 4, 0, 0]
  ],
  "q": 3
}
inputs/problem.json
{
  "num_col": 2,
  "num_row": 3,
  "offset": 3,
  "col_cost": [1.0, 1.0],
  "col_lower": [0.0, 1.0],
  "col_upper": [4.0, 1.0e30],
  "row_lower": [-1.0e30, 5.0, 6.0],
  "row_upper": [7.0, 15.0, 1.0e30],
  "a_matrix": {
    "format": "colwise",
    "start": [0, 2, 5],
    "index": [1, 2, 0, 1, 2],
    "value": [1.0, 3.0, 1.0, 2.0, 2.0]
  },
  "integrality": [1, 1]
}
inputs/distance.json
[
  [
    0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468,
    776, 662
  ],
  [
    548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016,
    868, 1210
  ],
  [
    776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788,
    1552, 754
  ],
  [
    696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164,
    560, 1358
  ],
  [
    582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050,
    674, 1244
  ],
  [
    274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514,
    1050, 708
  ],
  [
    502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514,
    1278, 480
  ],
  [
    194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662,
    742, 856
  ],
  [
    308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320,
    1084, 514
  ],
  [
    194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274,
    810, 468
  ],
  [
    536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388,
    1152, 354
  ],
  [
    502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650,
    274, 844
  ],
  [
    388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536,
    388, 730
  ],
  [
    354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342,
    422, 536
  ],
  [
    468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0,
    764, 194
  ],
  [
    776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422,
    764, 0, 798
  ],
  [
    662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536,
    194, 798, 0
  ]
]
inputs/pickups_deliveries.json
[
  [1, 6],
  [2, 10],
  [4, 3],
  [5, 9],
  [7, 8],
  [15, 11],
  [13, 12],
  [16, 14]
]
inputs/problem.json
{
  "vehicleNumber": 4,
  "depot": 0
}

Aside from the app.yaml manifest file, the code changes, and the introduction of the inputs directory, we need to make some more minor adjustments.

4.1. Xpress

We need to add a dependency for nextmv (the Nextmv Python SDK). This dependency is optional, and the modeling constructs are not needed to run a Nextmv Application locally. However, using the SDK modeling features makes it easier to work with Nextmv apps, as a lot of convenient functionality is already baked in.

Run the following command to add the nextmv dependency to your project.

uv add nextmv

After you are done Nextmv-ifying, your Nextmv app should have the following structure, for the example provided:

.
├── app.yaml
├── inputs
   └── input.json
├── main.py
├── pyproject.toml
└── uv.lock

Now you are ready to explore the Nextmv Platform 🥳.

4.2. HiGHS

  • Given we introduced the nlohmann/json library for JSON parsing and writing, we need to get the library.

    mkdir -p nlohmann
    curl -L https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp \
      -o nlohmann/json.hpp
    
  • When we push the Nextmv application to Cloud, we need to cross-compile for Linux ARM64. To do this, we can use Docker. Add the following Dockerfile to the root of your Nextmv application.

    Dockerfile
    FROM ubuntu:22.04
    
    # Install build dependencies.
    RUN apt-get update && apt-get install -y \
        build-essential \
        cmake \
        git \
        curl \
        && rm -rf /var/lib/apt/lists/*
    
    # Set working directory.
    WORKDIR /app
    
    # Build HiGHS.
    RUN git clone https://github.com/ERGO-Code/HiGHS.git
    WORKDIR /app/HiGHS
    RUN cmake -S. -B build 
    RUN cmake --build build --parallel
    
    # Download nlohmann/json header.
    WORKDIR /app
    RUN mkdir -p nlohmann
    RUN curl -L https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp \
            -o nlohmann/json.hpp
    
    # Copy source code.
    COPY main.cpp /app/
    
    # Compile the application.
    RUN g++ -std=c++11 main.cpp -o main \
            -I. \
            -I./HiGHS/highs \
            -I./HiGHS/build \
            -L./HiGHS/build/lib -lhighs
    
  • In the app.yaml manifest, we specified a build.sh script that will run before pushing the app. This script will cross-compile the necessary dependencies and the binary so that it is ready to run in the Nextmv Platform.

    build.sh
    #!/bin/bash
    
    set -euo pipefail
    
    # Prepare Docker environment
    docker rm -f highs-solver || true
    
    # Build the Docker image
    docker buildx build -f Dockerfile -t highs-solver --platform linux/arm64 --load .
    
    # Extract the compiled binary from the container
    docker run --name highs-solver --platform linux/arm64 highs-solver
    docker cp highs-solver:/app/main ./main
    echo "🐰 Binary extracted to ./main"
    mkdir -p HiGHS/build
    docker cp highs-solver:/app/HiGHS/build/lib ./HiGHS/build
    echo "🐰 Required libraries extracted to ./HiGHS/build/lib"
    docker rm highs-solver
    echo "🐰 Build completed successfully."
    

After you are done Nextmv-ifying, your Nextmv app should have the following structure for this example.

.
├── app.yaml
├── build.sh
├── Dockerfile
├── HiGHS
   ├── ... More stuff
   └── build
       └── lib
           ├── libhighs.1.12.dylib
           ├── libhighs.1.dylib -> libhighs.1.12.dylib
           ├── libhighs.dylib -> libhighs.1.dylib
           ├── libhighs.so -> libhighs.so.1
           ├── libhighs.so.1 -> libhighs.so.1.12.0
           └── libhighs.so.1.12.0
├── inputs
   └── problem.json
├── main.cpp
├── nlohmann
   └── json.hpp
└── README.md

Now you are ready to explore the Nextmv Platform 🥳.

4.3. OR-Tools

  • Modify the pom.xml file to include the new dependencies.

    pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.google.ortools</groupId>
        <artifactId>vrp-pickup-delivery</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <name>VRP Pickup Delivery</name>
        <description>Vehicle Routing Problem with Pickup and Delivery using OR-Tools</description>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>11</maven.compiler.source>
            <maven.compiler.target>11</maven.compiler.target>
            <ortools.version>9.8.3296</ortools.version>
        </properties>
    
        <dependencies>
            <!-- OR-Tools dependency -->
            <dependency>
                <groupId>com.google.ortools</groupId>
                <artifactId>ortools-java</artifactId>
                <version>${ortools.version}</version>
            </dependency>
    
            <!-- Gson for JSON parsing -->
            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.10.1</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <!-- Maven Compiler Plugin -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>11</source>
                        <target>11</target>
                    </configuration>
                </plugin>
    
                <!-- Maven Exec Plugin for running the application and cleanup -->
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <mainClass>com.google.ortools.constraintsolver.samples.VrpPickupDelivery</mainClass>
                    </configuration>
                    <executions>
                        <execution>
                            <id>remove-original-jar</id>
                            <phase>package</phase>
                            <goals>
                                <goal>exec</goal>
                            </goals>
                            <configuration>
                                <executable>rm</executable>
                                <arguments>
                                    <argument>-f</argument>
                                    <argument>original-main.jar</argument>
                                </arguments>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <!-- Maven Shade Plugin for creating an executable JAR -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>3.5.1</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <createDependencyReducedPom>false</createDependencyReducedPom>
                                <shadedArtifactAttached>false</shadedArtifactAttached>
                                <outputDirectory>.</outputDirectory>
                                <finalName>main</finalName>
                                <shadedClassifierName>shaded</shadedClassifierName>
                                <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope>
                                <createSourcesJar>false</createSourcesJar>
                                <transformers>
                                    <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                        <mainClass>
                                            com.google.ortools.constraintsolver.samples.VrpPickupDelivery</mainClass>
                                    </transformer>
                                </transformers>
                                <filters>
                                    <filter>
                                        <artifact>*:*</artifact>
                                        <excludes>
                                            <exclude>META-INF/*.SF</exclude>
                                            <exclude>META-INF/*.DSA</exclude>
                                            <exclude>META-INF/*.RSA</exclude>
                                        </excludes>
                                    </filter>
                                </filters>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

After you are done Nextmv-ifying, your Nextmv app should have the following structure for this example.

.
├── app.yaml
├── inputs
   ├── distance.json
   ├── pickups_deliveries.json
   └── problem.json
├── main.jar
├── pom.xml
├── README.md
└── src
    └── main
        └── java
            └── com
                └── google
                    └── ortools
                        └── constraintsolver
                            └── samples
                                └── VrpPickupDelivery.java

Now you are ready to explore the Nextmv Platform 🥳.

5. Re-compile and run the executable code

Given that we made some modifications to the examples to Nextmv-ify them, we need to re-compile and run again to make sure they work.

5.1. Xpress

You can now read the input file from stdin. Execute the following command to run the code.

cat inputs/input.json | uv run main.py
FICO Xpress v9.9.1, Community, solve started 11:18:53, Jul 9, 2026
Heap usage: 755KB (peak 755KB, 58KB system)
Minimizing MILP noname using up to 10 threads and up to 32GB memory, with these control settings:
OUTPUTLOG = 1
TIMELIMIT = 1
NLPPOSTSOLVE = 1
XSLP_DELETIONCONTROL = 0
XSLP_OBJSENSE = 1
Original problem has:
      345 rows          729 cols         2937 elements       729 entities
Presolved problem has:
      141 rows          201 cols          595 elements       201 entities
LP relaxation tightened
Presolve finished in 0 seconds
Heap usage: 2355KB (peak 2355KB, 58KB system)

Coefficient range                    original                 solved
  Coefficients   [min,max] : [ 1.00e+00,  1.00e+00] / [ 1.00e+00,  1.00e+00]
  RHS and bounds [min,max] : [ 1.00e+00,  1.00e+00] / [ 1.00e+00,  1.00e+00]
  Objective      [min,max] : [      0.0,       0.0] / [      0.0,       0.0]
Autoscaling applied standard scaling

Will try to keep branch and bound tree memory usage below 30.3GB
Starting concurrent solve with dual (1 thread)

Concurrent-Solve,   0s
            Dual
    objective   dual inf

------- optimal --------
Concurrent statistics:
          Dual: 207 simplex iterations, 0.00s
Optimal solution found

  Its         Obj Value      S   Ninf  Nneg        Sum Inf  Time
  207           .000000      P      0     0        .000000     0
Dual solved problem
  207 simplex iterations in 0.00 seconds at time 0

Final objective                       : 0.000000000000000e+00
  Max primal violation      (abs/rel) :       0.0 /       0.0
  Max dual violation        (abs/rel) :       0.0 /       0.0
  Max complementarity viol. (abs/rel) :       0.0 /       0.0

Starting root cutting & heuristics
Deterministic mode with up to 4 additional threads

Its Type    BestSoln    BestBound   Sols    Add    Del     Gap     GInf   Time
P             .000000      .000000      1               0.0e+00        0      0
STOPPING - MIPRELSTOP target reached (MIPRELSTOP=0.0001  gap=0).
*** Search completed ***
Uncrunching matrix
Final MIP objective                   : 0.000000000000000e-01
Final MIP bound                       : 0.000000000000000e-01
  Solution time / primaldual integral :      0.03s/ 99.033924%
  Work / work units per second        :      0.02 /      0.80
  Number of solutions found / nodes   :         1 /         0
  Max primal violation      (abs/rel) :       0.0 /       0.0
  Max integer violation     (abs    ) :       0.0
Solution:
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
{
  "options": {
    "duration": 1
  },
  "solution": {
    "row_0": [8,1,2,7,5,3,6,4,9],
    "row_1": [9,4,3,6,8,2,1,7,5],
    "row_2": [6,7,5,4,9,1,2,8,3],
    "row_3": [1,5,4,2,3,7,8,9,6],
    "row_4": [3,6,9,8,4,5,7,2,1],
    "row_5": [2,8,7,1,6,9,5,3,4],
    "row_6": [5,2,1,9,7,4,3,6,8],
    "row_7": [4,3,8,5,2,6,9,1,7],
    "row_8": [7,9,6,3,1,8,4,5,2]
  },
  "assets": [],
  "metrics": {
    "duration": 0.04,
    "objective": 0.0,
    "num_variables": 729,
    "num_constraints": 345
  }
}

5.2. HiGHS

  • Compile the example, using the following command. Note that this command is different from the one already shown before.

    g++ -std=c++11 main.cpp -o main \
      -I. \
      -I./HiGHS/highs \
      -I./HiGHS/build \
      -L./HiGHS/build/lib -lhighs
    
  • A ./main executable file should have been created.

  • Run the example, reading the problem definition from stdin, and writing the output to stdout.

cat inputs/problem.json | DYLD_LIBRARY_PATH=./HiGHS/build/lib ./main
cat inputs/problem.json | LD_LIBRARY_PATH=./HiGHS/build/lib ./main
Model status: Optimal
Simplex iteration count: 2
Objective function value: 5.75
Primal  solution status: Feasible
Dual    solution status: Feasible
Basis: Valid
Column 0; value = 0.5; dual = 0; status: Basic
Column 1; value = 2.25; dual = 0; status: Basic
Row    0; value = 2.25; dual = -0; status: Basic
Row    1; value = 5; dual = 0.25; status: At lower/fixed bound
Row    2; value = 6; dual = 0.25; status: At lower/fixed bound
{
  "metrics": {
    "duration": 0.008574333,
    "objective_value": 6.0,
    "simplex_iteration_count": 0,
    "status": "Optimal"
  },
  "solution": {
    "columns": [
      {
        "index": 0,
        "value": -0.0
      },
      {
        "index": 1,
        "value": 3.0
      }
    ],
    "rows": [
      {
        "index": 0,
        "value": 3.0
      },
      {
        "index": 1,
        "value": 6.0
      },
      {
        "index": 2,
        "value": 6.0
      }
    ]
  }
}

5.3. OR-Tools

  • The commands for compiling and running stay the same.

  • Execute the following command to compile the code.

    mvn clean package
    
  • A ./main.jar file should have been created.

  • Execute the following command to run the code.

java -jar main.jar
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Objective : 226116
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 0:
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 13 -> 15 -> 11 -> 12 -> 0
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1552m
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 1:
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 5 -> 2 -> 10 -> 16 -> 14 -> 9 -> 0
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 2192m
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 2:
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 4 -> 3 -> 0
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1392m
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Route for Vehicle 3:
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: 0 -> 7 -> 1 -> 6 -> 8 -> 0
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Distance of the route: 1780m
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery printSolution
INFO: Total Distance of all routes: 6916m
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery saveSolutionToFile
INFO: Solution saved to outputs/solutions/solution.json
Jul 08, 2026 12:43:11 PM com.google.ortools.constraintsolver.samples.VrpPickupDelivery saveMetricsToFile
INFO: Metrics saved to outputs/metrics.json

6. Create an account

The full suite of benefits starts with a Nextmv Cloud account.

  1. Visit the Nextmv Console to sign up for an account at https://cloud.nextmv.io.
  2. Fill out the form. A member of the Nextmv team will reach out to you to complete the sign-up process.
  3. Log in to your account. The Nextmv Console is ready to use!

Once you have logged in to your account, you need to fetch your API key. You can do so from your settings.

API keys

When you have your API key, it is convenient to save it as an environment variable so that you can use it for the rest of this tutorial.

export NEXTMV_API_KEY="<YOUR-API-KEY>"
$env:NEXTMV_API_KEY = "<YOUR-API-KEY>"

7. Install the Nextmv CLI

Please see the Nextmv CLI installation guide.

8. Create your Nextmv Cloud application

At the root of your local project (where the app.yaml manifest is located), run the following command:

nextmv cloud app create -a test-xpress --exist-ok
nextmv cloud app create -a test-highs --exist-ok
nextmv cloud app create -a test-ortools --exist-ok
 Creating or getting application...
{
  "id": "test-ortools",
  "name": "test-ortools",
  "description": "",
  "type": "custom",
  "default_instance": "latest",
  "default_experiment_instance": "",
  "subscription_id": "",
  "locked": false,
  "created_at": "2025-12-01T23:00:46.289233Z",
  "updated_at": "2026-07-08T17:01:51.797601Z"
}

This will create a new application in Nextmv Cloud. Note that the name and app ID can be different, but for simplicity this tutorial uses the same name and app ID. This command is saved as app1.sh in the full tutorial code. You can also create applications directly from Nextmv Console.

You can go to the Apps section in the Nextmv Console where you will see your applications.

Apps

9. Push your Nextmv application

So far, your application has run locally. You are going to push your app to Nextmv Cloud. Once an application has been pushed, you can run it remotely, perform testing, experimentation, and much more. Pushing is the equivalent of deploying an application, this is, taking the executable code and sending it to Nextmv Cloud.

Deploy your app (push it) to Nextmv Cloud:

nextmv cloud app push -a test-xpress
nextmv cloud app push -a test-highs
nextmv cloud app push -a test-ortools

This command is saved as app2.sh in the full tutorial code.

You can go to the Apps section in the Nextmv Console where you will see your application. You can click on it to see more details. Once you are in the overview of the application in the Nextmv Console, it should show the following:

Pushed app

  • There is now a pushed executable.
  • There is an auto-created latest instance, assigned to the executable.

An instance is like the endpoint of the application.

10. Run the Nextmv application remotely

To run the Nextmv application remotely, you have several options. For this tutorial, we will be using the Nextmv Console and CLI.

For Xpress and OR-Tools, there are no special requirements to run. For HiGHS, however, you need to specify the LD_LIBRARY_PATH environment variable, so that the application can find the required shared libraries. To achieve this, you can use secrets collections, which allow you to specify special files or environment variables to be used when running your application.

Run the following command to create a secrets collection for HiGHS:

nextmv cloud secrets create -a test-highs \
    --secrets '{"type": "env", "location": "LD_LIBRARY_PATH", "value": "./HiGHS/build/lib"}'
 Creating secrets collection...
{
  "id": "secrets-tr6fz6wi",
  "application_id": "test-highs",
  "name": "secrets-tr6fz6wi",
  "description": "",
  "created_at": "2026-07-09T16:45:40Z",
  "updated_at": "2026-07-09T16:45:40Z"
}

This command is saved as app3.sh in the full tutorial code for the highs/nextmv-ified example.

Now you are ready to start a new run. In the Nextmv Console, in the app overview page:

  1. Press the New run button.
  2. Drop the data files that you want to use. You will get a preview of the data.
    • For Xpress, use the input.json file.
    • For HiGHS, use the problem.json file.
    • For OR-Tools, use the distance.json, pickups_deliveries.json, and problem.json files.
  3. Configure your run according to the options that are set in the app.yaml manifest.
    • For Xpress, you can configure the duration.
    • For HiGHS, you can configure the duration.
    • For OR-Tools, you can configure the vehicle_maximum_travel_distance and the global_span_cost_coefficient.
  4. Configure the run settings.
    • For HiGHS, select the secrets collection you created in the previous step.
    • For Xpress and OR-Tools there is no special configuration needed.
  5. Start the run.

New run Xpress

New run HiGHS CLI

New run OR-Tools CLI

You can use the Nextmv Console to browse the information of the run:

  • Summary
  • Output
  • Input
  • Metadata
  • Logs

Nextmv is built for collaboration, so you can invite team members to your account and share run URLs.

Run summary Run metadata

Alternatively, you can run your Nextmv application using the Nextmv CLI. Here is an example command for the applications.

nextmv cloud run create -a test-xpress -i inputs/input.json -o "duration=5"
nextmv cloud run create -a test-highs -i inputs/problem.json -o "duration=1" \
    -s "<SECRETS_COLLECTION_ID_CREATED_PREVIOUSLY>"
nextmv cloud run create -a test-ortools -i inputs -o "vehicle_maximum_travel_distance=3000,global_span_cost_coefficient=100"
{
  "run_id": "devint-jjk3M1Zvg"
}

This command is saved in the full tutorial code as:

  • app3.sh for Xpress and OR-Tools.
  • app4.sh for HiGHS.

11. Perform a scenario test

We are going to take full advantage of the Nextmv Platform by creating a scenario test. Scenario tests are generally used as an exploratory test to understand the impacts to business metrics (or KPIs) on situations such as:

  • Updating a model with a new feature, such as an additional constraint.
  • Comparing how the same model performs in different conditions, such as low demand vs. high demand.
  • Doing a sensitivity analysis to understand how the model behaves when changing a parameter.

Start by creating an input set. As the name suggests, it is a set of inputs, and it serves as a base so that we can perform runs varying one or more configurations (options). To create an input set, you have several options. For this tutorial, we will be using the Nextmv Console and CLI. You may follow these steps for all examples.

  1. Navigate to the Input sets section.
  2. Set a name for your input set.
  3. Use the Instance + date range creation type given that we already have a few runs on the latest instance.
  4. Create the input set.

Input set

Another option for creating the input set is using the Nextmv CLI. Here is an example command for the applications.

nextmv cloud input-set create -a test-xpress -i latest
nextmv cloud input-set create -a test-highs -i latest
nextmv cloud input-set create -a test-ortools -i latest
{
  "id": "input-set-2",
  "name": "Input set 2",
  "description": "",
  "app_id": "test-highs",
  "created_at": "2025-12-02T15:24:35.980849238Z",
  "updated_at": "2025-12-02T15:24:35.980849238Z",
  "input_ids": [
    "latest-BnZOZ1ZDg",
    "latest-5qrIm1ZDg"
  ],
  "inputs": []
}

This command is saved in the full tutorial code as:

  • app4.sh for Xpress and OR-Tools.
  • app5.sh for HiGHS.

Before creating the scenario test, we need to make sure that the secrets (the environment variable) are available for the HiGHS app. To do this, we are going to update the latest instance to use the secrets collection we created before.

Run the following command to update the latest instance for HiGHS:

nextmv cloud instance update -a test-highs -i latest -s "<SECRETS_COLLECTION_ID_CREATED_PREVIOUSLY>"
 Updating instance...
 Instance latest updated successfully in application test-highs.
{
  "id": "latest",
  "application_id": "test-highs",
  "version_id": "",
  "name": "Latest",
  "description": "Auto-created instance to manage the latest pushed executable binary.",
  "configuration": {
    "execution_class": "6c9500mb870s",
    "secrets_collection_id": "secrets-tr6fz6wi",
    "queuing": {
      "priority": 6,
      "disabled": false
    }
  },
  "locked": false,
  "created_at": "2025-12-01T23:02:31.269239Z",
  "updated_at": "2026-07-10T14:32:18.043814Z"
}

This command is saved as app6.sh in the full tutorial code for the highs/nextmv-ified example.

Once your input set has been created, and the latest instance has been updated, we are going to create a scenario test. Similarly to runs and input sets, you may use the Console or CLI, amongst other options. We will continue to use both in this tutorial. You may follow these steps for all examples.

  1. Navigate to the Scenario section.
  2. Set a name for your scenario test.
  3. Select the input set you just created in the previous step.
  4. Select the latest instance.
  5. Create configuration combinations, which will be factored in to create the scenarios.
    • For Xpress, we are setting duration to be 1, 3, and 5 seconds.
    • For HiGHS, we are setting duration to be 1, 3, and 5 seconds.
    • For OR-Tools, we are setting vehicle_maximum_travel_distance to be 3000, 2500, and 2000; and global_span_cost_coefficient to be 100, and 1000.
  6. Optionally, you may configure repetitions. These are useful when the results are not deterministic.
  7. Create the scenario test. Review and confirm the number of scenarios that will be created.

Scenario test Xpress & HiGHS

Scenario test OR-Tools

Once all the runs in the scenario test are completed, you can visualize the result of the test. A pivot table is provided to create useful comparisons of your metrics across the scenario test runs.

Scenario test result

Another option for creating the scenario test is using the Nextmv CLI. Here is an example command for the applications.

SCENARIO='{
    "instance_id": "latest",
    "scenario_input": {
        "scenario_input_type": "input_set",
        "scenario_input_data": "<INPUT_SET_ID_CREATED_PREVIOUSLY>"
    },
    "configuration": [
        {
            "name": "duration",
            "values": ["1", "2", "3"]
        }
    ]
}'
nextmv cloud scenario create -a test-xpress --scenarios "$SCENARIO"
nextmv cloud instance update -a test-highs -i latest -s "<SECRETS_COLLECTION_ID_CREATED_PREVIOUSLY>"
SCENARIO='{
    "instance_id": "latest",
    "scenario_input": {
        "scenario_input_type": "input_set",
        "scenario_input_data": "<INPUT_SET_ID_CREATED_PREVIOUSLY>"
    },
    "configuration": [
        {
            "name": "vehicle_maximum_travel_distance",
            "values": ["3000", "2500", "2000"]
        },
        {
            "name": "global_span_cost_coefficient",
            "values": ["100", "1000"]
        }
    ]
}'
nextmv cloud scenario create -a test-ortools --scenarios "$SCENARIO"
{
  "scenario_test_id": "scenario-nqw5qzab"
}

This command is saved in the full tutorial code as:

  • app5.sh for Xpress and OR-Tools.
  • app6.sh for HiGHS.

🎉🎉🎉 Congratulations, you have finished this tutorial!

Full tutorial code

You can find the consolidated code examples used in this tutorial in the tutorials GitHub repository. The connect-your-model-cli dir contains all the code that was shown in this tutorial.

For each of the examples, you will find two directories:

  • original: the original example without any modifications.
  • nextmv-ified: the example converted into a Nextmv application.

Go into each directory for instructions about running the decision model.