Truck Loading Problem

Packing

Problem

In the Truck Loading Problem, a given set of items must be loaded into trucks. Thus, each item must be assigned to exactly one truck.

Each truck has two levels: a floor level and an upper level. Each level contains the same number of item positions, and each truck has the same weight capacity. Therefore, the total weight of the items assigned to a truck must not exceed this capacity.

In addition to their weight, items may have stacking restrictions:

  • Type 1 items must be placed on the floor and cannot have another item on top of them. They are called “Alone” items.
  • Type 2 items must be placed on the floor, but may have another item on top of them. They are called “Floor” items.
  • Type 3 items have no stacking restriction. They are called “No-restriction” items.
  • Type 4 items cannot have another item on top of them, but may be placed either on the floor or on the upper level. They are called “Delicate” items.

Finally, the upper level of a truck can be used only when its floor level is full. The objective is to minimize the number of used trucks.

Principles learned

  • Add set decision variables to model the contents of the bins
  • Define lambda functions to compute the total weight of a truck and enforce the stacking restrictions
  • Use the solution found by Hexaly Optimizer in a post-processing function

Data

The provided instances for the Truck Loading Problem are adapted from the Falkenauer instances from the BPPLIB. The format of the data files is as follows:

  • The first line contains a single integer: the number of items,
  • The second line contains two integers: the weight capacity of each truck, and the number of item positions of each level of a truck,
  • Then, each of the following lines describes one item using two integers:
    • First, its weight,
    • Then, an integer between 1 and 4 (inclusive) specifying its type:
      • 1 for “Alone” items,
      • 2 for “Floor” items,
      • 3 for “No-restriction” items,
      • or 4 for “Delicate” items.

Model

The Hexaly model for the Truck Loading Problem uses set decision variables. For each truck, we define a set variable that represents the items assigned to it. We constrain the set variables to form a partition, ensuring that each item belongs to exactly one truck.

We compute the total weight of a truck using a variadic sum operator on the set and a lambda function returning the weight associated with any item index. 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 weight not to exceed the truck capacity.

“Alone” and “Floor” items must be on the floor of their truck. Thus, the model ensures that they do not exceed the number of positions per level in any truck. We then add a similar constraint for “Alone” and “Delicate” items, which cannot have another item on top of them.

“Alone” items must be placed on the floor and block the corresponding above positions, since they cannot have another item above them. Therefore, we consider that they use two position units instead of one. The model then ensures that the number of blocked positions in each truck does not exceed the total number of positions.

We compute the total number of trucks used thanks to the count operator, which returns the number of elements in a set.

The model only gives the set of items to load in each truck. The exact placement of items within the trucks is computed separately by a post-processing function. It loads the “Alone” and “Floor” items first to ensure they are placed on the floor, then the “No-restriction” items, and finally the “Delicate” items, which are guaranteed to be on top.

Execution
hexaly truck_loading.hxm inFileName=instances/t60_00.txt [hxTimeLimit=] [solFileName=]
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
use io;
use math;

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

    if (inFileName == nil) throw usage;
    local inFile = io.openRead(inFileName);

    nbItems = inFile.readInt();
    truckWeightCapacity = inFile.readInt();
    levelSpotsCount = inFile.readInt();

    for [i in 0...nbItems] {
        itemWeights[i] = inFile.readInt();
        categoryItems[i] = inFile.readInt();

        // Type 1 items block both a floor position and the upper position above it
        volumes[i] = (categoryItems[i] == 1 ? 2 : 1);
    }

    // Bounds on the number of used trucks
    nbMinTrucks = ceil(sum[i in 0...nbItems](itemWeights[i]) / truckWeightCapacity);
    nbMaxTrucks = nbItems;
}

/* Declare the optimization model */
function model() {
    // Set decisions: trucks[k] represents the set of items assigned to truck k
    trucks[k in 0...nbMaxTrucks] <- set(nbItems);

    // Each item must be assigned to exactly one truck
    constraint partition[k in 0...nbMaxTrucks](trucks[k]);

    for [k in 0...nbMaxTrucks] {
        // Weight constraint for each truck 
        truckWeights[k] <- sum(trucks[k], i => itemWeights[i]);
        constraint truckWeights[k] <= truckWeightCapacity;

        // Volume constraint for each truck
        constraint sum(trucks[k], i => volumes[i]) <= 2 * levelSpotsCount;

        // Type 1 and type 2 items must be placed on the floor level
        constraint sum(trucks[k], i => or((categoryItems[i] == 1), (categoryItems[i] == 2))) <= levelSpotsCount;

        // Type 1 and type 4 items cannot have another item on top of them
        constraint sum(trucks[k], i => or((categoryItems[i] == 1), (categoryItems[i] == 4))) <= levelSpotsCount;

        // Truck k is used if at least one item is in it
        isUsed[k] <- (count(trucks[k]) > 0);
    }

    // Count the used trucks
    totalUsedTrucks <- sum[k in 0...nbMaxTrucks](isUsed[k]);

    // Minimize the number of used trucks
    minimize totalUsedTrucks;
}

