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

Multi-period, multi-site production planning

Description
Multi-period production planning for multiple production facilities, including opening/closing decisions for sites. Implementation of helper routines for enumeration of arrays with multiple indices.

prodplan_dnet.zip[download all files]

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





ProductionPlanning_Index.cs

// (c) 2023-2024 Fair Isaac Corporation

using System;
using System.Linq;
using System.Collections.Generic;
using Optimizer.Maps;
using Optimizer.Objects;
using ColumnType = Optimizer.ColumnType;
using static Optimizer.Objects.Utils;
using static Optimizer.XPRSprob;
using static Optimizer.Objects.ConstantExpression;

namespace XpressExamples
{
    /// <summary>A production planning example.</summary>
    /// <remarks>
    /// <p>
    ///   There is a set F of factories at which products can be produced from
    ///   raw materials.
    /// </p>
    /// <p>
    ///   Products are sold (at a certain price) out of the factories.
    ///   Products can be produced (at a certain cost) at the factories from raw
    ///   materials. Each product has raw material requirements.
    ///   Raw materials can be bought at the factories.
    ///   Products and raw materials can be stocked.
    ///   There is an initial stock of products and raw materials.
    ///   A factory can produce only if it is open. It can buy raw material or
    ///   sell products even it it is closed.
    /// </p>
    ///
    /// <p>
    ///   <b>Objective</b>
    ///   Maximize the total profit which comprises of
    ///   <list type='bullet'>
    ///     <item><description>
    ///   the revenue from selling products,
    ///     </description></item>
    ///     <item><description>
    ///   the production cost,
    ///     </description></item>
    ///     <item><description>
    ///   the cost for keeping products on stock,
    ///     </description></item>
    ///     <item><description>
    ///   the cost for keeping raw materials on stock,
    ///     </description></item>
    ///     <item><description>
    ///   the cost for buying raw material,
    ///     </description></item>
    ///     <item><description>
    ///   the cost for keeping a factory open
    ///     </description></item>
    ///   </list>
    /// </p>
    /// </remarks>
    public class ProductionPlanning_Index
    {
        const int PMAX = 2; // number of products
        const int FMAX = 2; // number of factories
        const int RMAX = 2; // number of raw material
        const int TMAX = 4; // number of time periods


        const double CPSTOCK = 2.0; // unit cost to store a product
        const double CRSTOCK = 2.0; // unit cost to store a raw material
        const double MAXRSTOCK = 300; // maximum number of raw material that can be stocked in a factory

        static readonly double[,] REV = // REV[p, t] equals unit price for selling product p in period t
            new double[PMAX, TMAX] {
                {400, 380, 405, 350},
                {410, 397, 412, 397}};

        static readonly double[,] CMAK = // CMAK[p, f] unit cost for producing product p at factory f
            new double[PMAX, FMAX] {
                {150, 153},
                { 75,  68}};

        static readonly double[,] CBUY = // CBUY[r, t] unit cost to buy raw material r in period t
          new double[RMAX, TMAX] {
                {100,  98,  97, 100},
                {200, 195, 198, 200}};

        static readonly double[] COPEN = // COPEN[f] fixed cost for factory f being open for one period
          new double[FMAX] { 50000, 63000 };

        static readonly double[,] REQ = // REQ[p, r]      raw material requirement (in units) of r to make one unit of p
          new double[PMAX, RMAX] {
                {1.0, 0.5},
                {1.3, 0.4}};

        static readonly double[,] MAXSELL = // MAXSELL[p, t]   maximum number of units that can be sold of product p in period t
          new double[PMAX, TMAX] {
                {650, 600, 500, 400},
                {600, 500, 300, 250}};

        static readonly double[] MAXMAKE = // MAXMAKE[f]   maximum number of units (over all products) a factory can produce per period
          new double[FMAX] { 400, 500 };

        static readonly double[,] PSTOCK0 = // PSTOCK0[p, f]  initial stock of product p at factory f
            new double[PMAX, FMAX] {
                {50, 100},
                {50,  50}};

        static readonly double[,] RSTOCK0 = // RSTOCK0[r, f]  initial stock of raw material r at factor f
            new double[RMAX, FMAX] {
                {100, 150},
                { 50, 100}};

