Order Batching Problem

Routing

Problem

The Order Batching Problem arises in warehouse logistics. In this context, a set of orders, each composed of articles located at specific positions in a rectangular warehouse with parallel aisles, must be grouped into batches. For each batch, a picker collects all items in a single tour through the warehouse, starting and ending at the depot.

A key constraint is the picker’s limited carrying capacity : each batch can only include a certain number of items. The distance traveled to complete a batch is determined using the S-shape routing policy, where the picker moves along the full length of each aisle, alternating direction with each pass (front-to-back, then back-to-front).

The overall goal is to organize the orders into batches in a way that minimizes the total distance traveled by the picker.

Principles learned

Data

The instances of the Order Batching Problem are in JSON format.

Each file contains the following fields:

  • “instanceName”: Name of the instance
  • “nbOrders”: Total number of customer orders
  • “capacity”: Maximum number of articles allowed per batch
  • “nbAisles”: Number of physical aisles in the warehouse
  • “maxNbBatches”: Upper bound on the number of batches
  • “aisleTraversal”: Length of each aisle (distance units)
  • “gap”: Distance between the center lines of two adjacent aisles
  • “depotOffset”: Distance from the depot to the first aisle
  • “orders”: This field contains a list of customer orders. Each order is defined by:
    • “nbItems” : Number of articles in the order
    • “aislesToVisit”: Indices of the aisles that must be visited to collect the order

We rely on standard JSON libraries to read instance data and write solution outputs for each API model:

  • C#: Newtonsoft.Json
  • Java: gson 2.8.8
  • Python: json
  • C++: nlohmann/json.hpp

For the Hexaly Modeler, the built‑in JSON module is used.

Model

The Hexaly model for the Order Batching Problem uses set decision variables, with each set representing the orders assigned to a batch.

First, these sets are constrained to form a partition, meaning each order must be assigned to exactly one batch.

We compute the total number of items in a batch using a variadic sum operator over the set and a lambda function that returns the number of items in each order. Note that the number of terms in this sum varies during the search, along with the size of the set. We can then constrain the total number of items to be lower than the picker’s capacity.

Then, for each batch, we determine which aisles must be visited.  An aisle is visited if at least one order in the batch has items stored in that aisle. Based on this, we compute the number of aisles visited and the index of the farthest aisle visited thanks to another lambda function.

Then, the S-shape traversal distance for a batch is computed as follows:  the picker starts at the depot, first travels horizontally to reach the farthest visited aisle (distance proportional to gap x maxVisitedAisle), then traverses the full length of every visited aisle (adjusting for parity so the picker always exits on the front side), and finally returns to the depot (see below). In the special case where the batch is empty, its distance is zero.

Overall, the objective is to minimize the total distance traveled for all batches.

Execution
hexaly order_batching_problem.hxm inFileName=instances/15s-50-40-0.json [hxTimeLimit=] [solFileName=]
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
use io;
use json;

// Read input
function input() {
    local usage = "Usage: hexaly order_batching_problem.hxm "
            + "inFileName=inputFile [solFileName=outputFile] [hxTimeLimit=timeLimit]";

    if (inFileName == nil) throw usage;
    local data = json.parse(inFileName);

    nbOrders = data["nbOrders"];                // Number of orders
    capacity = data["capacity"];                // Capacity of the picker
    nbAisles = data["nbAisles"];                // Number of aisles
    maxNbBatches = data["maxNbBatches"];        // Upper bound on the number of batches
    aisleTraversal = data["aisleTraversal"];    // Length of each aisle
    depotOffset = data["depotOffset"];          // Distance from the depot to the first aisle
    gap = data["gap"];           // Distance between the center lines and two adjacent aisles

    // Aisles visited by each order
    local orders = data["orders"];
    for [i in 0...nbOrders] {
        nbItems[i] = orders[i]["nbItems"];      // Number of items in each order
        for [a in 0...nbAisles] {
            visitsAisle[a][i] = 0;
        }
        local aisles = orders[i]["aislesToVisit"];
        for [j in 0...aisles.count()] {
            visitsAisle[aisles[j]][i] = 1;
        }
    }
}