/* Parametrize the solver */
function param() {
    if (hxTimeLimit == nil) hxTimeLimit = 10;  

    // Stop the search if the lower threshold is reached
    hxObjectiveThreshold = nbMinTrucks;
}

/* 
Auxiliary function used in the later "insideTruck" function. 
Inserts an item into the next free slot in the truck.
*/
function placeFirstAvailPos(positions, item) {
    if (positions[0].count() < levelSpotsCount) positions[0].add(item);
    else positions[1].add(item);
}

/*
The optimizer assigns items to trucks but does not position them inside each truck. 
This function computes a feasible two-level placement for the items in a truck.
*/
function insideTruck(truckSet) {
    // Split the items assigned to the truck by category
    setA = {}; // Type 1: floor level and nothing above. Called "Alone" items
    setF = {}; // Type 2: floor level. Called "Floor" items
    setN = {}; // Type 3: no restriction. Called "No-restriction" items
    setD = {}; // Type 4: nothing above. Called "Delicate" items
    for[i in truckSet] {    
        if (categoryItems[i] == 1) setA.add(i);
        else if (categoryItems[i] == 2) setF.add(i);  
        else if (categoryItems[i] == 3) setN.add(i); 
        else setD.add(i);
    }

    // Initialize the two-level position map: positions[0] represents the floor level, and positions[1] the upper level
    positions[0..1] = {};

    // Assign positions to items, from the most constraining category to the least constraining one: A -> F -> N -> D 

    // Place all "Alone" items on the floor, and leave the corresponding upper positions empty (represented by -1)
    for [i in setA] {
        positions[0].add(i);
        positions[1].add(-1);
    }

    // Place all "Floor" items on the floor
    for [i in setF] positions[0].add(i);

    // Place all "No-restriction" items at the first available position
    for [i in setN] placeFirstAvailPos(positions, i);

    // Place all "Delicate" items at the first available position
    for [i in setD] placeFirstAvailPos(positions, i);

    return positions;
}

/* Auxiliary function used to display the solution */
function padLeft(x, width) {
    local s;
    if (x == -1) s = " "; 
    else s = "" + x;
    while (s.length() < width) s = " " + s;
    return s;
}

/* Write the solution in a file */
function output() {
    if (solFileName == nil) return; 
    local solFile = io.openWrite(solFileName);

    solFile.println("Number of used trucks: " + totalUsedTrucks.value + "\n");

    printWidth = ceil(math.log10(nbItems - 1)) + 2;

    for [k in 0...nbMaxTrucks] {
        if (!isUsed[k].value) continue;
        truck = insideTruck(trucks[k].value);
        solFile.println("Truck weight: " + truckWeights[k].value);
        for [_ in 0...(levelSpotsCount * printWidth)] solFile.print("-");
        solFile.println();

        for [i in truck[1]] solFile.print(padLeft(i, printWidth));
        solFile.println();
        for [i in truck[0]] solFile.print(padLeft(i, printWidth));

        solFile.println();  
        for [_ in 0...(levelSpotsCount * printWidth)] solFile.print("-");
        solFile.println("\n");    
    }
}
Execution (Windows)
set PYTHONPATH=%HX_HOME%\bin\python
python truck_loading.py instances\t60_00.txt
Execution (Linux)
export PYTHONPATH=/opt/hexaly_15_0/bin/python
python truck_loading.py instances/t60_00.txt
# Copyright (c) Hexaly. Permission is hereby granted to use, copy,
# and modify this code for applications developed with Hexaly.
import hexaly.optimizer
import sys


def read_integers(filename):
    with open(filename) as f:
        return [int(elem) for elem in f.read().split()]

#
# Auxiliary function used in the later "insideTruck" function. 
# Inserts an item into the next free slot in the truck.
#
def place_first_available_pos(positions, item, level_spots_count):
    if len(positions[0]) < level_spots_count:
        positions[0].append(item)
    else:
        positions[1].append(item)

#
# The optimizer assigns items to trucks but does not position them inside each truck.  
# This function computes a feasible two-level placement for the items in a truck.
#
def inside_truck(truck_set, category_items, level_spots_count):
    # Split the items assigned to the truck by category
    set_a = []  # Type 1: floor level and nothing above. Called "Alone" items
    set_f = []  # Type 2: floor level. Called "Floor" items
    set_n = []  # Type 3: no restriction. Called "No-restriction" items
    set_d = []  # Type 4: nothing above. Called "Delicate" items

    for i in truck_set:
        category = category_items[i]
        if category == 1:
            set_a.append(i)
        elif category == 2:
            set_f.append(i)
        elif category == 3:
            set_n.append(i)
        else:
            set_d.append(i)

    # Initialize the two-level position list: positions[0] represents the floor level, and positions[1] the upper level
    positions = [[], []]

    #
    # Assign positions to items, from the most constraining category to the least constraining one: A -> F -> N -> D
    #

    # Place all "Alone" items on the floor, and leave the corresponding upper positions empty (represented by -1)
    for i in set_a:
        positions[0].append(i)
        positions[1].append(-1)

    # Place all "Floor" items on the floor
    for i in set_f:
        positions[0].append(i)

    # Place all "No-restriction" items at the first available position
    for i in set_n:
        place_first_available_pos(positions, i, level_spots_count)

    # Place all "Delicate" items at the first available position
    for i in set_d:
        place_first_available_pos(positions, i, level_spots_count)

    return positions

