Initializing help system before first use

Purchase - Definition of SOS-2


Type: Purchasing with pricebreaks
Rating: 3 (intermediate)
Description: A model for optimal purchasing with price-breaks featuring a complex MIP model, and formulation options using piecewise linear constraints (PurchasePWL) or SOS-2 (PurchaseSOS2).
File(s): PurchasePWL.cs, PurchasePWL.csproj, PurchaseSOS2.cs, PurchaseSOS2.csproj


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

using Optimizer.Objects;
using static Optimizer.Objects.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace XpressExamples
{
    /// <summary>Purchasing problem with price breaks using PieceWise Linear constraints</summary>
    /// <remarks>
    ///   There are three suppliers of a good, and they have quoted various
    ///   prices for various quantities of product.
    ///   We want to buy at least total cost, yet not buy too much from any
    ///   one supplier.
    ///
    ///   Each supplier offers decreasing prices for increased lot size, in the
    ///   form of incremental discounts.
    /// </remarks>
    class PurchasePWL
    {
        static readonly String DATAFILE = (Environment.GetEnvironmentVariable("EXAMPLE_DATA_DIR") != null ? Environment.GetEnvironmentVariable("EXAMPLE_DATA_DIR") : ".") + "/purchase.cdat";

        const int NB = 4;     /* Number of breakpoints */
        static double REQ;              /* Total quantity required */
        static int[] SUPPLIERS;         /* Suppliers */
        static double[] MAXPERC;        /* Maximum percentages for each supplier */
        static double[][] COST;          /* Industry sector of shares */
        static double[][] BREAKP;        /* Breakpoints (quantities at which unit cost changes) */

        public static void Main(string[] args)
        {
            ReadData();                     // Read data from file

            using (XpressProblem prob = new XpressProblem())
            {

                // Create the decision variables
                // Quantity to purchase from supplier s
                Variable[] buy = prob.AddVariables(SUPPLIERS.Length)
                        .WithUB(s => MAXPERC[s] * REQ / 100.0)
                        .WithName("buy {0}")
                        .ToArray();

                // Cost incurred from supplier s
                Variable[] cost = prob.AddVariables(SUPPLIERS.Length)
                        .WithName("cost {0}")
                        .ToArray();

                // The minimum quantity that must be bought
                prob.AddConstraint(Sum(buy) >= REQ);

                // Function to calculate cost at breakpoints
                double[][] COSTBREAK = new double[SUPPLIERS.Length][];
                for (int s = 0; s < SUPPLIERS.Length; s++)
                {
                    COSTBREAK[s] = new double[NB];
                    COSTBREAK[s][0] = 0;
                    for (int b = 1; b < NB; b++)
                        COSTBREAK[s][b] = COSTBREAK[s][b - 1] + COST[s][b] * (BREAKP[s][b] - BREAKP[s][b - 1]);
                }

                // Define relation between bought quantities and price paid per supplier
                for (int s = 0; s < SUPPLIERS.Length; s++)
                {
                    prob.AddConstraint(cost[s].PwlOf(buy[s], BREAKP[s], COSTBREAK[s]));
                }

                // Objective: total return
                prob.SetObjective(Sum(cost));

                // Solve the problem
                prob.MipOptimize("");

                Console.WriteLine("Problem status: " + prob.MIPStatus);
                if (prob.MIPStatus != Optimizer.MIPStatus.Solution &&
                        prob.MIPStatus != Optimizer.MIPStatus.Optimal)
                    throw new Exception("optimization failed with status " + prob.MIPStatus);

                // Solution printing
                Console.WriteLine("Total cost: " + prob.ObjVal);
                double[] sol = prob.GetSolution();
                for (int s = 0; s < SUPPLIERS.Length; s++)
                    if (buy[s].GetValue(sol) > 0.5)
                        Console.WriteLine("Supp. " + SUPPLIERS[s] + ": buy:" + buy[s].GetValue(sol) + ": cost:" + cost[s].GetValue(sol));
            }
        }

        /// <summary>Read a data vector</summary>
        /// <typeparam name="T">Data type.</typeparam>
        /// <param name="tokens">Token provider</param>
        /// <param name="makeData">Function to turn a <c>string</c> token into an instance of <c>T</c>.</param>
        /// <returns>The next vector read from <c>tokens</c>.</returns>
        private static T[] ReadVector<T>(IEnumerator<string> tokens, Func<string, T> makeData)
        {
            List<T> data = new List<T>();
            while (tokens.MoveNext())
            {
                string token = tokens.Current;
                if (token.Equals(";")) // Semicolon terminates vector
                    break;
                data.Add(makeData(token));
            }
            return data.ToArray();
        }
        /// <summary>Read a table of doubles.</summary>
        /// <remarks>
        /// Returns an <c>nrow</c> by <c>ncol</c> table of doubles.
        /// </remarks>
        /// <param name="tokens">Token provider.</param>
        /// <param name="nrow">Number of rows.</param>
        /// <param name="ncol">Number of columns.</param>
        /// <returns><c>nrow</c> by <c>ncol</c> double array.</returns>
        private static double[][] ReadTable(IEnumerator<string> tokens, int nrow, int ncol)
        {
            double[][] table = new double[nrow][];

            for (int r = 0; r < nrow; ++r)
            {
                table[r] = new double[ncol];
                table[r] = ReadVector(tokens, s => Double.Parse(s));
            }
            return table;
        }

        /// <summary>Fill the static data fields.</summary>
        private static void ReadData()
        {
            // Split the file content into tokens
            IEnumerator<string> tokens = File.ReadAllLines(DATAFILE)
                .SelectMany(s => Regex.Split(s, "\\s+")) // Split tokens at whitespace
                .SelectMany(s => (s.Length > 1 && s.EndsWith(";")) ? new string[] { s.Substring(0, s.Length - 1), ";" } : new string[] { s }) // Split comma into separate token
                .Where(s => s.Length > 0) // filter empty strings
                .GetEnumerator();
            while (tokens.MoveNext())
            {
                string token = tokens.Current;
                if (token.Equals("SUPPLIERS:"))
                {
                    SUPPLIERS = ReadVector(tokens, s => Int32.Parse(s));
                }
                else if (token.Equals("MAXPERC:"))
                {
                    MAXPERC = ReadVector(tokens, s => Double.Parse(s));
                }
                else if (token.Equals("REQ:"))
                {
                    tokens.MoveNext();
                    REQ = Double.Parse(tokens.Current);
                }
                else if (token.Equals("BREAKP:"))
                {
                    BREAKP = ReadTable(tokens, SUPPLIERS.Length, NB);
                }
                else if (token.Equals("COST:"))
                {
                    COST = ReadTable(tokens, SUPPLIERS.Length, NB);
                }
            }
        }
    }
}

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

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

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

  <ItemGroup>
    <Content Include="$(XpressExampleFiles)/purchase.cdat">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>


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

  <!-- This is for execution with "dotnet run" and friends which runs from the project directory rather than the output directory. -->
  <Target Name="CopyExampleData" AfterTargets="AfterBuild">
    <Copy SourceFiles="$(XpressExampleFiles)/purchase.cdat" DestinationFolder="$(ProjectDir)" />
  </Target>
  
