Initializing help system before first use

Multi-period, multi-site production planning


Type: Production planning
Rating: 3 (intermediate)
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.
File(s): ProductionPlanning_Index.cs, ProductionPlanning_Index.csproj


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)}");
            }
        }

    }
}

ProductionPlanning_Index.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.