        public static void Main(string[] args)
        {
            Console.WriteLine("Formulating the production planning problem");

            using (XpressProblem prob = new XpressProblem())
            {
                // make[p, f, t]: Amount of product p to make at factory f in period t
                Variable[,,] make = prob.AddVariables(PMAX, FMAX, TMAX)
                  .WithName("make_p{0}_f{1}_t{2}")
                  .ToArray();

                // sell[p, f, t]: Amount of product p to sell from factory f in period t
                Variable[,,] sell = prob.AddVariables(PMAX, FMAX, TMAX)
                  .WithName("sell_p{0}_f{1}_t{2}")
                  .ToArray();

                // pstock[p, f, t]: Stock level of product p at factor f at start of period t
                Variable[,,] pstock = prob.AddVariables(PMAX, FMAX, TMAX)
                  .WithName("pstock_p{0}_f{1}_t{2}")
                  .ToArray();

                // buy[r, f, t]: Amount of raw material r bought for factory f in period t
                Variable[,,] buy = prob.AddVariables(RMAX, FMAX, TMAX)
                  .WithName("buy_r{0}_f{1}_t{2}")
                  .ToArray();

                // rstock[r, f, t]: Stock level of raw material r at factory f at start of period t
                Variable[,,] rstock = prob.AddVariables(RMAX, FMAX, TMAX)
                  .WithName("rstock_r{0}_f{1}_t{2}")
                  .ToArray();

                // openm[f, t]: If factory f is open in period t
                Variable[,] openm = prob.AddVariables(FMAX, TMAX)
                  .WithType(ColumnType.Binary)
                  .WithUB(1)
                  .WithName("openm_f{0}_t{1}")
                  .ToArray();



                // ## Objective:
                // Maximize total profit
                // revenue from selling products
                // + REV[p, t] * sell[p, f, t]
                LinExpression revenue = LinExpression.Create();
                foreach (Index idx in LoopThrough3D(PMAX, FMAX, TMAX))
                {
                    int p = idx.i1, f = idx.i2, t = idx.i3;
                    revenue.AddTerm(REV[p, t] * sell[p, f, t]);
                }

                // cost for making products (must be subtracted from profit)
                // - CMAK[p, f] * make[p, f, t]
                LinExpression prodCost = LinExpression.Create();
                foreach (Index idx in LoopThrough3D(PMAX, FMAX, TMAX))
                {
                    int p = idx.i1, f = idx.i2, t = idx.i3;
                    prodCost.AddTerm(-CMAK[p, f] * make[p, f, t]);
                }

                // cost for storing products (must be subtracted from profit)
                // - CPSTOCK * pstock[p, f, t]
                LinExpression prodStorageCost = LinExpression.Create();
                foreach (Index idx in LoopThrough3D(PMAX, FMAX, TMAX))
                {
                    int p = idx.i1, f = idx.i2, t = idx.i3;
                    prodStorageCost.AddTerm(-CPSTOCK * pstock[p, f, t]);
                }


                // cost for opening a factory in a time period
                // - openm[f, t] * COPEN[f]
                LinExpression factoryCost = LinExpression.Create();
                foreach (Index idx in LoopThrough2D(FMAX, TMAX))
                {
                    int f = idx.i1, t = idx.i2;
                    factoryCost.AddTerm(-COPEN[f] * openm[f, t]);
                }

                // cost for buying raw material in time period t
                // - buy[r, f, t] * CBUY[r, t]
                LinExpression rawMaterialBuyCost = LinExpression.Create();
                foreach (Index idx in LoopThrough3D(RMAX, FMAX, TMAX))
                {
                    int r = idx.i1, f = idx.i2, t = idx.i3;
                    rawMaterialBuyCost.AddTerm(-CBUY[r, t] * buy[r, f, t]);
                }

                // cost for storing raw material (must be subtracted from profit)
                // - rstock[r, f, t] * CRSTOCK
                // an alternative way of setting an objective Expression is the below nested sum expression
                // this construction does not use an index loop
                Expression rawMaterialStorageCost = Sum(FMAX,
                  f => Sum(RMAX,
                    r => Sum(TMAX, t => rstock[r, f, t] * -CRSTOCK)
                  )
                );

                // sum up the 6 individual contributions to the overall profit
                Expression profit = Sum(revenue, prodCost, prodStorageCost, factoryCost, rawMaterialStorageCost, rawMaterialBuyCost);

                // set maximization of profit as objective function
                prob.SetObjective(profit, Optimizer.ObjSense.Maximize);

                // constraints
                // Product stock balance
                foreach (Index idx in LoopThrough3D(PMAX, FMAX, TMAX))
                {
                    int p = idx.i1, f = idx.i2, t = idx.i3;

                    // for each time period except the last time period, surplus is available as stock for the next time period
                    if (t < TMAX - 1)
                    {
                        prob.AddConstraint(
                          pstock[p, f, t] + make[p, f, t] == sell[p, f, t] + pstock[p, f, t + 1])
                          .SetName($"prod_stock_balance_p{p}_f{f}_t{t}");
                    }
                    else
                    {
                        prob.AddConstraint(
                          pstock[p, f, t] + make[p, f, t] >= sell[p, f, t])
                          .SetName($"prod_stock_balance_p{p}_f{f}_t{t}");
                    }
                }

                // Raw material stock balance
                foreach (Index idx in LoopThrough3D(RMAX, FMAX, TMAX))
                {
                    int r = idx.i1, f = idx.i2, t = idx.i3;
                    if (t < TMAX - 1)
                    {
                        prob.AddConstraint(
                          rstock[r, f, t] + buy[r, f, t] == rstock[r, f, t + 1] + Sum(PMAX, p => make[p, f, t] * REQ[p, r]))
                          .SetName($"raw_material_stock_balance_r{r}_f{f}_t{t}");
                    }
                    else
                    {
                        prob.AddConstraint(
                          rstock[r, f, t] + buy[r, f, t] >= Sum(PMAX, p => make[p, f, t] * REQ[p, r]))
                          .SetName($"raw_material_stock_balance_r{r}_f{f}_t{t}");
                    }
                }

                // Limit on the amount of product p to be sold
                // exemplifies how to loop through multiple ranges
                prob.AddConstraints(PMAX, TMAX,
                  (p, t) =>
                    (Sum(FMAX, f => sell[p, f, t]) <= MAXSELL[p, t])
                    .SetName($"maxsell_p{p}_t{t}")
                );


                // Capacity limit at factory f
                // exemplifies how to loop through multiple ranges
                prob.AddConstraints(FMAX, TMAX,
                  (f, t) =>
                    (Sum(PMAX, p => make[p, f, t]) <= openm[f, t].Mul(MAXMAKE[f]))
                    .SetName($"capacity_f{f}_t{t}")
                );

                // Raw material stock limit
                // exemplifies how to loop through multiple ranges
                prob.AddConstraints(FMAX, TMAX,
                  (f, t) =>
                    (Sum(RMAX, r => rstock[r, f, t]) <= MAXRSTOCK)
                    .SetName($"raw_material_stock_limit_f{f}_t{t}")
                );

                // Initial product storage
                // loop through a custom IEnumerable, in our case our custom index object.
                prob.AddConstraints(LoopThrough2D(PMAX, FMAX),
                  idx =>
                    // pstock is indexed (p, f, t), PSTOCK0 is indexed (p, f)
                    (pstock[idx.i1, idx.i2, 0] == PSTOCK0[idx.i1, idx.i2])
                    .SetName($"initial_product_stock_p{idx.i1}_f{idx.i2}")
                );

                // Initial raw material storage
                // classic for loop
                foreach (Index idx in LoopThrough2D(RMAX, FMAX))
                {
                    int r = idx.i1, f = idx.i2;
                    prob.AddConstraint((rstock[r, f, 0] == RSTOCK0[r, f])
                    .SetName($"initial_raw_material_stock_r{r}_f{f}"));
                }

                // write the problem in LP format for manual inspection
                Console.WriteLine("Writing the problem to 'ProductionPlanning.lp'");
                prob.WriteProb("ProductionPlanning.lp", "l");

                // Solve the problem
                Console.WriteLine("Solving the problem");
                prob.Optimize();

                Console.WriteLine("Problem finished with SolStatus {0}", prob.SolStatus);

                if (prob.SolStatus != Optimizer.SolStatus.Optimal)
                {
                    throw new Exception("Problem not solved to optimality");
                }

                Console.WriteLine("Solution has objective value (profit) of {0}", prob.MIPObjVal);

                Console.WriteLine("");
                Console.WriteLine("*** Solution ***");
                double[] sol = prob.GetSolution();

                // Is factory f open at time period t?
                for (int f = 0; f < FMAX; f++)
                {
                    for (int t = 0; t < TMAX; t++)
                    {
                        Console.WriteLine($"Factory {f}\tTime Period {t} : Open = {openm[f, t].GetValue(sol)}");
                    }
                }
                Console.WriteLine("");

                // Production plan for producing
                WriteSol3D(sol, make, PMAX, FMAX, TMAX, new string[] { "Product", "Factory", "Time Period" }, "Make");
                Console.WriteLine("");

                // Production plan for selling products
                WriteSol3D(sol, sell, PMAX, FMAX, TMAX, new string[] { "Product", "Factory", "Time Period" }, "Sell");
                Console.WriteLine("");

                // Production plan for keeping products in stock
                WriteSol3D(sol, pstock, PMAX, FMAX, TMAX, new string[] { "Product", "Factory", "Time Period" }, "Pstock");
                Console.WriteLine("");

                // Production plan for keeping raw material in stock
                WriteSol3D(sol, rstock, RMAX, FMAX, TMAX, new string[] { "Material", "Factory", "Time Period" }, "Rstock");
                Console.WriteLine("");

                // Buying plan for raw material
                WriteSol3D(sol, buy, RMAX, FMAX, TMAX, new string[] { "Material", "Factory", "Time Period" }, "Buy");
                Console.WriteLine("");
            }
        }