// Declare the optimization model
function model() {
    // Set decision: batch[k] contains orders assigned to batch k
    batch[k in 0...maxNbBatches] <- set(nbOrders);

    // Partition constraint: each order is assigned to exactly one batch
    constraint partition[k in 0...maxNbBatches](batch[k]);

    for [k in 0...maxNbBatches] {
        // Capacity constraint: total number of items of orders assigned to batch k is limited
        local batchItems <- sum(batch[k], i => nbItems[i]);
        constraint batchItems <= capacity;

        // Expressions for objective function
        // Batch size
        batchCount[k] <- count(batch[k]);
        // Check which aisles are visited by current batch k
        for [a in 0...nbAisles] {
            visited[k][a] <- sum(batch[k], i => visitsAisle[a][i]) >= 1;
        }

        // Number of visited aisles
        nbVisited[k] <- sum[a in 0...nbAisles](visited[k][a]);
        // Farthest visited aisle
        maxVisited[k] <- max[a in 0...nbAisles](visited[k][a] * a);

        // S-shape traversal distance (full traversal of every visited aisle)
        distance[k] <- (batchCount[k] > 0) * (
                2 * depotOffset
                + 2 * gap * maxVisited[k]
                + (nbVisited[k] + nbVisited[k] % 2) * aisleTraversal
        );
    }

    // Minimize total distance
    totalDistance <- sum[k in 0...maxNbBatches](distance[k]);
    minimize totalDistance;
}

// Parameterize the solver
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 20;
}

// Write the solution in a JSON file
//   - Objective function
//   - Content of each batch
//       | Computed orders
//       | Travelled Distance
//       | Visited Aisles
function output() {
    if (hxSolution.status == "OPTIMAL" || hxSolution.status == "FEASIBLE") {
        if (solFileName == nil) return;
        local solFile = io.openWrite(solFileName);
        solFile.println("{");
        solFile.println("  \"objective\": " + totalDistance.value + ",");
        solFile.println("  \"batches\": [");
        local first = true;
        for [k in 0...maxNbBatches] {
            local c = batchCount[k].value;
            if (c > 0) {
                if (!first) solFile.println(",");
                first = false;
                local aisleStr = "";
                local firstAisle = true;
                for [a in 0...nbAisles] {
                    if (visited[k][a].value == 1) {
                        if (!firstAisle) aisleStr += ",";
                        firstAisle = false;
                        aisleStr += a;
                    }
                }
                solFile.print("    {\"orders\": [");
                for [j in 0...c] {
                    if (j > 0) solFile.print(", ");
                    solFile.print(batch[k].value[j]);
                }
                solFile.print("], \"distance\": " + distance[k].value);
                solFile.print(", \"visitedAisles\": [" + aisleStr + "]}");
            }
        }
        solFile.println("");
        solFile.println("  ]");
        solFile.println("}");
        solFile.close();
        println("Solution written in file ", solFileName);
    } else {
        println("No feasible solution have been found within the allowed time.");
    }
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python order_batching_problem.py instances\15s-50-40-0.json
 
Execution (Linux)
export PYTHONPATH=/opt/hexaly_15_0/bin/python
python order_batching_problem.py instances/15s-50-40-0.json
 
# Copyright (c) Hexaly. Permission is hereby granted to use, copy,
# and modify this code for applications developed with Hexaly.
import hexaly.optimizer
import json
import sys

def read_instance(input_file):
    with open(input_file) as f:
        data = json.load(f)

    nb_orders = data["nbOrders"]                # Number of orders
    capacity = data["capacity"]                 # Capacity of the picker
    nb_aisles = data["nbAisles"]                # Number of aisles
    max_nb_batches = data["maxNbBatches"]       # Upper bound on the number of batches
    aisle_traversal = data["aisleTraversal"]    # Length of each aisle
    depot_offset = data["depotOffset"]          # Distance from the depot to the first aisle
    gap = data["gap"]            # Distance between the center lines and two adjacent aisles

    nb_items = [0] * nb_orders                                  # Number of items in each order
    visits_aisle = [[0] * nb_orders for _ in range(nb_aisles)]  # Aisles visited by each order
    orders = data["orders"]
    for i in range(nb_orders):
        nb_items[i] = orders[i]["nbItems"]
        for a in orders[i]["aislesToVisit"]:
            visits_aisle[a][i] = 1

    return nb_orders, capacity, nb_aisles, max_nb_batches, aisle_traversal, gap, \
            depot_offset, nb_items, visits_aisle

# Create and solve the model
def main(input_file, output_file, time_limit):
    # Read instance data
    nb_orders, capacity, nb_aisles, max_nb_batches, aisle_traversal, gap, \
            depot_offset, nb_items, visits_aisle = read_instance(input_file)

    with hexaly.optimizer.HexalyOptimizer() as optimizer:
        # Declare the optimization model
        model = optimizer.model

        # Set decision: batch[k] contains orders assigned to batch k
        batch = [model.set(nb_orders) for _ in range(max_nb_batches)]

        # Partition constraint: each order is assigned to exactly one batch
        model.constraint(model.partition(batch))

        # Initialize batch properties
        batch_count = [None] * max_nb_batches
        distance = [None] * max_nb_batches
        visited = [[None] * nb_aisles for _ in range(max_nb_batches)]

        # Create arrays for aisle visits
        visits_aisle_array = [model.array(visits_aisle[a]) for a in range(nb_aisles)]

        # Create lambda function for nbItems (reused across batches)
        nb_items_array = model.array(nb_items)
        nb_items_lambda = model.lambda_function(lambda i: nb_items_array[i])

        for k in range(max_nb_batches):
            # Capacity constraint: total number of items of orders assigned to batch k is limited
            batch_items = model.sum(batch[k], nb_items_lambda)
            model.constraint(batch_items <= capacity)

            # Expressions for objective function
            # Batch size
            batch_count[k] = model.count(batch[k])
            # Check which aisles are visited by current batch k
            for a in range(nb_aisles):
                arr = visits_aisle_array[a]
                visited[k][a] = model.sum(batch[k], model.lambda_function(lambda i: arr[i])) >= 1

            # Number of visited aisles
            nb_visited = model.sum(visited[k])
            # Farthest visited aisle
            max_visited = model.max(visited[k][a] * a for a in range(nb_aisles))

            # S-shape traversal distance (full traversal of every visited aisle)
            distance[k] = (batch_count[k] > 0) * (
                    2 * depot_offset
                    + 2 * gap * max_visited
                    + (nb_visited + nb_visited % 2) * aisle_traversal
            )

        # Minimize total distance
        total_distance = model.sum(distance)

        model.minimize(total_distance)
        model.close()

        # Parameterize the solver
        optimizer.param.time_limit = time_limit
        optimizer.solve()

        # Write the solution in a JSON file
        #   - Objective function
        #   - Content of each batch
        #       | Computed orders
        #       | Travelled Distance
        #       | Visited Aisles
        feasible_list = ["HxSolutionStatus.FEASIBLE", "HxSolutionStatus.OPTIMAL"]
        if (str(optimizer.solution.status) in feasible_list):
            if output_file is not None:
                sol = {"objective": int(total_distance.value), "batches": []}
                for k in range(max_nb_batches):
                    c = batch_count[k].value
                    if c > 0:
                        sol["batches"].append({
                            "orders": list(batch[k].value),
                            "distance": int(distance[k].value),
                            "visitedAisles": [
                                a for a in range(nb_aisles)
                                if visited[k][a].value == 1
                            ],
                        })
                with open(output_file, "w") as f:
                    json.dump(sol, f, indent = 2)
                    f.write("\n")
                print("Solution written in file ", output_file)
        else:
            print("No feasible solution have been found within the allowed time.")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python order_batching_problem.py inputFile [outputFile] [timeLimit]")
        sys.exit(1)

    input_file = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) >= 3 else None
    time_limit = int(sys.argv[3]) if len(sys.argv) >= 4 else 20
    main(input_file, output_file, time_limit)