# Auxiliary function used to display the solution
def pad_left(x, width):
    return (" " if x == -1 else str(x)).rjust(width)


def solve(instance_file, sol_file, time_limit=10):
    #
    # Read instance data
    #
    file_it = iter(read_integers(instance_file))

    nb_items = int(next(file_it))
    truck_weight_capacity = int(next(file_it))
    level_spots_count = int(next(file_it))

    item_weights = []
    category_items = []
    volumes_data = []

    for _ in range(nb_items):
        weight = int(next(file_it))
        category = int(next(file_it))
        item_weights.append(weight)
        category_items.append(category)

        # "Alone" items block both a floor position and the upper position above it
        volumes_data.append(2 if category == 1 else 1)

    # Bounds on the number of used trucks
    nb_min_trucks = (sum(item_weights) + truck_weight_capacity - 1) // truck_weight_capacity
    nb_max_trucks = nb_items

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

        # Set decisions: trucks[k] represents the items assigned to truck k
        trucks = [model.set(nb_items) for _ in range(nb_max_trucks)]

        # Each item must be assigned to exactly one truck
        model.constraint(model.partition(trucks))

        #
        # Create arrays and functions to retrieve the item's data
        #
        weights = model.array(item_weights)
        categories = model.array(category_items)
        volumes = model.array(volumes_data)

        weight_lambda = model.lambda_function(lambda i: weights[i])
        volume_lambda = model.lambda_function(lambda i: volumes[i])
        floor_lambda = model.lambda_function(
            lambda i: model.or_(categories[i] == 1, categories[i] == 2)
        )
        delicate_lambda = model.lambda_function(
            lambda i: model.or_(categories[i] == 1, categories[i] == 4)
        )

        truck_weights = []
        is_used = []

        for k in range(nb_max_trucks):
            truck_weight = model.sum(trucks[k], weight_lambda)
            truck_weights.append(truck_weight)

            # Weight constraint for each truck
            model.constraint(truck_weight <= truck_weight_capacity)

            # Volume constraint for each truck
            model.constraint(model.sum(trucks[k], volume_lambda) <= 2 * level_spots_count)

            # Type 1 and type 2 items must be placed on the floor level
            model.constraint(model.sum(trucks[k], floor_lambda) <= level_spots_count)

            # Type 1 and type 4 items cannot have another item on top of them
            model.constraint(model.sum(trucks[k], delicate_lambda) <= level_spots_count)

            # Truck k is used if at least one item is in it
            is_used.append(model.count(trucks[k]) > 0)

        # Count the used trucks
        total_used_trucks = model.sum(is_used)

        # Minimize the number of used trucks
        model.minimize(total_used_trucks)
        model.close()

        # Parametrize the optimizer
        optimizer.param.time_limit = time_limit

        # Stop the search if the lower threshold is reached
        optimizer.param.set_objective_threshold(0, nb_min_trucks)

        optimizer.solve()

        #
        # Write the solution in a file
        #
        if sol_file is not None:
            print_width = len(str(nb_items - 1)) + 2

            with open(sol_file, "w") as f:
                f.write(f"Number of used trucks: {total_used_trucks.value}\n\n")

                for k in range(nb_max_trucks):
                    if not is_used[k].value:
                        continue

                    truck = inside_truck(trucks[k].value, category_items, level_spots_count)
                    line = "-" * (level_spots_count * print_width)

                    f.write(f"Truck weight: {truck_weights[k].value}\n")
                    f.write(line + "\n")

                    for item in truck[1]:
                        f.write(pad_left(item, print_width))
                    f.write("\n")
                    for item in truck[0]:
                        f.write(pad_left(item, print_width))

                    f.write("\n")
                    f.write(line + "\n\n")


def main():
    if len(sys.argv) < 2:
        print("Usage: python truck_loading.py inputFile [outputFile] [timeLimit]")
        sys.exit(1)

    instance_file = sys.argv[1]
    if (len(sys.argv) >= 4):
        time_limit = int(sys.argv[3])  
    else: 
        time_limit = 10

    if (len(sys.argv) >= 3):
        solve(instance_file, sys.argv[2], time_limit)
    else:
        solve(instance_file, None, time_limit)


if __name__ == "__main__":
    main()
Compilation / Execution (Windows)
cl /EHsc truck_loading.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly150.lib
truck_loading instances\t60_00.txt
Compilation / Execution (Linux)
g++ truck_loading.cpp -I/opt/hexaly_15_0/include -lhexaly150 -lpthread -o truck_loading
./truck_loading instances/t60_00.txt
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
#include "optimizer/hexalyoptimizer.h"
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

using namespace hexaly;
using namespace std;

class TruckLoading {
private:
    // Number of items
    int nbItems;

    // Weight capacity of each truck
    int truckWeightCapacity;

    // Number of available spots per level for each truck
    int levelSpotsCount;

    // Weight of each item
    vector<hxint> itemWeightsData;

    // Type (ie. stacking restrictions) of each item 
    vector<hxint> categoryItemsData;

