FICO
FICO Xpress Optimization Examples Repository
FICO Optimization Community FICO Xpress Optimization Home
Back to examples browserPrevious exampleNext example

Cutstk - Column generation for a cutting stock problem

Description
This example features iteratively adding new variables, basis in/output and working with subproblems. The column (=cutting pattern) generation algorithm is implemented as a loop over the root node of the MIP problem.

cutstk_cpp.zip[download all files]

Source Files
By clicking on a file name, a preview is opened at the bottom of this page.
CuttingStock.cpp[download]





CuttingStock.cpp

#include <xpress.hpp>
#include <stdexcept>  // For throwing exceptions

using namespace xpress;
using namespace xpress::objects;
using xpress::objects::utils::scalarProduct;

/**
 * Cutting stock problem. The problem is solved by column (= cutting pattern)
 * generation heuristic looping over the root node.
 */

class CuttingStock {
public:
    const std::vector<double> width = { 17, 21, 22.5, 24, 29.5 }; // Possible widths
    const std::vector<int> demand = { 150, 96, 48, 108, 227 };    // Demand per width

    XpressProblem& prob;

    std::vector<Variable> patternVars;         // Nr of rolls made with vPattern[i]
    std::vector<Inequality> demandConstraints; // 5 demand constraints
    LinTermMap objExpr;                        // Objective function

    CuttingStock(XpressProblem& prob) : prob(prob) {}; // Class constructor
    void printSolution();     // To print the solution to console
    void modelCutStock();     // Initial problem definition with few variables
    void solveCutStock();     // Iteratively adds variables (i.e. columns) until optimality

private:
    const int NWIDTHS  = 5;    // Number of unique widths to produce
    const int MAXWIDTH = 94;   // Length of each roll / raw material
    const int MAXCOL   = 10;   // Stop after generating 10 extra columns
    const double EPS   = 1e-6; // Epsilon, for some inprecision tolerance
};

void CuttingStock::printSolution() {
    std::vector<double> sol = prob.getSolution();
    std::cout << "Objective Value: " << prob.attributes.getObjVal() << std::endl;
    std::cout << "Variables:" << std::endl << "\t";
    std::ios_base::fmtflags oldFlags(std::cout.flags());
    std::cout << std::fixed << std::setprecision(2);
    for (Variable v : prob.getVariables()) {

      std::cout << "[" << v.getName() << ": " << v.getValue(sol) << "]";
    }
    std::cout.flags(oldFlags);
    std::cout << std::endl;
}

/* ************************************************************************ */
/*  Integer Knapsack Algorithm for solving the integer knapsack problem     */
/*     z = max{cx : ax <= R, x <= d, x in Z^N}                              */
/*                                                                          */
/*  Input data:                                                             */
/*    N:        Number of item types                                        */
/*    c[i]:     Unit profit of item type i, i=1..n                          */
/*    a[i]:     Unit resource use of item type i, i=1..n                    */
/*    R:        Total resource available                                    */
/*    d[i]:     Demand for item type i, i=1..n                              */
/*  Return values:                                                          */
/*    xbest[i]: Number of items of type i used in optimal solution          */
/*    zbest:    Value of optimal solution                                   */
/* ************************************************************************ */
double solveKnapsack(int N,    std::vector<double> c, std::vector<double> a,
                     double R, std::vector<int> d,    std::vector<int>& xbest) {

    XpressProblem subProb;
    std::vector<Variable> x = subProb.addVariables(N) // Variable for each unique width to produce
                                     .withType(ColumnType::Integer)
                                     .withName("x_%d")
                                     .withUB([&](int i) { return double(d[i]); })
                                     .toArray();

    subProb.addConstraint(scalarProduct(x, a) <= R);

    subProb.setObjective(scalarProduct(x, c), ObjSense::Maximize);

    subProb.optimize();
    // Check the solution status
    if (subProb.attributes.getSolStatus() != SolStatus::Optimal && subProb.attributes.getSolStatus() != SolStatus::Feasible) {
        std::ostringstream oss; oss << subProb.attributes.getSolStatus(); // Convert xpress::SolStatus to String
        throw std::runtime_error("Optimization of subproblem failed with status " + oss.str());
    }

    double zbest = subProb.attributes.getObjVal();
    std::vector<double> sol = subProb.getSolution();

    // Return values
    for (int i=0; i<N; i++) xbest[i] = int (std::round(x[i].getValue(sol)));
    return zbest;
}