Compilation / Execution (Windows)
cl /EHsc order_batching_problem.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly150.lib
order_batching_problem instances\15s-50-40-0.json
 
 
Compilation / Execution (Linux)
g++ order_batching_problem.cpp -I/opt/hexaly_15_0/include -lhexaly150 -lpthread -o order_batching_problem
./order_batching_problem instances/15s-50-40-0.json
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
#include "optimizer/hexalyoptimizer.h"
#include "json.hpp"
#include <fstream>
#include <iostream>
#include <vector>

using namespace hexaly;
using namespace std;
using json = nlohmann::json;

class OrderBatchingProblem {
public:
    // Instance data
    int nbOrders;                       // Number of orders
    int capacity;                       // Capacity of the picker
    int nbAisles;                       // Number of aisles
    int maxNbBatches;                   // Upper bound on the number of batches
    double aisleTraversal;              // Length of each aisle
    double gap;                         // Distance between the center lines and two adjacent aisles
    double depotOffset;                 // Distance from the depot to the first aisle
    vector<int> nbItems;                // Number of items in each order
    vector<vector<int>> visitsAisle;    // Aisles visited by each order

    // Hexaly Optimizer
    HexalyOptimizer optimizer;
    // Decision variables
    vector<HxExpression> batch;

    // Size of each batch
    vector<HxExpression> batchCount;
    // Aisles visited by each batch
    vector<vector<HxExpression>> visited;
    // Distance travelled by each batch
    vector<HxExpression> distance;