    // Volume occupied by each item
    vector<hxint> volumesData;

    // Bounds on the number of used trucks
    int nbMinTrucks;
    int nbMaxTrucks;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;

    // Decision variables and expressions 
    vector<HxExpression> trucks;
    vector<HxExpression> truckWeights;
    vector<HxExpression> isUsed;
    HxExpression totalUsedTrucks;

    struct TruckPlacement {
        vector<int> floor;
        vector<int> upper;
    };

public:
    /* Read instance data */
    void readInstance(const string& fileName) {
        ifstream infile;
        infile.exceptions(ifstream::failbit | ifstream::badbit);
        infile.open(fileName.c_str());

        infile >> nbItems;
        infile >> truckWeightCapacity;
        infile >> levelSpotsCount;

        itemWeightsData.resize(nbItems);
        categoryItemsData.resize(nbItems);
        volumesData.resize(nbItems);

        hxint totalWeight = 0;
        for (int i = 0; i < nbItems; ++i) {
            infile >> itemWeightsData[i];
            infile >> categoryItemsData[i];
            // Type 1 items block both a floor position and the upper position above it
            volumesData[i] = (categoryItemsData[i] == 1 ? 2 : 1);
            totalWeight += itemWeightsData[i];
        }

        // Bounds on the number of used trucks
        nbMinTrucks = (totalWeight + truckWeightCapacity - 1) / truckWeightCapacity;
        nbMaxTrucks = nbItems;
    }

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

        trucks.resize(nbMaxTrucks);
        truckWeights.resize(nbMaxTrucks);
        isUsed.resize(nbMaxTrucks);

        // Set decisions: trucks[k] represents the set of items assigned to truck k
        for (int k = 0; k < nbMaxTrucks; ++k) {
            trucks[k] = model.setVar(nbItems);
        }

        // Each item must be assigned to exactly one truck
        model.constraint(model.partition(trucks.begin(), trucks.end()));

        // Create arrays and functions to retrieve the item's data 
        HxExpression itemWeights = model.array(itemWeightsData.begin(), itemWeightsData.end());
        HxExpression categoryItems = model.array(categoryItemsData.begin(), categoryItemsData.end());
        HxExpression volumes = model.array(volumesData.begin(), volumesData.end());

        HxExpression weightLambda = model.lambdaFunction([&](HxExpression i) {
            return itemWeights[i];
        });

        HxExpression volumeLambda = model.lambdaFunction([&](HxExpression i) {
            return volumes[i];
        });

        HxExpression mustBeOnFloorLambda = model.lambdaFunction([&](HxExpression i) {
            return model.or_(model.eq(categoryItems[i], 1), model.eq(categoryItems[i], 2));
        });

        HxExpression cannotHaveAboveLambda = model.lambdaFunction([&](HxExpression i) {
            return model.or_(model.eq(categoryItems[i], 1), model.eq(categoryItems[i], 4));
        });

        for (int k = 0; k < nbMaxTrucks; ++k) {
            // Weight constraint for each truck
            truckWeights[k] = model.sum(trucks[k], weightLambda);
            model.constraint(truckWeights[k] <= truckWeightCapacity);

            // Volume constraint for each truck
            model.constraint(model.sum(trucks[k], volumeLambda) <= 2 * levelSpotsCount);

            // Type 1 and type 2 items must be placed on the floor level
            model.constraint(model.sum(trucks[k], mustBeOnFloorLambda) <= levelSpotsCount);

            // Type 1 and type 4 items cannot have another item on top of them
            model.constraint(model.sum(trucks[k], cannotHaveAboveLambda) <= levelSpotsCount);

            // Truck k is used if at least one item is in it
            isUsed[k] = model.count(trucks[k]) > 0;
        }

        // Count the used trucks
        totalUsedTrucks = model.sum(isUsed.begin(), isUsed.end());

        // Minimize the number of used trucks
        model.minimize(totalUsedTrucks);

        model.close();

        // Parametrize the optimizer
        optimizer.getParam().setTimeLimit(timeLimit);

        // Stop the search if the lower threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, static_cast<hxint>(nbMinTrucks));

        optimizer.solve();
    }