        /// <summary>Helper class for convenient index iteration</summary>
        public sealed class Index
        {
            public readonly int i1;
            public readonly int i2;
            public readonly int i3;

            public Index(int i1, int i2, int i3)
            {
                this.i1 = i1;
                this.i2 = i2;
                this.i3 = i3;
            }
            public override string ToString() { return $"Index {this.i1} {this.i2} {this.i3}"; }
        }
        /// <summary>
        /// Loop through a 3-dimensional range, always starting at 0.
        /// </summary>
        /// <param name="max1">First index varies between 0 and max1</param>
        /// <param name="max2">Second index varies between 0 and max2</param>
        /// <param name="max3">Third index varies between 0 and max3</param>
        /// <returns>an Enumerable that can be used to loop over the cross product of the three ranges</returns>
        public static IEnumerable<Index> LoopThrough3D(int max1, int max2, int max3)
        {
            for (int i1 = 0; i1 < max1; i1++)
            {
                for (int i2 = 0; i2 < max2; i2++)
                {
                    if (max3 > 0)
                    {
                        for (int i3 = 0; i3 < max3; i3++)
                        {
                            Index idx = new Index(i1, i2, i3);
                            yield return idx;
                        }
                    }
                    else
                    {
                        yield return new Index(i1, i2, -1);
                    }
                }
            }
        }
        /// <summary>
        /// Loop through a 2-dimensional range, starting at 0
        /// </summary>
        /// <param name="max1">First index varies between 0 and max1</param>
        /// <param name="max2">Second index varies between 0 and max2</param>
        /// <returns>an Enumerable that can be used to loop over the cross product of the two ranges. The third
        /// entry of the returned Index object will always be -1</returns>
        public static IEnumerable<Index> LoopThrough2D(int max1, int max2)
        {
            return LoopThrough3D(max1, max2, 0);
        }

        /// <summary>
        /// Convenience function for printing solution values stored in a 3-dimensional Variable array
        /// </summary>
        /// <param name="sol">solution object, obtained via prob.getSolution()</param>
        /// <param name="array">3-dimensional array of Xpress Variables</param>
        /// <param name="max1">First index varies between 0 and max1</param>
        /// <param name="max2">Second index varies between 0 and max2</param>
        /// <param name="max3">Third index varies between 0 and max3</param>
        /// <param name="dimNames">An array with a name for every dimension</param>
        /// <param name="name">The name of the array</param>
        public static void WriteSol3D(double[] sol, Variable[,,] array, int max1, int max2, int max3, string[] dimNames, string name)
        {
            foreach (Index idx in LoopThrough3D(max1, max2, max3))
            {
                int i1 = idx.i1, i2 = idx.i2, i3 = idx.i3;
                Console.WriteLine($"{dimNames[0]} {i1}\t{dimNames[1]} {i2}\t{dimNames[2]} {i3} : {name} = {array[i1, i2, i3].GetValue(sol)}");
            }
        }

    }
}

Back to examples browserPrevious exampleNext example