    // Objective function : total travelled distance
    HxExpression totalDistance;

    // Read instance data
    void readInstance(const string& fileName) {
        std::ifstream infile(fileName);
        if (!infile.is_open()) {
            throw std::runtime_error("Cannot open file: " + fileName);
        }
        json data = json::parse(infile);
        infile.close();

        nbOrders = data["nbOrders"];
        capacity = data["capacity"];
        nbAisles = data["nbAisles"];
        maxNbBatches = data["maxNbBatches"];
        aisleTraversal = data["aisleTraversal"];
        gap = data["gap"];
        depotOffset = data["depotOffset"];

        nbItems.resize(nbOrders);
        visitsAisle.resize(nbAisles, vector<int>(nbOrders, 0));

        for (int i = 0; i < nbOrders; ++i) {
            nbItems[i] = data["orders"][i]["nbItems"];
            for (int a : data["orders"][i]["aislesToVisit"]) {
                visitsAisle[a][i] = 1;
            }
        }
    }

    // Create and solve the model
    void solve(int timeLimit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Set decision: batch[k] contains orders assigned to batch k
        batch.resize(maxNbBatches);
        for (int k = 0; k < maxNbBatches; ++k) {
            batch[k] = model.setVar(nbOrders);
        }
        // Partition constraint: each order is assigned to exactly one batch
        model.constraint(model.partition(batch.begin(), batch.end()));

        // Initialize batch properties
        batchCount.resize(maxNbBatches);
        distance.resize(maxNbBatches);
        visited.resize(maxNbBatches);

        // Create arrays for aisle visits
        vector<HxExpression> visitsAisleArray(nbAisles);
        for (int a = 0; a < nbAisles; ++a) {
            visitsAisleArray[a] = model.array(visitsAisle[a].begin(), visitsAisle[a].end());
        }
        // Create lambda function for nbItems (reused across batches)
        HxExpression nbItemsArray = model.array(nbItems.begin(), nbItems.end());
        HxExpression nbItemsLambda = model.createLambdaFunction(
                [&](HxExpression i) { return model.at(nbItemsArray, i); });

        for (int k = 0; k < maxNbBatches; ++k) {
            // Capacity constraint: total number of items of orders assigned to batch k is limited
            HxExpression batchItems = model.sum(batch[k], nbItemsLambda);
            model.constraint(batchItems <= capacity);

            // Expressions for objective function
            // Batch size
            batchCount[k] = model.count(batch[k]);
            // Check which aisles are visited by current batch k
            visited[k].resize(nbAisles);
            for (int a = 0; a < nbAisles; ++a) {
                HxExpression visitedLambda = model.createLambdaFunction(
                        [&](HxExpression i) { return model.at(visitsAisleArray[a], i); });
                visited[k][a] = model.sum(batch[k], visitedLambda) >= 1;
            }

            // Number of visited aisles
            HxExpression nbVisited = model.sum();
            for (int a = 0; a < nbAisles; ++a) {
                nbVisited.addOperand(visited[k][a]);
            }
            // Farthest visited aisle
            HxExpression maxVisited = model.max();
            for (int a = 0; a < nbAisles; ++a) {
                maxVisited.addOperand(visited[k][a] * a);
            }
            // S-shape traversal distance (full traversal of every visited aisle)
            distance[k] = (batchCount[k] > 0) * (
                    2 * depotOffset
                    + 2 * gap * maxVisited
                    + (nbVisited + nbVisited % 2) * aisleTraversal);
        }

        // Minimize total distance
        totalDistance = model.sum();
        for (int k = 0; k < maxNbBatches; ++k) {
            totalDistance.addOperand(distance[k]);
        }
        model.minimize(totalDistance);
        model.close();

        // Parameterize the solver
        optimizer.getParam().setTimeLimit(timeLimit);
        optimizer.solve();
    }