private:
    /* 
    Auxiliary function used in the later "insideTruck" function.
    Inserts an item into the next free slot in the truck. 
    */
    void placeFirstAvailPos(TruckPlacement& positions, int item) const {
        if (static_cast<int>(positions.floor.size()) < levelSpotsCount) {
            positions.floor.push_back(item);
        } else {
            positions.upper.push_back(item);
        }
    }

    /*
    The optimizer assigns items to trucks but does not position them inside each truck. 
    This function computes a feasible two-level placement for the items in a truck.
    */
    TruckPlacement insideTruck(const HxCollection& truckSet) const {
        // Split the items assigned to the truck by category.
        vector<int> setA; // Type 1: floor level and nothing above. Called "Alone" items
        vector<int> setF; // Type 2: floor level. Called "Floor" items
        vector<int> setN; // Type 3: no restriction. Called "No-restriction" items
        vector<int> setD; // Type 4: nothing above. Called "Delicate" items

        for (int p = 0; p < truckSet.count(); ++p) {
            int i = static_cast<int>(truckSet[p]);
            if (categoryItemsData[i] == 1) {
                setA.push_back(i);
            } else if (categoryItemsData[i] == 2) {
                setF.push_back(i);
            } else if (categoryItemsData[i] == 3) {
                setN.push_back(i);
            } else {
                setD.push_back(i);
            }
        }

        TruckPlacement positions;

        // Assign positions to items, from the most constraining category to the least constraining one: A -> F -> N -> D 

        // Place all "Alone" items on the floor, and leave the corresponding upper positions empty, represented by -1
        for (int i : setA) {
            positions.floor.push_back(i);
            positions.upper.push_back(-1);
        }

        // Place all "Floor" items on the floor
        for (int i : setF) {
            positions.floor.push_back(i);
        }

        // Place all "No-restriction" items at the first available position
        for (int i : setN) {
            placeFirstAvailPos(positions, i);
        }

        // Place all "Delicate" items at the first available position
        for (int i : setD) {
            placeFirstAvailPos(positions, i);
        }

        return positions;
    }

    /* Auxiliary function used to display the solution */
    string padLeft(int x, int width) const {
        string s = (x == -1 ? " " : to_string(x));
        while (static_cast<int>(s.length()) < width) {
            s = " " + s;
        }
        return s;
    }

