| |||||||||||||
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.
Source Files By clicking on a file name, a preview is opened at the bottom of this page.
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)}"); } } } } | |||||||||||||
© Copyright 2024 Fair Isaac Corporation. |