    // Write the solution in a JSON file
    //   - Objective function
    //   - Content of each batch
    //       | Computed orders
    //       | Travelled Distance
    //       | Visited Aisles
    void writeSolution(const string& fileName) {
        if (optimizer.getSolution().getStatus() == 2
                || optimizer.getSolution().getStatus() == 3) {
            json sol;
            sol["objective"] = (long long) totalDistance.getDoubleValue();
            sol["batches"] = json::array();
            for (int k = 0; k < maxNbBatches; ++k) {
                int c = (int) batchCount[k].getValue();
                if (c > 0) {
                    json b;
                    HxCollection batchValue = batch[k].getCollectionValue();
                    b["orders"] = json::array();
                    for (int j = 0; j < c; ++j)
                        b["orders"].push_back(batchValue[j]);
                    b["distance"] = (long long) distance[k].getDoubleValue();
                    b["visitedAisles"] = json::array();
                    for (int a = 0; a < nbAisles; ++a)
                        if (visited[k][a].getValue() == 1)
                            b["visitedAisles"].push_back(a);
                    sol["batches"].push_back(b);
                }
            }
            ofstream outfile(fileName);
            outfile << sol.dump(2) << endl;
            printf("Solution written in file %s\n\n", fileName.c_str());
        } else {
            printf("No feasible solution have been found within the allowed time.\n");
        }
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "Usage: order_batching_problem inputFile [outputFile] [timeLimit]" << endl;
        return 1;
    }
    const char* instanceFile = argv[1];
    const char* solFile = argc > 2 ? argv[2] : NULL;
    const char* strTimeLimit = argc > 3 ? argv[3] : "20";

    try {
        OrderBatchingProblem model;
        model.readInstance(instanceFile);
        model.solve(atoi(strTimeLimit));

        if (solFile != NULL) {
            model.writeSolution(solFile);
        }

        return 0;
    } catch (const exception& e) {
        cerr << "An error occurred: " << e.what() << endl;
        return 1;
    }
}
Compilation / Execution (Windows)
copy %HX_HOME%\bin\Hexaly.NET.dll .
csc OrderBatchingProblem.cs /reference:Hexaly.NET.dll
OrderBatchingProblem instances\15s-50-40-0.json
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
using System;
using System.Collections.Generic;
using System.IO;
using Hexaly.Optimizer;
using Newtonsoft.Json.Linq;

public class OrderBatchingProblem : IDisposable
{
    // Instance data
    int nbOrders;           // Number of orders
    int capacity;           // Capacity of the picker
    int nbAisles;           // Number of aisles
    int maxNbBatches;       // Upper bound on the number of batches
    double aisleTraversal;  // Length of each aisle
    double gap;             // Distance between the center lines and two adjacent aisles
    double depotOffset;     // Distance from the depot to the first aisle
    int[] nbItems;          // Number of items in each order
    int[][] visitsAisle;    // Aisles visited by each order

    // Hexaly Optimizer
    HexalyOptimizer optimizer;
    HxExpression[] batch;           // Decision variables
    HxExpression[] batchCount;      // Size of each batch
    HxExpression[][] visited;       // Aisles visited by each batch
    HxExpression[] distance;        // Distance travelled by each batch

    // Objective function : total travelled distance
    HxExpression totalDistance;

    public OrderBatchingProblem()
    {
        optimizer = new HexalyOptimizer();
    }

    public void Dispose()
    {
        if (optimizer != null) optimizer.Dispose();
    }

    // Read instance data
    void ReadInstance(string fileName)
    {
        JObject data = JObject.Parse(File.ReadAllText(fileName));

        nbOrders = data["nbOrders"].Value<int>();
        capacity = data["capacity"].Value<int>();
        nbAisles = data["nbAisles"].Value<int>();
        maxNbBatches = data["maxNbBatches"].Value<int>();
        aisleTraversal = data["aisleTraversal"].Value<double>();
        gap = data["gap"].Value<double>();
        depotOffset = data["depotOffset"].Value<double>();

        nbItems = new int[nbOrders];
        visitsAisle = new int[nbAisles][];
        for (int a = 0; a < nbAisles; ++a)
        {
            visitsAisle[a] = new int[nbOrders];
        }
        JArray orders = (JArray)data["orders"];
        for (int i = 0; i < nbOrders; ++i)
        {
            nbItems[i] = orders[i]["nbItems"].Value<int>();
            JArray aisles = (JArray)orders[i]["aislesToVisit"];
            foreach (var a in aisles)
            {
                visitsAisle[a.Value<int>()][i] = 1;
            }
        }
    }