public:
    /* Write the solution in a file */
    void writeSolution(const string& fileName) {
        ofstream outfile;
        outfile.exceptions(ofstream::failbit | ofstream::badbit);
        outfile.open(fileName.c_str());

        outfile << "Number of used trucks: " << totalUsedTrucks.getValue() << "\n" << endl;

        int printWidth = 2;
        if (nbItems > 1) {
            printWidth = static_cast<int>(ceil(log10(static_cast<double>(nbItems - 1)))) + 2;
        }

        for (int k = 0; k < nbMaxTrucks; ++k) {
            if (!isUsed[k].getValue()) continue;

            HxCollection truckCollection = trucks[k].getCollectionValue();
            TruckPlacement truck = insideTruck(truckCollection);

            outfile << "Truck weight: " << truckWeights[k].getValue() << endl;

            for (int i = 0; i < levelSpotsCount * printWidth; ++i) outfile << "-";
            outfile << endl;

            for (int item : truck.upper) outfile << padLeft(item, printWidth);
            outfile << endl;
            for (int item : truck.floor) outfile << padLeft(item, printWidth);

            outfile << endl;
            for (int i = 0; i < levelSpotsCount * printWidth; ++i) outfile << "-";
            outfile << "\n" << endl;
        }
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "Usage: truck_loading inputFile [outputFile] [timeLimit]" << endl;
        return 1;
    }

    const char* instanceFile = argv[1];
    const char* solFile = (argc > 2 ? argv[2] : NULL);
    const int timeLimit = (argc > 3 ? atoi(argv[3]) : 5);

    try {
        TruckLoading model;
        model.readInstance(instanceFile);
        model.solve(timeLimit);
        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 TruckLoading.cs /reference:Hexaly.NET.dll
TruckLoading instances\t60_00.txt
// 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 System.Linq;
using Hexaly.Optimizer;

public class TruckLoading : IDisposable
{
    // Number of items
    int nbItems;
    
    // Weight capacity of each truck
    int truckWeightCapacity;

    // Number of available spots per level for each truck
    int levelSpotsCount;

    // Weight of each item
    long[] itemWeightsData;

    // Type (ie. stacking restrictions) of each item 
    long[] categoryItemsData;

    // Volume occupied by each item
    long[] volumesData;

    // Bounds on the number of used trucks
    int nbMinTrucks;
    int nbMaxTrucks;

    // Hexaly Optimizer
    HexalyOptimizer optimizer;

    // Decision variables and expressions
    HxExpression[] trucks;
    HxExpression[] truckWeights;
    HxExpression[] isUsed;
    HxExpression totalUsedTrucks;

    struct TruckPlacement
    {
        public List<int> floor;
        public List<int> upper;

        public TruckPlacement(bool init)
        {
            floor = new List<int>();
            upper = new List<int>();
        }
    }

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

    /* Read instance data */
    void ReadInstance(string fileName)
    {
        using (StreamReader input = new StreamReader(fileName))
        {
            nbItems = int.Parse(input.ReadLine());

            string[] secondLine = input.ReadLine()
                .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);

            truckWeightCapacity = int.Parse(secondLine[0]);
            levelSpotsCount = int.Parse(secondLine[1]);

            itemWeightsData = new long[nbItems];
            categoryItemsData = new long[nbItems];
            volumesData = new long[nbItems];

            long totalWeight = 0;
            for (int i = 0; i < nbItems; ++i)
            {
                string[] line = input.ReadLine()
                    .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);

                itemWeightsData[i] = long.Parse(line[0]);
                categoryItemsData[i] = long.Parse(line[1]);

                // Type 1 items block both a floor position and the upper position above it
                volumesData[i] = (categoryItemsData[i] == 1 ? 2 : 1);
                totalWeight += itemWeightsData[i];
            }

            // Bounds on the number of used trucks
            nbMinTrucks = (int)Math.Ceiling((double)totalWeight / truckWeightCapacity);
            nbMaxTrucks = nbItems;
        }
    }

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

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

        trucks = new HxExpression[nbMaxTrucks];
        truckWeights = new HxExpression[nbMaxTrucks];
        isUsed = new HxExpression[nbMaxTrucks];

        // Set decisions: trucks[k] represents the set of items assigned to truck k
        for (int k = 0; k < nbMaxTrucks; ++k)
        {
            trucks[k] = model.Set(nbItems);
        }

        // Each item must be assigned to exactly one truck
        model.Constraint(model.Partition(trucks));

        /* Create arrays and functions to retrieve the item's data */
        HxExpression itemWeights = model.Array(itemWeightsData);
        HxExpression categoryItems = model.Array(categoryItemsData);
        HxExpression volumes = model.Array(volumesData);

        HxExpression weightLambda = model.LambdaFunction(i => itemWeights[i]);

        HxExpression volumeLambda = model.LambdaFunction(i => volumes[i]);

        HxExpression mustBeOnFloorLambda = model.LambdaFunction(i =>
            model.Or(model.Eq(categoryItems[i], 1), model.Eq(categoryItems[i], 2))
        );

        HxExpression cannotHaveAboveLambda = model.LambdaFunction(i =>
            model.Or(model.Eq(categoryItems[i], 1), model.Eq(categoryItems[i], 4))
        );

        for (int k = 0; k < nbMaxTrucks; ++k)
        {
            // Weight constraint for each truck
            truckWeights[k] = model.Sum(trucks[k], weightLambda);
            model.Constraint(truckWeights[k] <= truckWeightCapacity);

            // Volume constraint for each truck
            model.Constraint(model.Sum(trucks[k], volumeLambda) <= 2 * levelSpotsCount);

            // Type 1 and type 2 items must be placed on the floor level
            model.Constraint(model.Sum(trucks[k], mustBeOnFloorLambda) <= levelSpotsCount);

            // Type 1 and type 4 items cannot have another item on top of them
            model.Constraint(model.Sum(trucks[k], cannotHaveAboveLambda) <= levelSpotsCount);

            // Truck k is used if at least one item is in it
            isUsed[k] = model.Count(trucks[k]) > 0;
        }

        // Count the used trucks
        totalUsedTrucks = model.Sum(isUsed);

        // Minimize the number of used trucks
        model.Minimize(totalUsedTrucks);

        model.Close();

        // Parametrize the optimizer
        optimizer.GetParam().SetTimeLimit(limit);

        // Stop the search if the lower threshold is reached
        optimizer.GetParam().SetObjectiveThreshold(0, nbMinTrucks);

        optimizer.Solve();
    }

    /* 
    Auxiliary function used in the later "insideTruck" function.
    Inserts an item into the next free slot in the truck. 
    */
    void PlaceFirstAvailPos(ref TruckPlacement positions, int item)
    {
        if (positions.floor.Count < levelSpotsCount)
        {
            positions.floor.Add(item);
        }
        else
        {
            positions.upper.Add(item);
        }
    }

    /*
    The optimizer assigns items to trucks but does not position them inside each truck. 
    This function computes a feasible two-level placement for the items in a truck.
    */
    TruckPlacement InsideTruck(HxCollection truckSet)
    {
        List<int> setA = new List<int>(); // Type 1: floor level and nothing above. Called "Alone" items
        List<int> setF = new List<int>(); // Type 2: floor level. Called "Floor" items
        List<int> setN = new List<int>(); // Type 3: no restriction. Called "No-restriction" items
        List<int> setD = new List<int>(); // Type 4: nothing above. Called "Delicate" items

        // Split the items assigned to the truck by category
        for (int p = 0; p < truckSet.Count(); ++p)
        {
            int i = (int)truckSet[p];
            if (categoryItemsData[i] == 1)
            {
                setA.Add(i);
            }
            else if (categoryItemsData[i] == 2)
            {
                setF.Add(i);
            }
            else if (categoryItemsData[i] == 3)
            {
                setN.Add(i);
            }
            else
            {
                setD.Add(i);
            }
        }

        TruckPlacement positions = new TruckPlacement(true);

        // Assign positions to items, from the most constraining category to the least constraining one: A -> F -> N -> D 

        // Place all "Alone" items on the floor, and leave the corresponding upper positions empty, represented by -1.
        foreach (int i in setA)
        {
            positions.floor.Add(i);
            positions.upper.Add(-1);
        }

        // Place all "Floor" items on the floor
        foreach (int i in setF)
        {
            positions.floor.Add(i);
        }

        // Place all "No-restriction" items at the first available position
        foreach (int i in setN)
        {
            PlaceFirstAvailPos(ref positions, i);
        }

        // Place all "Delicate" items at the first available position
        foreach (int i in setD)
        {
            PlaceFirstAvailPos(ref positions, i);
        }

        return positions;
    }

    /* Auxiliary function used to display the solution */
    string PadLeft(int x, int width)
    {
        string s = (x == -1 ? " " : x.ToString());
        while (s.Length < width)
        {
            s = " " + s;
        }
        return s;
    }

    /* Write the solution in a file */
    void WriteSolution(string fileName)
    {
        using (StreamWriter output = new StreamWriter(fileName))
        {
            output.WriteLine("Number of used trucks: " + totalUsedTrucks.GetValue());
            output.WriteLine();

            int printWidth = 2;
            if (nbItems > 1)
            {
                printWidth = (int)Math.Ceiling(Math.Log10((double)(nbItems - 1))) + 2;
            }

            for (int k = 0; k < nbMaxTrucks; ++k)
            {
                if (isUsed[k].GetValue() == 0) continue;

                HxCollection truckCollection = trucks[k].GetCollectionValue();
                TruckPlacement truck = InsideTruck(truckCollection);

                output.WriteLine("Truck weight: " + truckWeights[k].GetValue());

                for (int i = 0; i < levelSpotsCount * printWidth; ++i) output.Write("-");
                output.WriteLine();

                foreach (int item in truck.upper) output.Write(PadLeft(item, printWidth));
                output.WriteLine();
                foreach (int item in truck.floor) output.Write(PadLeft(item, printWidth));

                output.WriteLine();
                for (int i = 0; i < levelSpotsCount * printWidth; ++i) output.Write("-");
                output.WriteLine();
                output.WriteLine();
            }
        }
    }

    public static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.Error.WriteLine("Usage: TruckLoading 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] : "10";
        
        using (TruckLoading model = new TruckLoading())
        {
            model.ReadInstance(instanceFile);
            model.Solve(int.Parse(strTimeLimit));
            if (outputFile != null)
                model.WriteSolution(outputFile);
        }
    }
}
Compilation / Execution (Windows)
javac TruckLoading.java -cp %HX_HOME%\bin\hexaly.jar
java -cp %HX_HOME%\bin\hexaly.jar;. TruckLoading instances\t60_00.txt
Compilation / Execution (Linux)
javac TruckLoading.java -cp /opt/hexaly_15_0/bin/hexaly.jar
java -cp /opt/hexaly_15_0/bin/hexaly.jar:. TruckLoading instances/t60_00.txt
// 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.*;

