Cutting Stock Problem
PackingProblem
In the Cutting Stock Problem, we must cut rectangular items of various widths and heights from large rolls (or plates) of fixed dimensions. The cutting follows a structured two-level guillotine pattern:
- Bands: Items sharing the same width are grouped side by side along their height, forming horizontal bands. Each band has a fixed width (the common item width) and a total length equal to the sum of the item heights.
- Rolls: Bands are then stacked vertically within a roll. The total width of bands in a roll must not exceed the roll width. Similarly, each band’s length must fit within the roll length.
The objective is to minimize the number of rolls used.
Principles learned
- Use two levels of set decision variables to model a hierarchical packing structure (items into bands, bands into rolls)
- Use the distinct operator with a lambda to enforce that all items in a band share the same width
- Guard max over sets with a ternary (
count(s) > 0 ? max(s, ...) : 0) to avoid NaN on empty sets
Data
The Cutting Stock instances provided are the A instances from the 2DPackLib. The format of the data files is as follows:
- First line: number of item types
- Second line: roll width, roll length
- For each item type: width, height, demand
Model
The Hexaly model for the Cutting Stock Problem uses two levels of set decision variables. At the first level, for each band, we define a set variable representing the items assigned to this band. At the second level, for each roll, we define a set variable representing the bands assigned to this roll. Both levels use partition constraints to ensure that every item belongs to exactly one band, and every band belongs to exactly one roll.
Within each band, all items must share the same width. This is enforced using the distinct operator with a lambda function: distinct(bands[b], (i) => rawWidth[i]) returns the set of distinct widths in the band, and we constrain its count to be at most 1. We compute the band length as the sum of item heights using a variadic sum operator, and we constrain each band’s length to fit within the roll length. At the roll level, we compute the total width of bands using a variadic sum over the roll’s set of bands with a lambda function returning each band’s width, and we constrain it not to exceed the roll width.
The model computes a simple lower bound on the number of rolls (total item area divided by roll area) and uses hxObjectiveThreshold to stop early when this bound is reached.
- Execution
-
hexaly cutting_stock.hxm inFileName=instances/A1.txt [hxTimeLimit=] [solFileName=]
// Copyright (c) Hexaly. Permission is hereby granted to use, copy,
// and modify this code for applications developed with Hexaly.
use io;
function input() {
local usage =
"Usage: hexaly cutting_stock.hxm "
+ "inFileName=inputFile [solFileName=outputFile] [hxTimeLimit=timeLimit]";
if (inFileName == nil) throw usage;
local inFile = io.openRead(inFileName);
nbItemTypes = inFile.readInt();
rollWidth = inFile.readInt();
rollLength = inFile.readInt();
// Read item types and expand by demand
nbItems = 0;
for [t in 0...nbItemTypes] {
local weight = inFile.readInt();
local length = inFile.readInt();
local demand = inFile.readInt();
for [i in 0...demand] {
rawWidth[nbItems] = weight;
rawLength[nbItems] = length;
nbItems = nbItems + 1;
}
}
nbMaxBands = nbItems;
nbMaxRolls = nbItems;
// Lower bound on total number of rolls: total item area / rollArea
minNumberOfRolls =
ceil(sum[i in 0...nbItems](rawWidth[i] * rawLength[i])
/ (rollWidth * rollLength));
}
/* Declare the optimization model */
function model() {
// Set decisions: bands[b] represents the items assigned to band b
bands[0...nbMaxBands] <- set(nbItems);
// Set decisions: rolls[r] represents the bands assigned to roll r
rolls[0...nbMaxRolls] <- set(nbMaxBands);
// Each item must be in exactly one band, and each band in exactly one roll
constraint partition[b in 0...nbMaxBands](bands[b]);
constraint partition[r in 0...nbMaxRolls](rolls[r]);
for [b in 0...nbMaxBands] {
// Length constraint for each band
bandLength[b] <- sum(bands[b], i => rawLength[i]);
constraint bandLength[b] <= rollLength;
// All items in a band must have the same width
distinctWidths[b] <- distinct(bands[b], i => rawWidth[i]);
constraint count(distinctWidths[b]) <= 1;
bandWidth[b] <-
count(bands[b]) > 0 ? max(bands[b], i => rawWidth[i]) : 0;
}
for [r in 0...nbMaxRolls] {
// The roll's width is the sum of the widths of its bands
totalWidth[r] <- sum(rolls[r], b => bandWidth[b]);
constraint totalWidth[r] <= rollWidth;
rollUsed[r] <- count(rolls[r]) > 0;
}
numberOfUsedRolls <- sum[r in 0...nbMaxRolls](rollUsed[r]);
minimize numberOfUsedRolls;
}
/* Parametrize the solver */
function param() {
if (hxTimeLimit == nil) hxTimeLimit = 10;
// Stop the search if the lower threshold is reached
hxObjectiveThreshold = minNumberOfRolls;
}
/* Write the solution */
function output() {
if (solFileName == nil) return;
local solFile = io.openWrite(solFileName);
solFile.println("Number of rolls used: ", numberOfUsedRolls.value);
solFile.println("Roll dimensions: ", rollLength, " x ", rollWidth);
solFile.println();
local rollIndex = 0;
for [r in 0...nbMaxRolls] {
if (rolls[r].value.count() == 0) continue;
solFile.println(
"Roll ",
rollIndex,
" | width: ",
totalWidth[r].value,
"/",
rollWidth
);
for [b in rolls[r].value] {
if (bands[b].value.count() == 0) continue;
solFile.print(
" Band width=",
bandWidth[b].value,
" length=",
bandLength[b].value,
" | Items: "
);
for [i in bands[b].value] solFile.print(i, " ");
solFile.println();
}
rollIndex = rollIndex + 1;
}
}
- Execution (Windows)
-
set PYTHONPATH=%HX_HOME%\bin\pythonpython cutting_stock.py instances\A1.txt
- Execution (Linux)
-
export PYTHONPATH=/opt/hexaly_15_0/bin/pythonpython cutting_stock.py instances/A1.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
import math
def read_integers(filename):
with open(filename) as f:
return [int(elem) for elem in f.read().split()]
def main(instance_file, str_time_limit, output_file):
#
# Read instance data
#
file_it = iter(read_integers(instance_file))
nb_item_types = int(next(file_it))
roll_width = int(next(file_it))
roll_length = int(next(file_it))
# Read item types and expand by demand
raw_width = []
raw_length = []
for t in range(nb_item_types):
width = int(next(file_it))
length = int(next(file_it))
demand = int(next(file_it))
for c in range(demand):
raw_width.append(width)
raw_length.append(length)
nb_items = len(raw_width)
nb_max_bands = nb_items
nb_max_rolls = nb_items
# Lower bound on total number of rolls: total item area / rollArea
total_area = sum(raw_width[i] * raw_length[i] for i in range(nb_items))
min_number_of_rolls = int(math.ceil(total_area / (roll_width * roll_length)))
with hexaly.optimizer.HexalyOptimizer() as optimizer:
#
# Declare the optimization model
#
model = optimizer.model
# Set decisions: bands[b] represents the items assigned to band b
bands = [model.set(nb_items) for _ in range(nb_max_bands)]
# Set decisions: rolls[r] represents the bands assigned to roll r
rolls = [model.set(nb_max_bands) for _ in range(nb_max_rolls)]
# Each item must be in exactly one band, and each band in exactly one roll
model.constraint(model.partition(bands))
model.constraint(model.partition(rolls))
# Create model arrays for item data
widths = model.array(raw_width)
lengths = model.array(raw_length)
band_width = [None for _ in range(nb_max_bands)]
band_length = [None for _ in range(nb_max_bands)]
for b in range(nb_max_bands):
# Length constraint for each band
band_length[b] = model.sum(
bands[b], model.lambda_function(lambda i: lengths[i])
)
model.constraint(band_length[b] <= roll_length)
# All items in a band must have the same width
distinct_widths = model.distinct(
bands[b], model.lambda_function(lambda i: widths[i])
)
model.constraint(model.count(distinct_widths) <= 1)
band_width[b] = model.iif(
model.count(bands[b]) > 0,
model.max(bands[b], model.lambda_function(lambda i: widths[i])),
0,
)
# Create array of band width expressions for roll lambdas
band_width_array = model.array(band_width)
roll_used = [None for _ in range(nb_max_rolls)]
total_width = [None for _ in range(nb_max_rolls)]
for r in range(nb_max_rolls):
# The roll's width is the sum of the widths of its bands
total_width[r] = model.sum(
rolls[r], model.lambda_function(lambda b: band_width_array[b])
)
model.constraint(total_width[r] <= roll_width)
roll_used[r] = model.count(rolls[r]) > 0
# Minimize the number of used rolls
number_of_used_rolls = model.sum(roll_used)
model.minimize(number_of_used_rolls)
model.close()
# Parameterize the optimizer
optimizer.param.time_limit = int(str_time_limit)
# Stop the search if the lower threshold is reached
optimizer.param.set_objective_threshold(0, min_number_of_rolls)
optimizer.solve()
# Write the solution in a file
if output_file is not None:
with open(output_file, "w") as f:
f.write("Number of rolls used: %d\n" % number_of_used_rolls.value)
f.write("Roll dimensions: %d x %d\n\n" % (roll_length, roll_width))
roll_index = 0
for r in range(nb_max_rolls):
if rolls[r].value.count() == 0:
continue
f.write("Roll %d | width: %d/%d\n"
% (roll_index, total_width[r].value, roll_width))
for b in rolls[r].value:
if bands[b].value.count() == 0:
continue
f.write(" Band width=%d length=%d | Items: "
% (band_width[b].value, band_length[b].value))
for i in bands[b].value:
f.write("%d " % i)
f.write("\n")
roll_index += 1
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python cutting_stock.py inputFile [output_file] [time_limit]")
sys.exit(1)
instance_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
str_time_limit = sys.argv[3] if len(sys.argv) > 3 else "10"
main(instance_file, str_time_limit, output_file)
- Compilation / Execution (Windows)
-
cl /EHsc cutting_stock.cpp -I%HX_HOME%\include /link %HX_HOME%\bin\hexaly15_0.libcutting_stock instances\A1.txt
- Compilation / Execution (Linux)
-
g++ cutting_stock.cpp -I/opt/hexaly_15_0/include -lhexaly150 -lpthread -o cutting_stock./cutting_stock instances/A1.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 <fstream>
#include <iostream>
#include <vector>
using namespace hexaly;
class CuttingStock {
private:
// Number of item types and items (expanded by demand)
int nbItemTypes;
int nbItems;
// Roll dimensions
int rollWidth;
int rollLength;
// Maximum number of bands and rolls
int nbMaxBands;
int nbMaxRolls;
// Minimum number of rolls (lower bound)
int minNumberOfRolls;
// Item dimensions (expanded by demand)
std::vector<hxint> rawWidth;
std::vector<hxint> rawLength;
// Hexaly Optimizer
HexalyOptimizer optimizer;
// Decision variables
std::vector<HxExpression> bands;
std::vector<HxExpression> rolls;
// Band expressions
std::vector<HxExpression> bandWidth;
std::vector<HxExpression> bandLength;
// Roll expressions
std::vector<HxExpression> rollUsed;
std::vector<HxExpression> totalWidth;
// Objective
HxExpression numberOfUsedRolls;
public:
/* Read instance data */
void readInstance(const std::string& fileName) {
std::ifstream infile;
infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
infile.open(fileName.c_str());
infile >> nbItemTypes;
infile >> rollWidth;
infile >> rollLength;
// Read item types and expand by demand
for (int t = 0; t < nbItemTypes; ++t) {
int width, length, demand;
infile >> width >> length >> demand;
for (int c = 0; c < demand; ++c) {
rawWidth.push_back(width);
rawLength.push_back(length);
}
}
nbItems = rawWidth.size();
nbMaxBands = nbItems;
nbMaxRolls = nbItems;
// Lower bound on total number of rolls: total item area / rollArea
double totalArea = 0;
for (int i = 0; i < nbItems; ++i) {
totalArea += (double)rawWidth[i] * rawLength[i];
}
minNumberOfRolls = (int)ceil(totalArea / ((double)rollWidth * rollLength));
}
void solve(int limit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
bands.resize(nbMaxBands);
rolls.resize(nbMaxRolls);
bandWidth.resize(nbMaxBands);
bandLength.resize(nbMaxBands);
rollUsed.resize(nbMaxRolls);
totalWidth.resize(nbMaxRolls);
// Set decisions: bands[b] represents the items assigned to band b
for (int b = 0; b < nbMaxBands; ++b) {
bands[b] = model.setVar(nbItems);
}
// Set decisions: rolls[r] represents the bands assigned to roll r
for (int r = 0; r < nbMaxRolls; ++r) {
rolls[r] = model.setVar(nbMaxBands);
}
// Each item must be in exactly one band, and each band in exactly one roll
model.constraint(model.partition(bands.begin(), bands.end()));
model.constraint(model.partition(rolls.begin(), rolls.end()));
// Create model arrays for item data
HxExpression widths = model.array(rawWidth.begin(), rawWidth.end());
HxExpression lengths = model.array(rawLength.begin(), rawLength.end());
for (int b = 0; b < nbMaxBands; ++b) {
// Length constraint for each band
bandLength[b] = model.sum(bands[b],
model.createLambdaFunction([&](HxExpression i) { return lengths[i]; }));
model.constraint(bandLength[b] <= rollLength);
// All items in a band must have the same width
HxExpression distinctWidths = model.distinct(bands[b],
model.createLambdaFunction([&](HxExpression i) { return widths[i]; }));
model.constraint(model.count(distinctWidths) <= 1);
bandWidth[b] = model.iif(
model.count(bands[b]) > 0,
model.max(bands[b],
model.createLambdaFunction([&](HxExpression i) { return widths[i]; })),
0);
}
// Create array of band width expressions for roll lambdas
HxExpression bandWidthArray = model.array(bandWidth.begin(), bandWidth.end());
for (int r = 0; r < nbMaxRolls; ++r) {
// The roll's width is the sum of the widths of its bands
totalWidth[r] = model.sum(rolls[r],
model.createLambdaFunction([&](HxExpression b) { return bandWidthArray[b]; }));
model.constraint(totalWidth[r] <= rollWidth);
rollUsed[r] = model.count(rolls[r]) > 0;
}
// Minimize the number of used rolls
numberOfUsedRolls = model.sum(rollUsed.begin(), rollUsed.end());
model.minimize(numberOfUsedRolls);
model.close();
// Parametrize the optimizer
optimizer.getParam().setTimeLimit(limit);
// Stop the search if the lower threshold is reached
optimizer.getParam().setObjectiveThreshold(0, (hxint)minNumberOfRolls);
optimizer.solve();
}
/* Write the solution in a file */
void writeSolution(const std::string& fileName) {
std::ofstream outfile;
outfile.exceptions(std::ofstream::failbit | std::ofstream::badbit);
outfile.open(fileName.c_str());
outfile << "Number of rolls used: " << numberOfUsedRolls.getValue() << std::endl;
outfile << "Roll dimensions: " << rollLength << " x " << rollWidth << std::endl;
outfile << std::endl;
int rollIndex = 0;
for (int r = 0; r < nbMaxRolls; ++r) {
HxCollection rollCollection = rolls[r].getCollectionValue();
if (rollCollection.count() == 0)
continue;
outfile << "Roll " << rollIndex << " | width: " << totalWidth[r].getValue() << "/"
<< rollWidth << std::endl;
for (int bi = 0; bi < rollCollection.count(); ++bi) {
int b = rollCollection[bi];
HxCollection bandCollection = bands[b].getCollectionValue();
if (bandCollection.count() == 0)
continue;
outfile << " Band width=" << bandWidth[b].getValue()
<< " length=" << bandLength[b].getValue() << " | Items: ";
for (int ii = 0; ii < bandCollection.count(); ++ii) {
outfile << bandCollection[ii] << " ";
}
outfile << std::endl;
}
rollIndex++;
}
}
};
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: cutting_stock inputFile [outputFile] [timeLimit]" << std::endl;
return 1;
}
const char* instanceFile = argv[1];
const char* solFile = argc > 2 ? argv[2] : NULL;
const char* strTimeLimit = argc > 3 ? argv[3] : "10";
try {
CuttingStock model;
model.readInstance(instanceFile);
model.solve(atoi(strTimeLimit));
if (solFile != NULL)
model.writeSolution(solFile);
return 0;
} catch (const std::exception& e) {
std::cerr << "An error occurred: " << e.what() << std::endl;
return 1;
}
}
- Compilation / Execution (Windows)
-
copy %HX_HOME%\bin\Hexaly.NET.dll .csc CuttingStock.cs /reference:Hexaly.NET.dllCuttingStock instances\A1.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 CuttingStock : IDisposable
{
// Number of item types and items (expanded by demand)
int nbItemTypes;
int nbItems;
// Roll dimensions
int rollWidth;
int rollLength;
// Maximum number of bands and rolls
int nbMaxBands;
int nbMaxRolls;
// Minimum number of rolls (lower bound)
int minNumberOfRolls;
// Item dimensions (expanded by demand)
long[] rawWidth;
long[] rawLength;
// Hexaly Optimizer
HexalyOptimizer optimizer;
// Decision variables
HxExpression[] bands;
HxExpression[] rolls;
// Band expressions
HxExpression[] bandWidth;
HxExpression[] bandLength;
// Roll expressions
HxExpression[] rollUsed;
HxExpression[] totalWidth;
// Objective
HxExpression numberOfUsedRolls;
public CuttingStock()
{
optimizer = new HexalyOptimizer();
}
/* Read instance data */
void ReadInstance(string fileName)
{
using (StreamReader input = new StreamReader(fileName))
{
string[] allTokens = input.ReadToEnd().Split(
new char[] { ' ', '\t', '\n', '\r' },
StringSplitOptions.RemoveEmptyEntries);
int idx = 0;
nbItemTypes = int.Parse(allTokens[idx++]);
rollWidth = int.Parse(allTokens[idx++]);
rollLength = int.Parse(allTokens[idx++]);
// Read item types and expand by demand
List<long> widthList = new List<long>();
List<long> lengthList = new List<long>();
for (int t = 0; t < nbItemTypes; ++t)
{
int width = int.Parse(allTokens[idx++]);
int length = int.Parse(allTokens[idx++]);
int demand = int.Parse(allTokens[idx++]);
for (int c = 0; c < demand; ++c)
{
widthList.Add(width);
lengthList.Add(length);
}
}
nbItems = widthList.Count;
rawWidth = widthList.ToArray();
rawLength = lengthList.ToArray();
nbMaxBands = nbItems;
nbMaxRolls = nbItems;
// Lower bound on total number of rolls: total item area / rollArea
long totalArea = 0;
for (int i = 0; i < nbItems; ++i)
totalArea += rawWidth[i] * rawLength[i];
minNumberOfRolls = (int)Math.Ceiling((double)totalArea / ((double)rollWidth * rollLength));
}
}
public void Dispose()
{
if (optimizer != null)
optimizer.Dispose();
}
void Solve(int limit)
{
// Declare the optimization model
HxModel model = optimizer.GetModel();
bands = new HxExpression[nbMaxBands];
rolls = new HxExpression[nbMaxRolls];
bandWidth = new HxExpression[nbMaxBands];
bandLength = new HxExpression[nbMaxBands];
rollUsed = new HxExpression[nbMaxRolls];
totalWidth = new HxExpression[nbMaxRolls];
// Set decisions: bands[b] represents the items assigned to band b
for (int b = 0; b < nbMaxBands; ++b)
bands[b] = model.Set(nbItems);
// Set decisions: rolls[r] represents the bands assigned to roll r
for (int r = 0; r < nbMaxRolls; ++r)
rolls[r] = model.Set(nbMaxBands);
// Each item must be in exactly one band, and each band in exactly one roll
model.Constraint(model.Partition(bands));
model.Constraint(model.Partition(rolls));
// Create model arrays for item data
HxExpression widths = model.Array(rawWidth);
HxExpression lengths = model.Array(rawLength);
for (int b = 0; b < nbMaxBands; ++b)
{
// Length constraint for each band
bandLength[b] = model.Sum(bands[b],
model.LambdaFunction(i => lengths[i]));
model.Constraint(bandLength[b] <= rollLength);
// All items in a band must have the same width
HxExpression distinctWidths = model.Distinct(bands[b],
model.LambdaFunction(i => widths[i]));
model.Constraint(model.Count(distinctWidths) <= 1);
bandWidth[b] = model.If(
model.Count(bands[b]) > 0,
model.Max(bands[b], model.LambdaFunction(i => widths[i])),
0);
}
// Create array of band width expressions for roll lambdas
HxExpression bandWidthArray = model.Array(bandWidth);
for (int r = 0; r < nbMaxRolls; ++r)
{
// The roll's width is the sum of the widths of its bands
totalWidth[r] = model.Sum(rolls[r],
model.LambdaFunction(b => bandWidthArray[b]));
model.Constraint(totalWidth[r] <= rollWidth);
rollUsed[r] = model.Count(rolls[r]) > 0;
}
// Minimize the number of used rolls
numberOfUsedRolls = model.Sum(rollUsed);
model.Minimize(numberOfUsedRolls);
model.Close();
// Parametrize the optimizer
optimizer.GetParam().SetTimeLimit(limit);
// Stop the search if the lower threshold is reached
optimizer.GetParam().SetObjectiveThreshold(0, minNumberOfRolls);
optimizer.Solve();
}
/* Write the solution in a file */
void WriteSolution(string fileName)
{
using (StreamWriter output = new StreamWriter(fileName))
{
output.WriteLine("Number of rolls used: " + numberOfUsedRolls.GetValue());
output.WriteLine("Roll dimensions: " + rollLength + " x " + rollWidth);
output.WriteLine();
int rollIndex = 0;
for (int r = 0; r < nbMaxRolls; ++r)
{
HxCollection rollCollection = rolls[r].GetCollectionValue();
if (rollCollection.Count() == 0)
continue;
output.WriteLine("Roll " + rollIndex + " | width: "
+ totalWidth[r].GetValue() + "/" + rollWidth);
for (int i = 0; i < rollCollection.Count(); ++i)
{
int b = (int)rollCollection[i];
HxCollection bandCollection = bands[b].GetCollectionValue();
if (bandCollection.Count() == 0)
continue;
output.Write(" Band width=" + bandWidth[b].GetValue()
+ " length=" + bandLength[b].GetValue() + " | Items: ");
for (int ii = 0; ii < bandCollection.Count(); ++ii)
output.Write(bandCollection[ii] + " ");
output.WriteLine();
}
rollIndex++;
}
}
}
public static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: CuttingStock inputFile [solFile] [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 (CuttingStock model = new CuttingStock())
{
model.ReadInstance(instanceFile);
model.Solve(int.Parse(strTimeLimit));
if (outputFile != null)
model.WriteSolution(outputFile);
}
}
}
- Compilation / Execution (Windows)
-
javac CuttingStock.java -cp %HX_HOME%\bin\hexaly.jarjava -cp %HX_HOME%\bin\hexaly.jar;. CuttingStock instances\A1.txt
- Compilation / Execution (Linux)
-
javac CuttingStock.java -cp /opt/hexaly_15_0/bin/hexaly.jarjava -cp /opt/hexaly_15_0/bin/hexaly.jar:. CuttingStock instances/A1.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 CuttingStock {
// Number of item types and items (expanded by demand)
private int nbItemTypes;
private int nbItems;
// Roll dimensions
private int rollWidth;
private int rollLength;
// Maximum number of bands and rolls
private int nbMaxBands;
private int nbMaxRolls;
// Minimum number of rolls (lower bound)
private int minNumberOfRolls;
// Item dimensions (expanded by demand)
private long[] rawWidth;
private long[] rawLength;
// Hexaly Optimizer
private final HexalyOptimizer optimizer;
// Decision variables
private HxExpression[] bands;
private HxExpression[] rolls;
// Band expressions
private HxExpression[] bandWidth;
private HxExpression[] bandLength;
// Roll expressions
private HxExpression[] rollUsed;
private HxExpression[] totalWidth;
// Objective
private HxExpression numberOfUsedRolls;
private CuttingStock(HexalyOptimizer optimizer) {
this.optimizer = optimizer;
}
/* Read instance data */
private void readInstance(String fileName) throws IOException {
try (Scanner input = new Scanner(new File(fileName))) {
input.useLocale(Locale.ROOT);
nbItemTypes = input.nextInt();
rollWidth = input.nextInt();
rollLength = input.nextInt();
// Read item types and expand by demand
ArrayList<Long> widthList = new ArrayList<>();
ArrayList<Long> lengthList = new ArrayList<>();
for (int t = 0; t < nbItemTypes; ++t) {
int width = input.nextInt();
int length = input.nextInt();
int demand = input.nextInt();
for (int c = 0; c < demand; ++c) {
widthList.add((long) width);
lengthList.add((long) length);
}
}
nbItems = widthList.size();
rawWidth = new long[nbItems];
rawLength = new long[nbItems];
for (int i = 0; i < nbItems; ++i) {
rawWidth[i] = widthList.get(i);
rawLength[i] = lengthList.get(i);
}
nbMaxBands = nbItems;
nbMaxRolls = nbItems;
// Lower bound on total number of rolls: total item area / rollArea
long totalArea = 0;
for (int i = 0; i < nbItems; ++i) {
totalArea += rawWidth[i] * rawLength[i];
}
minNumberOfRolls = (int) Math.ceil((double) totalArea / ((double) rollWidth * rollLength));
}
}
private void solve(int limit) {
// Declare the optimization model
HxModel model = optimizer.getModel();
bands = new HxExpression[nbMaxBands];
rolls = new HxExpression[nbMaxRolls];
bandWidth = new HxExpression[nbMaxBands];
bandLength = new HxExpression[nbMaxBands];
rollUsed = new HxExpression[nbMaxRolls];
totalWidth = new HxExpression[nbMaxRolls];
// Set decisions: bands[b] represents the items assigned to band b
for (int b = 0; b < nbMaxBands; ++b) {
bands[b] = model.setVar(nbItems);
}
// Set decisions: rolls[r] represents the bands assigned to roll r
for (int r = 0; r < nbMaxRolls; ++r) {
rolls[r] = model.setVar(nbMaxBands);
}
// Each item must be in exactly one band, and each band in exactly one roll
model.constraint(model.partition(bands));
model.constraint(model.partition(rolls));
// Create model arrays for item data
HxExpression widths = model.array(rawWidth);
HxExpression lengths = model.array(rawLength);
for (int b = 0; b < nbMaxBands; ++b) {
// Length constraint for each band
bandLength[b] = model.sum(bands[b],
model.lambdaFunction(i -> model.at(lengths, i)));
model.constraint(model.leq(bandLength[b], rollLength));
// All items in a band must have the same width
HxExpression distinctWidths = model.distinct(bands[b],
model.lambdaFunction(i -> model.at(widths, i)));
model.constraint(model.leq(model.count(distinctWidths), 1));
bandWidth[b] = model.iif(
model.gt(model.count(bands[b]), 0),
model.max(bands[b],
model.lambdaFunction(i -> model.at(widths, i))),
0);
}
// Create array of band width expressions for roll lambdas
HxExpression bandWidthArray = model.array(bandWidth);
for (int r = 0; r < nbMaxRolls; ++r) {
// The roll's width is the sum of the widths of its bands
totalWidth[r] = model.sum(rolls[r],
model.lambdaFunction(b -> model.at(bandWidthArray, b)));
model.constraint(model.leq(totalWidth[r], rollWidth));
rollUsed[r] = model.gt(model.count(rolls[r]), 0);
}
// Minimize the number of used rolls
numberOfUsedRolls = model.sum(rollUsed);
model.minimize(numberOfUsedRolls);
model.close();
// Parametrize the optimizer
optimizer.getParam().setTimeLimit(limit);
// Stop the search if the lower threshold is reached
optimizer.getParam().setObjectiveThreshold(0, minNumberOfRolls);
optimizer.solve();
}
/* Write the solution in a file */
private void writeSolution(String fileName) throws IOException {
try (PrintWriter output = new PrintWriter(fileName)) {
output.println("Number of rolls used: " + numberOfUsedRolls.getValue());
output.println("Roll dimensions: " + rollLength + " x " + rollWidth);
output.println();
int rollIndex = 0;
for (int r = 0; r < nbMaxRolls; ++r) {
HxCollection rollCollection = rolls[r].getCollectionValue();
if (rollCollection.count() == 0)
continue;
output.println("Roll " + rollIndex + " | width: "
+ totalWidth[r].getValue() + "/" + rollWidth);
for (int bi = 0; bi < rollCollection.count(); ++bi) {
int b = (int) rollCollection.get(bi);
HxCollection bandCollection = bands[b].getCollectionValue();
if (bandCollection.count() == 0)
continue;
output.print(" Band width=" + bandWidth[b].getValue()
+ " length=" + bandLength[b].getValue() + " | Items: ");
for (int ii = 0; ii < bandCollection.count(); ++ii) {
output.print(bandCollection.get(ii) + " ");
}
output.println();
}
rollIndex++;
}
}
}
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: java CuttingStock 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()) {
CuttingStock model = new CuttingStock(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);
}
}
}