    // Create and solve the model
    void Solve(int limit)
    {
        // Declare the optimization model
        HxModel model = optimizer.GetModel();

        // Set decision: batch[k] contains orders assigned to batch k
        batch = new HxExpression[maxNbBatches];
        for (int k = 0; k < maxNbBatches; ++k)
        {
            batch[k] = model.Set(nbOrders);
        }
        // Partition constraint: each order is assigned to exactly one batch
        model.Constraint(model.Partition(batch));

        // Initialize batch properties
        batchCount = new HxExpression[maxNbBatches];
        distance = new HxExpression[maxNbBatches];
        visited = new HxExpression[maxNbBatches][];

        // Create arrays for aisle visits
        HxExpression[] visitsAisleArray = new HxExpression[nbAisles];
        for (int a = 0; a < nbAisles; ++a)
        {
            long[] col = new long[nbOrders];
            for (int i = 0; i < nbOrders; ++i)
            {
                col[i] = visitsAisle[a][i];
            }
            visitsAisleArray[a] = model.Array(col);
        }

        // Create lambda function for nbItems (reused across batches)
        long[] nbItemsLong = new long[nbOrders];
        for (int i = 0; i < nbOrders; ++i)
        {
            nbItemsLong[i] = nbItems[i];
        }
        HxExpression nbItemsArray = model.Array(nbItemsLong);
        HxExpression nbItemsLambda = model.LambdaFunction(i => nbItemsArray[i]);

        for (int k = 0; k < maxNbBatches; ++k)
        {
            // Capacity constraint: total number of items of orders assigned to batch k is limited
            HxExpression batchItems = model.Sum(batch[k], nbItemsLambda);
            model.Constraint(batchItems <= capacity);

            // Expressions for objective function
            // Batch size
            batchCount[k] = model.Count(batch[k]);
            // Check which aisles are visited by current batch k
            visited[k] = new HxExpression[nbAisles];
            for (int a = 0; a < nbAisles; ++a)
            {
                int localA = a;
                HxExpression visitedLambda = model.LambdaFunction(i => visitsAisleArray[localA][i]);
                visited[k][a] = model.Sum(batch[k], visitedLambda) >= 1;
            }

            // Number of visited aisles
            HxExpression nbVisited = model.Sum();
            for (int a = 0; a < nbAisles; ++a)
            {
                nbVisited.AddOperand(visited[k][a]);
            }
            // Farthest visited aisle
            HxExpression maxVisited = model.Max();
            for (int a = 0; a < nbAisles; ++a)
            {
                maxVisited.AddOperand(visited[k][a] * a);
            }
            // S-shape traversal distance (full traversal of every visited aisle)
            distance[k] = (batchCount[k] > 0) * (
                    2 * depotOffset
                    + 2 * gap * maxVisited
                    + (nbVisited + nbVisited % 2) * aisleTraversal);
        }

        // Minimize total distance
        totalDistance = model.Sum();
        for (int k = 0; k < maxNbBatches; ++k)
        {
            totalDistance.AddOperand(distance[k]);
        }
        model.Minimize(totalDistance);
        model.Close();

        // Parametrize the solver
        optimizer.GetParam().SetTimeLimit(limit);
        optimizer.Solve();
    }

    // Write the solution in a JSON file
    //   - Objective function
    //   - Content of each batch
    //       | Computed orders
    //       | Travelled Distance
    //       | Visited Aisles
    void WriteSolution(string fileName)
    {
        if (optimizer.GetSolution().GetStatus() == HxSolutionStatus.Feasible
                || optimizer.GetSolution().GetStatus() == HxSolutionStatus.Optimal)
        {
            var sol = new JObject();
            sol["objective"] = (long)totalDistance.GetDoubleValue();
            var batchesArr = new JArray();
            for (int k = 0; k < maxNbBatches; ++k)
            {
                int c = (int)batchCount[k].GetValue();
                if (c > 0)
                {
                    var b = new JObject();
                    var orders = new JArray();
                    HxCollection batchValue = batch[k].GetCollectionValue();
                    for (int j = 0; j < c; ++j)
                    {
                        orders.Add(batchValue[j]);
                    }
                    b["orders"] = orders;
                    b["distance"] = (long)distance[k].GetDoubleValue();
                    var visitedAisles = new JArray();
                    for (int a = 0; a < nbAisles; ++a)
                    {
                        if (visited[k][a].GetValue() == 1)
                        {
                            visitedAisles.Add(a);
                        }
                    }
                    b["visitedAisles"] = visitedAisles;
                    batchesArr.Add(b);
                }
            }
            sol["batches"] = batchesArr;
            File.WriteAllText(fileName, sol.ToString(Newtonsoft.Json.Formatting.Indented) + "\n");
            Console.WriteLine("Solution written in file " + fileName);
        }
        else
        {
            Console.WriteLine("No feasible solution have been found within the allowed time.");
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.Error.WriteLine("Usage: OrderBatchingProblem inputFile [outputFile] [timeLimit]");
            Environment.Exit(1);
        }
        string instanceFile = args[0];
        string outputFile = args.Length > 1 ? args[1] : null;
        string strTimeLimit = args.Length > 2 ? args[2] : "20";

        using (OrderBatchingProblem model = new OrderBatchingProblem())
        {
            model.ReadInstance(instanceFile);
            model.Solve(int.Parse(strTimeLimit));

            if (outputFile != null)
            {
                model.WriteSolution(outputFile);
            }
        }
    }
}
Compilation / Execution (Windows)
javac OrderBatchingProblem.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. OrderBatchingProblem instances\15s-50-40-0.json
Compilation / Execution (Linux)
javac OrderBatchingProblem.java -cp /opt/hexaly_15_0/bin/hexaly.jar
java -cp /opt/hexaly_15_0/bin/hexaly.jar:. OrderBatchingProblem instances/15s-50-40-0.json
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
import java.util.*;
import java.io.*;
import com.hexaly.optimizer.*;
import com.google.gson.*;
import com.google.gson.stream.*;