// Function to initially model the cutting stock problem
void CuttingStock::modelCutStock() {
    // Generate NWIDTHS initial basic patterns. The basic patterns only use one unique width
    // i.e. (1,0,0,0,0), (0,2,0,0,0), (0,0,6,0,0), etc.
    int nrInitialPatterns = NWIDTHS;
    std::vector<std::vector<int>> patterns(NWIDTHS, std::vector<int>(nrInitialPatterns));
    for (int w=0; w<NWIDTHS; w++) {
        // Here we generate the initial trivial patterns.
        patterns[w][w] = int(std::floor(double(MAXWIDTH) / width[w]));
    }

    patternVars = prob.addVariables(nrInitialPatterns)
                      .withType(ColumnType::Integer)
                      .withLB(0)
                      .withUB([&](int p) {return std::ceil( double(demand[p]) / patterns[p][p] ); })
                      .withName([&](int p) {return xpress::format("pat_%d", p + 1); })
                      .toArray();

    // Minimize total number of rolls
    std::for_each(patternVars.begin(), patternVars.end(), [&](Variable pattern) { objExpr.addTerm(pattern); });
    prob.setObjective(objExpr);

    // Satisfy the demand per width;
    // Sum of patterns producing width[w] must exceed demand[w]. The patterns p producing the
    // width[w] are exactly those p for which patterns[w][p] is nonzero, so we can use scalarProduct
    xpress::Iterable<Inequality> constraintsIter = prob.addConstraints(NWIDTHS, [&](int w) {
        return (scalarProduct(patternVars, patterns[w]) >= demand[w]).setName("Demand_" + std::to_string(w));
    });
    // Save the Inequality objects for access later
    demandConstraints = std::vector<Inequality>(constraintsIter.begin(), constraintsIter.end());
}


/* ************************************************************************ */
/*   Column generation loop at the top node:                                */
/*     solve the LP-relaxation and save the basis                           */
/*     generate new column(s) (=cutting pattern)                            */
/*     add the new column to the master problem                             */
/*     load the modified problem and load the saved basis                   */
/* ************************************************************************ */
void CuttingStock::solveCutStock() {
     // Variable we will contain the return value of the knapsack subproblem solution's x-variables
    std::vector<int> subProbSolX(NWIDTHS);

    std::cout << "Generating columns" << std::endl;
    for (int c=0; c<MAXCOL; c++) {

        // Solve the LP-relaxation of the problem. (This is so we can get access to dual values,
        // as for MIPs dual values do not exist)
        prob.lpOptimize();

        // Check the solution status
        if (prob.attributes.getSolStatus() != SolStatus::Optimal && prob.attributes.getSolStatus() != SolStatus::Feasible) {
            std::ostringstream oss; oss << prob.attributes.getSolStatus(); // Convert xpress::SolStatus to String
            throw std::runtime_error("Optimization failed with status " + oss.str());
        }

        // Save the current basis (so we can reload it for faster resolve)
        std::vector<int> rowstat(prob.attributes.getRows());
        std::vector<int> colstat(prob.attributes.getCols());
        prob.getBasis(rowstat, colstat);

        // Solve subproblem
        double z = solveKnapsack(NWIDTHS, prob.getDuals(demandConstraints), width, MAXWIDTH, demand, subProbSolX);

        if (z < 1 + EPS) {
            std::cout << "  No profitable column found." << std::endl;
            break;
        } else {
            std::cout << "  New pattern found with marginal cost " << z - 1 << std::endl;
            // Create a new variable for this pattern:
            Variable new_var = prob.addVariable(ColumnType::Integer, xpress::format("pat_%d", prob.attributes.getCols() + 1));

            // Add the basis-status of the new variable to colstat. The status=0 means non-basic
            // The new variable is non-basic since it has value 0 (i.e. pattern is unused so far)
            colstat.push_back(0);

            // Add new var. to the objective
            new_var.chgObj(1.0);
            // Add new var. to demand constraints
            for (int i = 0; i < NWIDTHS; ++i) {
                if (subProbSolX[i] > 0) {
                    demandConstraints[i].chgCoef(new_var, subProbSolX[i]);
                }
            }

            // Load back the previous optimal basis for faster resolve
            prob.loadBasis(rowstat, colstat);
        }
    }

    // Write the problem to .lp file
    prob.writeProb("CuttingStock.lp", "l");

    // After adding all the columns to the LP-relaxation, now solve the MIP
    std::cout << "Solving the final MIP problem" << std::endl;
    prob.mipOptimize();

    // Check the solution status
    if (prob.attributes.getSolStatus() != SolStatus::Optimal && prob.attributes.getSolStatus() != SolStatus::Feasible) {
        std::ostringstream oss; oss << prob.attributes.getSolStatus(); // Convert xpress::SolStatus to String
        throw std::runtime_error("Optimization failed with status " + oss.str());
    }
}


int main() {
    try {
        // Create a problem instance
        XpressProblem prob;

        // Output all messages
        prob.callbacks.addMessageCallback(XpressProblem::console);
        prob.controls.setOutputLog(0); // Disable outputlog

        // Model, Solve, and Print the cutting stock problem
        CuttingStock cutStockProb = CuttingStock(prob);
        cutStockProb.modelCutStock();
        cutStockProb.solveCutStock();
        cutStockProb.printSolution();
        return 0;
    }
    catch (std::exception& e) {
        std::cout << "Exception: " << e.what() << std::endl;
        return -1;
    }
}

Back to examples browserPrevious exampleNext example