</Project>

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

using Optimizer.Objects;
using static Optimizer.Objects.Utils;
using static Optimizer.Objects.SOS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text.RegularExpressions;

namespace XpressExamples
{
    /// <summary>Purchasing problem with price breaks using Special Ordered Set (SOS) type 2 constraints</summary>
    /// <remarks>
    ///   There are three suppliers of a good, and they have quoted various
    ///   prices for various quantities of product.
    ///   We want to buy at least total cost, yet not buy too much from any
    ///   one supplier.
    ///
    ///   Each supplier offers decreasing prices for increased lot size, in
    ///   the form of incremental discounts.
    /// </remarks>
    class PurchaseSOS2
    {
        static readonly String DATAFILE = (Environment.GetEnvironmentVariable("EXAMPLE_DATA_DIR") != null ? Environment.GetEnvironmentVariable("EXAMPLE_DATA_DIR") : ".") + "/purchase.cdat";

        const int NB = 4;     /* Number of breakpoints */
        static double REQ;              /* Total quantity required */
        static int[] SUPPLIERS;         /* Suppliers */
        static double[] MAXPERC;        /* Maximum percentages for each supplier */
        static double[][] COST;          /* Industry sector of shares */
        static double[][] BREAKP;        /* Breakpoints (quantities at which unit cost changes) */