public class TruckLoading {
    // Number of items
    private int nbItems;

    // Weight capacity of each truck
    private int truckWeightCapacity;

    // Number of available spots per level for each truck
    private int levelSpotsCount;

    // Weight of each item
    private long[] itemWeights;

    // Type (ie. stacking restrictions) of each item 
    private long[] categoryItems;

    // Volume occupied by each item
    private long[] volumes;

    // Bounds on the number of used trucks
    private int nbMinTrucks;
    private int nbMaxTrucks;

    // Hexaly Optimizer
    private final HexalyOptimizer optimizer;

    // Decision variables and expressions
    private HxExpression[] trucks;
    private HxExpression[] truckWeights;
    private HxExpression[] isUsed;
    private HxExpression totalUsedTrucks;

    private String inFileName;

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

    /* Read instance data */
    private void readInstance(String fileName) throws IOException {
        inFileName = fileName;

        try (Scanner input = new Scanner(new File(fileName))) {
            input.useLocale(Locale.ROOT);

            nbItems = input.nextInt();
            truckWeightCapacity = input.nextInt();
            levelSpotsCount = input.nextInt();

            itemWeights = new long[nbItems];
            categoryItems = new long[nbItems];
            volumes = new long[nbItems];

            long totalWeight = 0;
            for (int i = 0; i < nbItems; ++i) {
                itemWeights[i] = input.nextInt();
                categoryItems[i] = input.nextInt();
                totalWeight += itemWeights[i];

                // Type 1 items block both a floor position and the upper position above it
                volumes[i] = categoryItems[i] == 1 ? 2 : 1;
            }

            // Bounds on the number of used trucks
            nbMinTrucks = (int) Math.ceil((double) totalWeight / truckWeightCapacity);
            nbMaxTrucks = nbItems;
        }
    }

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

        trucks = new HxExpression[nbMaxTrucks];
        truckWeights = new HxExpression[nbMaxTrucks];
        isUsed = new HxExpression[nbMaxTrucks];

        // Set decisions: trucks[k] represents the set of items assigned to truck k
        for (int k = 0; k < nbMaxTrucks; ++k) {
            trucks[k] = model.setVar(nbItems);
        }

        // Each item must be assigned to exactly one truck
        model.constraint(model.partition(trucks));

        /* Create arrays and functions to retrieve the item's data */
        HxExpression weightsArray = model.array(itemWeights);
        HxExpression volumesArray = model.array(volumes);
        HxExpression categoriesArray = model.array(categoryItems);

        HxExpression weightLambda = model.lambdaFunction(i -> model.at(weightsArray, i));
        HxExpression volumeLambda = model.lambdaFunction(i -> model.at(volumesArray, i));
        HxExpression floorLevelLambda = model.lambdaFunction(i -> model.or(
                model.eq(model.at(categoriesArray, i), 1),
                model.eq(model.at(categoriesArray, i), 2)));
        HxExpression delicatenessLambda = model.lambdaFunction(i -> model.or(
                model.eq(model.at(categoriesArray, i), 1),
                model.eq(model.at(categoriesArray, i), 4)));