public class OrderBatchingProblem {
    // Instance data
    private int nbOrders;           // Number of orders
    private int capacity;           // Capacity of the picker
    private int nbAisles;           // Number of aisles
    private int maxNbBatches;       // Upper bound on the number of batches
    private double aisleTraversal;  // Length of each aisle
    private double gap;             // Distance between the center lines and two adjacent aisles
    private double depotOffset;     // Distance from the depot to the first aisle
    private int[] nbItems;          // Number of items in each order
    private int[][] visitsAisle;    // Aisles visited by each order

    // Hexaly Optimizer
    private final HexalyOptimizer optimizer;
    // Decision variables
    private HxExpression[] batch;

    // Size of each batch
    private HxExpression[] batchCount;
    // Aisles visited by each batch
    private HxExpression[][] visited;
    // Distance travelled by each batch
    private HxExpression[] distance;

    // Objective function : total travelled distance
    private HxExpression totalDistance;

    private OrderBatchingProblem(HexalyOptimizer optimizer) {
        this.optimizer = optimizer;
    }

    // Read instance data
    private void readInstance(String fileName) throws IOException {
        JsonReader reader = new JsonReader(
            new InputStreamReader(new FileInputStream(fileName)));
        JsonObject data = JsonParser.parseReader(reader).getAsJsonObject();

        nbOrders = data.get("nbOrders").getAsInt();
        capacity = data.get("capacity").getAsInt();
        nbAisles = data.get("nbAisles").getAsInt();
        maxNbBatches = data.get("maxNbBatches").getAsInt();
        aisleTraversal = data.get("aisleTraversal").getAsDouble();
        gap = data.get("gap").getAsDouble();
        depotOffset = data.get("depotOffset").getAsDouble();

        nbItems = new int[nbOrders];
        visitsAisle = new int[nbAisles][nbOrders];

        JsonArray orders = (JsonArray) data.get("orders");
        for (int i = 0; i < nbOrders; ++i) {
            JsonObject order = (JsonObject) orders.get(i);
            nbItems[i] = order.get("nbItems").getAsInt();
            JsonArray aisles = (JsonArray) order.get("aislesToVisit");
            for (int j = 0; j < aisles.size(); ++j) {
                int a = aisles.get(j).getAsInt();
                visitsAisle[a][i] = 1;
            }
        }
    }

