Initializing help system before first use

Cutstk - Column generation for a cutting stock problem


Type: Column generation
Rating: 4 (medium-difficult)
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.
File(s): CuttingStock.cs, CuttingStock.csproj


CuttingStock.cs
// (c) 2023-2024 Fair Isaac Corporation

using System;
using System.Linq;
using Optimizer;
using Optimizer.Objects;
using static Optimizer.Objects.Utils;
using static Optimizer.Objects.ConstantExpression;

namespace XpressExamples
{
    /// <summary>Cutting stock problem</summary>
    /// <remarks>
    ///   Solved by column (= cuttingpattern) generation heuristic looping
    ///   over the root node.
    /// </remarks>
    internal class CuttingStock
    {
        const int NWIDTHS = 5;
        const int MAXWIDTH = 94;
        const double EPS = 1e-6;
        const int MAXCOL = 10;

        /****Data****/
        static readonly double[] width = { 17, 21, 22.5, 24, 29.5 }; /* Possible widths */
        static readonly int[] demand = { 150, 96, 48, 108, 227 };  /* Demand per width */

        /****Shared Variables****/
        static Variable[] vPattern; /* Rolls per pattern */
        static Inequality[] cDemand; /* Demand constraints */
        static LinTermMap eObj; /* Objective function */

        private static void PrintProblemSolution(XpressProblem prob)
        {
            double[] sol = prob.GetSolution();
            Console.WriteLine("Objective: " + prob.ObjVal);
            Console.WriteLine("Variables:");
            Console.Write("\t");
            foreach (Variable v in prob.GetVariables())
            {
                Console.Write("[{0}: {1:f2}] ", v.GetName(), v.GetValue(sol));
            }
            Console.WriteLine();
        }


        /**************************************************************************/
        /* 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                                  */
        /**************************************************************************/
        private static double SolveKnapsack(int N, double[] c, double[] a, double R, int[] d, int[] xbest)
        {
            using (XpressProblem prob = new XpressProblem())
            {
                Variable[] x = prob.AddVariables(N)
                    .WithType(ColumnType.Integer)
                    .WithName(i => $"x_{i}")
                    .WithLB(i => d[i])
                    .ToArray();

                prob.AddConstraint(
                    ScalarProduct(x, a) <= R
                );

                prob.SetObjective(
                    ScalarProduct(x, c),
                    Optimizer.ObjSense.Maximize
                );

                prob.MipOptimize();
                double zbest = prob.ObjVal;
                double[] sol = prob.GetSolution();
                xbest = sol.Select(value => (int)Math.Round(value)).ToArray();

                return zbest;
            }
        }

        private static void ModCutStock(XpressProblem p)
        {
            // (Basic) cutting patterns
            int[,] patterns = new int[NWIDTHS, NWIDTHS];
            foreach (int j in Enumerable.Range(0, NWIDTHS))
            {
                patterns[j, j] = (int)Math.Floor((double)MAXWIDTH / width[j]);
            }

            vPattern = p.AddVariables(NWIDTHS)
                .WithUB(i => Math.Ceiling((double)demand[i] / patterns[i, i]))
                .WithName(i => $"pat_{i + 1}")
                .ToArray();

            // Minimize total number of rolls
            p.SetObjective(Sum(vPattern));


            // Satisfy the demand per width;
            // Sum of patterns must exceed demand
            cDemand = p.AddConstraints(NWIDTHS,
                    i => (Sum(NWIDTHS, j => vPattern[j] * patterns[i, j]) >= demand[i]).SetName($"Demand_{i}")).ToArray();
        }

        /**************************************************************************/
        /*  Column generation loop at the top node:                               */
        /*    solve the LP and save the basis                                     */
        /*    get the solution values                                             */
        /*    generate new column(s) (=cutting pattern)                           */
        /*    load the modified problem and load the saved basis                  */
        /**************************************************************************/
        private static void SolveCutStock(XpressProblem p)
        {
            // Basis information
            int[] rowstat = new int[p.Rows];
            int[] colstat = new int[p.Cols];
            int[] x = new int[NWIDTHS];
            long starttime = Environment.TickCount64;

            foreach (int npass in Enumerable.Range(0, MAXCOL))
            {
                p.LpOptimize();                // Solve the LP
                if (p.SolStatus != Optimizer.SolStatus.Optimal)
                    throw new Exception("failed to optimize with status " + p.SolStatus);
                p.GetBasis(rowstat, colstat);  // Save the current basis

                // solve integer knpsack problem
                double z = SolveKnapsack(NWIDTHS, p.GetDuals(cDemand), width, MAXWIDTH, demand, x);

                Console.WriteLine("Iteration {0:d}, {1:f2} sec",
                    npass + 1, (Environment.TickCount64 - starttime) / 1000.0
                );

                if (z < 1 + EPS)
                {
                    Console.WriteLine("No profitable column found.");
                    Console.WriteLine();
                }
                else
                {
                    Console.WriteLine("New pattern found with marginal cost {0}", z - 1);
                    // Create a new variable for this pattern:
                    Variable v = p.AddVariable(
                        0.0,
                        Double.PositiveInfinity,
                        ColumnType.Integer,
                        String.Format("pat_{0:d}", p.Cols + 1)
                    );
                    // Add new variable to the objective
                    v.ChgObj(1.0);
                    // Add new variable to demand constraints
                    double ub = 0.0;
                    for (int i = 0; i < NWIDTHS; ++i)
                    {
                        if (x[i] > 0)
                        {
                            cDemand[i].ChgCoef(v, x[i]);
                            ub = Math.Max(ub, Math.Ceiling(demand[i] / (double)x[i]));
                        }
                    }
                    // Change the upper bound on the new variable
                    v.SetUB(ub);
                    p.LoadBasis(rowstat, colstat);
                }
            }

            p.MipOptimize();
            if (p.SolStatus != Optimizer.SolStatus.Optimal)
                throw new Exception("failed to optimize with status " + p.SolStatus);
            PrintProblemSolution(p);
        }

        public static void Main(string[] args)
        {
            using (XpressProblem prob = new XpressProblem())
            {
                // Output all messages.
                prob.callbacks.AddMessageCallback(DefaultMessageListener.Console);
                prob.OutputLog = 0;
                prob.MIPLog = 0;

                ModCutStock(prob);
                SolveCutStock(prob);
            }
        }
    }
}

CuttingStock.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>

    <IsPackable>false</IsPackable>
    <XpressExampleFiles Condition="'$(XpressExampleFiles)'==''">../../../data</XpressExampleFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="FICO.Xpress.XPRSdn" Version="41.1.1" /> <!-- Version 41.1.1 or later -->
  </ItemGroup>
  
</Project>

© 2001-2024 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.