        for (int k = 0; k < nbMaxTrucks; ++k) {
            // Weight constraint for each truck 
            truckWeights[k] = model.sum(trucks[k], weightLambda);
            model.constraint(model.leq(truckWeights[k], truckWeightCapacity));

            // Volume constraint for each truck
            model.constraint(model.leq(model.sum(trucks[k], volumeLambda), 2 * levelSpotsCount));

            // Type 1 and type 2 items must be placed on the floor level
            model.constraint(model.leq(model.sum(trucks[k], floorLevelLambda), levelSpotsCount));

            // Type 1 and type 4 items cannot have another item on top of them
            model.constraint(model.leq(model.sum(trucks[k], delicatenessLambda), levelSpotsCount));

            // Truck k is used if at least one item is in it
            isUsed[k] = model.gt(model.count(trucks[k]), 0);
        }

        // Count the used trucks
        totalUsedTrucks = model.sum(isUsed);

        // Minimize the number of used trucks
        model.minimize(totalUsedTrucks);

        model.close();

        // Parametrize the optimizer 
        optimizer.getParam().setTimeLimit(limit);

        // Stop the search if the lower threshold is reached
        optimizer.getParam().setObjectiveThreshold(0, nbMinTrucks);

        optimizer.solve();
    }

    /* 
    Auxiliary function used in the later "insideTruck" function. 
    Inserts an item into the next free slot in the truck.
    */
    private void placeFirstAvailPos(List<Integer>[] positions, int item) {
        if (positions[0].size() < levelSpotsCount) positions[0].add(item);
        else positions[1].add(item);
    }

    /*
    The optimizer assigns items to trucks but does not position them inside each truck. 
    This function computes a feasible two-level placement for the items in a truck.
    */
    @SuppressWarnings("unchecked")
    private List<Integer>[] insideTruck(HxCollection truckSet) {
        // Split the items assigned to the truck by category
        List<Integer> setA = new ArrayList<>(); // Type 1: floor level and nothing above. Called "Alone" items
        List<Integer> setF = new ArrayList<>(); // Type 2: floor level. Called "Floor" items
        List<Integer> setN = new ArrayList<>(); // Type 3: no restriction. Called "No-restriction" items
        List<Integer> setD = new ArrayList<>(); // Type 4: nothing above. Called "Delicate" items

        for (int pos = 0; pos < truckSet.count(); ++pos) {
            int i = (int) truckSet.get(pos);
            if (categoryItems[i] == 1) {
                setA.add(i);
            } else if (categoryItems[i] == 2) {
                setF.add(i);  
            } else if (categoryItems[i] == 3) {
                setN.add(i); 
            } else {
                setD.add(i);
            }
        }

        // Initialize the two-level position map: positions[0] represents the floor level, and positions[1] the upper level
        List<Integer>[] positions = new ArrayList[2];
        positions[0] = new ArrayList<>();
        positions[1] = new ArrayList<>();

        // Assign positions to items, from the most constraining category to the least constraining one: A -> F -> N -> D 

        // Place all "Alone" items on the floor, and leave the corresponding upper positions empty (represented by -1)
        for (int i : setA) {
            positions[0].add(i);
            positions[1].add(-1);
        }

        // Place all "Floor" items on the floor
        for (int i : setF) positions[0].add(i);

        // Place all "No-restriction" items at the first available position
        for (int i : setN) placeFirstAvailPos(positions, i);

        // Place all "Delicate" items at the first available position
        for (int i : setD) placeFirstAvailPos(positions, i);

        return positions;
    }

    /* Auxiliary function used to display the solution */
    private String padLeft(int x, int width) {
        String s;
        if (x == -1) s = " "; 
        else s = "" + x;
        while (s.length() < width) s = " " + s;
        return s;
    }

    /* Write the solution in a file */
    private void writeSolution(String fileName) throws IOException {
        try (PrintWriter output = new PrintWriter(fileName)) {
            output.println("Number of used trucks: " + totalUsedTrucks.getValue() + "\n");

            int printWidth = (int) Math.ceil(Math.log10(nbItems - 1)) + 2;

            for (int k = 0; k < nbMaxTrucks; ++k) {
                if (isUsed[k].getValue() == 0) continue;
                List<Integer>[] truck = insideTruck(trucks[k].getCollectionValue());
                output.println("Truck weight: " + truckWeights[k].getValue());
                for (int i = 0; i < levelSpotsCount * printWidth; ++i) output.print("-");
                output.println();

                for (int i : truck[1]) output.print(padLeft(i, printWidth));
                output.println();
                for (int i : truck[0]) output.print(padLeft(i, printWidth));

                output.println();  
                for (int i = 0; i < levelSpotsCount * printWidth; ++i) output.print("-");
                output.println("\n");    
            }
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java TruckLoading 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] : "10";

        try (HexalyOptimizer optimizer = new HexalyOptimizer()) {
            TruckLoading model = new TruckLoading(optimizer);
            model.readInstance(instanceFile);
            model.solve(Integer.parseInt(strTimeLimit));
            if (outputFile != null)
                model.writeSolution(outputFile);
        } catch (Exception ex) {
            System.err.println(ex);
            ex.printStackTrace();
            System.exit(1);
        }
    }
}