        public static void Main(string[] args)
        {
            ReadData();                     // Read data from file

            using (XpressProblem prob = new XpressProblem())
            {

                // Create the decision variables
                // Quantity to purchase from supplier s
                Variable[] buy = prob.AddVariables(SUPPLIERS.Length)
                        .WithUB(s => MAXPERC[s] * REQ / 100.0)
                        .WithName("buy {0}")
                        .ToArray();

                // Weights at breakpoint k for supplier s
                Variable[][] weight = Enumerable.Range(0, SUPPLIERS.Length)
                                .Select(s => prob.AddVariables(NB)
                                          .WithName(b => $"w({s})({b})")
                                          .ToArray())
                                .ToArray();

                // The minimum quantity that must be bought
                prob.AddConstraint(Sum(buy) >= REQ);

                // Define relation between bought quantities and price paid per supplier
                for (int s = 0; s < SUPPLIERS.Length; s++)
                {
                    // The convexity row (weight sum to 1)
                    prob.AddConstraint(Sum(NB, b => weight[s][b]) == 1);

                    // Define buy and also order the weight variables by breakpoint quantities
                    prob.AddConstraint(Sum(NB, b => weight[s][b] * BREAKP[s][b]) == buy[s]);

                    // Define the weight as SOS-2
                    prob.AddConstraint(Sos2(weight[s], BREAKP[s], $"sos_{s}"));
                }

                // Function to calculate cost at breakpoints
                double[][] COSTBREAK = new double[SUPPLIERS.Length][];
                for (int s = 0; s < SUPPLIERS.Length; s++)
                {
                    COSTBREAK[s] = new double[NB];
                    COSTBREAK[s][0] = 0;
                    for (int b = 1; b < NB; b++)
                        COSTBREAK[s][b] = COSTBREAK[s][b - 1] + COST[s][b] * (BREAKP[s][b] - BREAKP[s][b - 1]);
                }

                // Objective: total return
                prob.SetObjective(Sum(NB,
                   b => Sum(SUPPLIERS.Length, s => weight[s][b] * COSTBREAK[s][b])));

                // Solve the problem
                prob.MipOptimize("");

                Console.WriteLine("Problem status: " + prob.MIPStatus);
                if (prob.MIPStatus != Optimizer.MIPStatus.Solution &&
                        prob.MIPStatus != Optimizer.MIPStatus.Optimal)
                    throw new Exception("optimization failed with status " + prob.MIPStatus);

                // Solution printing
                Console.WriteLine("Total cost: " + prob.ObjVal);
                double[] sol = prob.GetSolution();
                for (int s = 0; s < SUPPLIERS.Length; s++)
                    if (buy[s].GetValue(sol) > 0.5)
                        Console.WriteLine("Supp. " + SUPPLIERS[s] + ": buy:" + buy[s].GetValue(sol));
            }
        }

        /// <summary>Read a data vector</summary>
        /// <typeparam name="T">Data type.</typeparam>
        /// <param name="tokens">Token provider</param>
        /// <param name="makeData">Function to turn a <c>string</c> token into an instance of <c>T</c>.</param>
        /// <returns>The next vector read from <c>tokens</c>.</returns>
        private static T[] ReadVector<T>(IEnumerator<string> tokens, Func<string, T> makeData)
        {
            List<T> data = new List<T>();
            while (tokens.MoveNext())
            {
                string token = tokens.Current;
                if (token.Equals(";")) // Semicolon terminates vector
                    break;
                data.Add(makeData(token));
            }
            return data.ToArray();
        }
        /// <summary>Read a table of doubles.</summary>
        /// <remarks>
        /// Returns an <c>nrow</c> by <c>ncol</c> table of doubles.
        /// </remarks>
        /// <param name="tokens">Token provider.</param>
        /// <param name="nrow">Number of rows.</param>
        /// <param name="ncol">Number of columns.</param>
        /// <returns><c>nrow</c> by <c>ncol</c> double array.</returns>
        private static double[][] ReadTable(IEnumerator<string> tokens, int nrow, int ncol)
        {
            double[][] table = new double[nrow][];

            for (int r = 0; r < nrow; ++r)
            {
                table[r] = new double[ncol];
                table[r] = ReadVector(tokens, s => Double.Parse(s));

            }
            return table;
        }

        /// <summary>Fill the static data fields.</summary>
        private static void ReadData()
        {
            // Split the file content into tokens
            IEnumerator<string> tokens = File.ReadAllLines(DATAFILE)
                .SelectMany(s => Regex.Split(s, "\\s+")) // Split tokens at whitespace
                .SelectMany(s => (s.Length > 1 && s.EndsWith(";")) ? new string[] { s.Substring(0, s.Length - 1), ";" } : new string[] { s }) // Split comma into separate token
                .Where(s => s.Length > 0) // filter empty strings
                .GetEnumerator();
            while (tokens.MoveNext())
            {
                string token = tokens.Current;
                if (token.Equals("SUPPLIERS:"))
                {
                    SUPPLIERS = ReadVector(tokens, s => Int32.Parse(s));
                }
                else if (token.Equals("MAXPERC:"))
                {
                    MAXPERC = ReadVector(tokens, s => Double.Parse(s));
                }
                else if (token.Equals("REQ:"))
                {
                    tokens.MoveNext();
                    REQ = Double.Parse(tokens.Current);
                }
                else if (token.Equals("BREAKP:"))
                {
                    BREAKP = ReadTable(tokens, SUPPLIERS.Length, NB);
                }
                else if (token.Equals("COST:"))
                {
                    COST = ReadTable(tokens, SUPPLIERS.Length, NB);
                }
            }
        }
    }
}

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

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

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

  <ItemGroup>
    <Content Include="$(XpressExampleFiles)/purchase.cdat">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>


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

  <!-- This is for execution with "dotnet run" and friends which runs from the project directory rather than the output directory. -->
  <Target Name="CopyExampleData" AfterTargets="AfterBuild">
    <Copy SourceFiles="$(XpressExampleFiles)/purchase.cdat" DestinationFolder="$(ProjectDir)" />
  </Target>
  
</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.