    // Create and solve the model
    private void solve(int limit) {
        // Declare the optimization model
        HxModel model = optimizer.getModel();

        // Set decision: batch[k] contains orders assigned to batch k
        batch = new HxExpression[maxNbBatches];
        for (int k = 0; k < maxNbBatches; ++k) {
            batch[k] = model.setVar(nbOrders);
        }
        // Partition constraint: each order is assigned to exactly one batch
        model.constraint(model.partition(batch));

        // Initialize batch properties
        batchCount = new HxExpression[maxNbBatches];
        distance = new HxExpression[maxNbBatches];
        visited = new HxExpression[maxNbBatches][];

        // Create arrays for aisle visits
        HxExpression[] visitsAisleArray = new HxExpression[nbAisles];
        for (int a = 0; a < nbAisles; ++a) {
            long[] col = new long[nbOrders];
            for (int i = 0; i < nbOrders; ++i) {
                col[i] = visitsAisle[a][i];
            }
            visitsAisleArray[a] = model.array(col);
        }

        // Create lambda function for nbItems (reused across batches)
        long[] nbItemsLong = new long[nbOrders];
        for (int i = 0; i < nbOrders; ++i) {
            nbItemsLong[i] = nbItems[i];
        }
        HxExpression nbItemsArray = model.array(nbItemsLong);
        HxExpression nbItemsLambda = model.lambdaFunction(
                i -> model.at(nbItemsArray, i));

        for (int k = 0; k < maxNbBatches; ++k) {
            // Capacity constraint: total number of items of orders assigned to batch k is limited
            HxExpression batchItems = model.sum(batch[k], nbItemsLambda);
            model.constraint(model.leq(batchItems, capacity));

            // Expressions for objective function
            // Batch size
            batchCount[k] = model.count(batch[k]);
            // Check which aisles are visited by current batch k
            visited[k] = new HxExpression[nbAisles];
            for (int a = 0; a < nbAisles; ++a) {
                final int localA = a;
                HxExpression visitedLambda = model.lambdaFunction(
                        i -> model.at(visitsAisleArray[localA], i));
                visited[k][a] = model.geq(model.sum(batch[k], visitedLambda), 1);
            }

            // Number of visited aisles
            HxExpression nbVisited = model.sum();
            for (int a = 0; a < nbAisles; ++a) {
                nbVisited.addOperand(visited[k][a]);
            }
            // Farthest visited aisle
            HxExpression maxVisited = model.max();
            for (int a = 0; a < nbAisles; ++a) {
                maxVisited.addOperand(model.prod(visited[k][a], a));
            }
            // S-shape traversal distance (full traversal of every visited aisle)
            distance[k] = model.prod(
                    model.gt(batchCount[k], 0),
                    model.sum(
                            model.sum(2 * depotOffset, model.prod(2 * gap, maxVisited)),
                            model.prod(
                                    model.sum(nbVisited, model.mod(nbVisited, 2)),
                                    aisleTraversal)));
        }

        // Minimize total distance
        totalDistance = model.sum();
        for (int k = 0; k < maxNbBatches; ++k) {
            totalDistance.addOperand(distance[k]);
        }
        model.minimize(totalDistance);
        model.close();

        // Parameterize the solver
        optimizer.getParam().setTimeLimit(limit);
        optimizer.solve();
    }

    // Write the solution in a JSON file
    //   - Objective function
    //   - Content of each batch
    //       | Computed orders
    //       | Travelled Distance
    //       | Visited Aisles
    private void writeSolution(String fileName) throws IOException {
        JsonObject sol = new JsonObject();
        sol.addProperty("objective", (long) totalDistance.getDoubleValue());
        JsonArray batchesArr = new JsonArray();
        for (int k = 0; k < maxNbBatches; ++k) {
            int c = (int) batchCount[k].getValue();
            if (c > 0) {
                JsonObject b = new JsonObject();
                JsonArray orders = new JsonArray();
                HxCollection batchValue = batch[k].getCollectionValue();
                for (int j = 0; j < c; ++j) {
                    orders.add(batchValue.get(j));
                }
                b.add("orders", orders);
                b.addProperty("distance", (long) distance[k].getDoubleValue());
                JsonArray visitedAisles = new JsonArray();
                for (int a = 0; a < nbAisles; ++a) {
                    if (visited[k][a].getValue() == 1) {
                        visitedAisles.add(a);
                    }
                }
                b.add("visitedAisles", visitedAisles);
                batchesArr.add(b);
            }
        }
        sol.add("batches", batchesArr);
        try (PrintWriter output = new PrintWriter(fileName)) {
            Gson gson = new GsonBuilder().setPrettyPrinting().create();
            output.println(gson.toJson(sol));
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: OrderBatchingProblem inputFile [outputFile] [timeLimit]");
            System.exit(1);
        }
        String instanceFile = args[0];
        String outputFile = args.length > 1 ? args[1] : null;
        String strTimeLimit = args.length > 2 ? args[2] : "20";

        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            OrderBatchingProblem model = new OrderBatchingProblem(optimizer);
            model.readInstance(instanceFile);
            model.solve(Integer.parseInt(strTimeLimit));
            if (optimizer.getSolution().getStatus() == HxSolutionStatus.Feasible
                    || optimizer.getSolution().getStatus() == HxSolutionStatus.Optimal) {
                if (outputFile != null) {
                    model.writeSolution(outputFile);
                }
            } else {
                System.err.println("No feasible solution have been found within the allowed time.");
            }
        } catch (Exception ex) {
            System.err.println(ex);
            ex.printStackTrace();
            System.exit(1);
        }
    }
}