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.cpp, PurchaseSOS2.cpp


PurchasePWL.cpp
// (c) 2024-2024 Fair Isaac Corporation

/**
 * Purchasing problem with price breaks using PieceWise Linear constraints.
 * 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.
 */

#include <xpress.hpp>

using namespace xpress;
using namespace xpress::objects;
using xpress::objects::utils::sum;

void readData();

/** The file from which data for this example is read. */
char const *const DATAFILE = "purchase.cdat";

int const NB = 4;            /* Number of breakpoints */
double REQ;                  /* Total quantity required */
std::vector<int> SUPPLIERS;  /* Suppliers */
std::vector<double> MAXPERC; /* Maximum percentages for each supplier */
std::vector<std::vector<double>> COST; /* Unit cost */
std::vector<std::vector<double>>
    BREAKP; /* Breakpoints (quantities at which unit cost changes) */

int main() {
  readData(); // Read data from file

  XpressProblem prob;

  // Create the decision variables
  // Quantity to purchase from supplier s
  std::vector<Variable> buy =
      prob.addVariables(SUPPLIERS.size())
          .withUB([&](auto i) { return MAXPERC[i] * REQ / 100.0; })
          .withName("buy %d")
          .toArray();

  // Cost incurred from supplier s
  std::vector<Variable> cost =
      prob.addVariables(SUPPLIERS.size())
          .withName("cost %d")
          .toArray();

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

  // Function to calculate cost at breakpoints
  std::vector<std::vector<double>> COSTBREAK(SUPPLIERS.size(),
                                             std::vector<double>(NB));
  for (unsigned s = 0; s < SUPPLIERS.size(); s++) {
    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 (unsigned s = 0; s < SUPPLIERS.size(); s++) {
    prob.addConstraint(cost[s].pwlOf(buy[s], BREAKP[s], COSTBREAK[s]));
  }

  // Objective: sum of costs
  prob.setObjective(sum(cost));

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

  std::cout << "Problem status: " << prob.attributes.getMipStatus()
            << std::endl;
  if (prob.attributes.getMipStatus() != MIPStatus::Solution &&
      prob.attributes.getMipStatus() != MIPStatus::Optimal)
    throw std::runtime_error("optimization failed with status " +
                             to_string(prob.attributes.getMipStatus()));

  // Solution printing
  std::cout << "Total cost: " << prob.attributes.getObjVal() << std::endl;
  auto sol = prob.getSolution();
  for (unsigned s = 0; s < SUPPLIERS.size(); s++)
    std::cout << "Supp. " << SUPPLIERS[s] << ": buy:" << buy[s].getValue(sol)
              << ": cost:" << cost[s].getValue(sol) << std::endl;

  return 0;
}

// Minimalistic data parsing.
#include <fstream>
#include <iterator>

/**
 * Read a list of strings. Iterates <code>it</code> until a semicolon is
 * encountered or the iterator ends.
 *
 * @param it The token sequence to read.
 * @param conv  Function that converts a string to <code>T</code>.
 * @return A vector of all tokens before the first semicolon.
 */
template <typename T>
std::vector<T> readStrings(std::istream_iterator<std::string> &it,
                           std::function<T(std::string const &)> conv) {
  std::vector<T> result;
  while (it != std::istream_iterator<std::string>()) {
    std::string token = *it++;
    if (token.size() > 0 && token[token.size() - 1] == ';') {
      if (token.size() > 1) {
        result.push_back(conv(token.substr(0, token.size() - 1)));
      }
      break;
    } else {
      result.push_back(conv(token));
    }
  }
  return result;
}

/**
 * Read a table of doubles. Allocates a <code>nrow</code> by <code>ncol</code>
 * double table and fills it by the sparse data from the token sequence.
 * <code>it</code> is assumed to hold <code>nrow</code> sequences of
 * indices, each of which is terminated by a semicolon. The indices in those
 * vectors specify the entries in the corresponding row of the
 * table.
 *
 * @tparam R     Type for row count.
 * @paraam C     Type for column count.
 * @param it     Token sequence.
 * @param nrow   Number of rows in the table.
 * @param ncol   Number of columns in the table.
 * @return The double table.
 */
template<typename R,typename C>
std::vector<std::vector<double>>
readTable(std::istream_iterator<std::string> &it, R nrow, C ncol) {
  std::vector<std::vector<double>> tbl(nrow);
  for (R r = 0; r < nrow; r++) {
    tbl[r] = readStrings<double>(it, [](auto &s) { return std::stod(s); });
    if (static_cast<C>(tbl[r].size()) != ncol)
      throw std::runtime_error("not enough columns in file");
  }
  return tbl;
}

void readData() {
  std::string dataDir("../../data");
#ifdef _WIN32
  size_t len;
  char buffer[1024];
  if ( !getenv_s(&len, buffer, sizeof(buffer), "EXAMPLE_DATA_DIR") &&
       len && len < sizeof(buffer) )
    dataDir = buffer;
#else
  char const *envDir = std::getenv("EXAMPLE_DATA_DIR");
  if (envDir)
    dataDir = envDir;
#endif
  std::string dataFile = dataDir + "/" + DATAFILE;
  std::ifstream ifs(dataFile);
  if (!ifs)
    throw std::runtime_error("Could not open " + dataFile);
  std::stringstream data(std::string((std::istreambuf_iterator<char>(ifs)),
                                     (std::istreambuf_iterator<char>())));
  std::istream_iterator<std::string> it(data);
  while (it != std::istream_iterator<std::string>()) {
    std::string token = *it++;
    if (token == "SUPPLIERS:")
      SUPPLIERS = readStrings<int>(it, [](auto &s) { return std::stoi(s); });
    else if (token == "MAXPERC:")
      MAXPERC = readStrings<double>(it, [](auto &s) { return std::stod(s); });
    else if (token == "REQ:")
      REQ = std::stod(*it++);
    else if (token == "BREAKP:")
      BREAKP = readTable(it, SUPPLIERS.size(), NB);
    else if (token == "COST:")
      COST = readTable(it, SUPPLIERS.size(), NB);
  }
}

PurchaseSOS2.cpp
// (c) 2024-2024 Fair Isaac Corporation

/**
 * Purchasing problem with price breaks using Special Ordered Set (SOS) type 2
 * constraints. 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.
 */
#include <xpress.hpp>

using namespace xpress;
using namespace xpress::objects;
using xpress::objects::utils::scalarProduct;
using xpress::objects::utils::sum;

void readData();

/** The file from which data for this example is read. */
char const *const DATAFILE = "purchase.cdat";

int const NB = 4;            /* Number of breakpoints */
double REQ;                  /* Total quantity required */
std::vector<int> SUPPLIERS;  /* Suppliers */
std::vector<double> MAXPERC; /* Maximum percentages for each supplier */
std::vector<std::vector<double>> COST; /* Unit cost */
std::vector<std::vector<double>>
    BREAKP; /* Breakpoints (quantities at which unit cost changes) */

int main() {
  readData(); // Read data from file

  XpressProblem prob;

  // Create the decision variables
  // Quantity to purchase from supplier s
  std::vector<Variable> buy =
      prob.addVariables(SUPPLIERS.size())
          .withUB([&](auto i) { return MAXPERC[i] * REQ / 100.0; })
          .withName("buy %d")
          .toArray();

  // Weights at breakpoint k for supplier s
  std::vector<std::vector<Variable>> weight =
      prob.addVariables(SUPPLIERS.size(), NB)
          .withName("weight[%d,%d]")
          .toArray();

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

  // Define relation between bought quantities and price paid per supplier
  for (unsigned s = 0; s < SUPPLIERS.size(); s++) {
    // The convexity row (weight sum to 1)
    prob.addConstraint(sum(weight[s]) == 1);

    // Define buy and also order the weight variables by breakpoint quantities
    prob.addConstraint(scalarProduct(weight[s], BREAKP[s]) == buy[s]);

    // Define the weight as SOS-2
    prob.addConstraint(SOS::sos2(weight[s], BREAKP[s],
                                 "SOS 2 Constraint no. " + std::to_string(s)));
  }

  // Function to calculate cost at breakpoints
  std::vector<std::vector<double>> COSTBREAK(SUPPLIERS.size(),
                                             std::vector<double>(NB));
  for (unsigned s = 0; s < SUPPLIERS.size(); s++) {
    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: sum of costs*weights
  prob.setObjective(sum(NB, [&](auto b) {
    return sum(SUPPLIERS.size(),
               [&](auto s) { return weight[s][b] * COSTBREAK[s][b]; });
  }));

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

  std::cout << "Problem status: " << prob.attributes.getMipStatus()
            << std::endl;
  if (prob.attributes.getMipStatus() != MIPStatus::Solution &&
      prob.attributes.getMipStatus() != MIPStatus::Optimal)
    throw std::runtime_error("optimization failed with status " +
                             to_string(prob.attributes.getMipStatus()));

  // Solution printing
  std::cout << "Total cost: " << prob.attributes.getObjVal() << std::endl;
  auto sol = prob.getSolution();
  for (unsigned s = 0; s < SUPPLIERS.size(); s++)
    std::cout << "Supp. " << SUPPLIERS[s] << ": buy:" << buy[s].getValue(sol)
              << std::endl;
  return 0;
}

// Minimalistic data parsing.
#include <fstream>
#include <iterator>

/**
 * Read a list of strings. Iterates <code>it</code> until a semicolon is
 * encountered or the iterator ends.
 *
 * @param it The token sequence to read.
 * @param conv  Function that converts a string to <code>T</code>.
 * @return A vector of all tokens before the first semicolon.
 */
template <typename T>
std::vector<T> readStrings(std::istream_iterator<std::string> &it,
                           std::function<T(std::string const &)> conv) {
  std::vector<T> result;
  while (it != std::istream_iterator<std::string>()) {
    std::string token = *it++;
    if (token.size() > 0 && token[token.size() - 1] == ';') {
      if (token.size() > 1) {
        result.push_back(conv(token.substr(0, token.size() - 1)));
      }
      break;
    } else {
      result.push_back(conv(token));
    }
  }
  return result;
}

/**
 * Read a table of doubles. Allocates a <code>nrow</code> by <code>ncol</code>
 * double table and fills it by the sparse data from the token sequence.
 * <code>it</code> is assumed to hold <code>nrow</code> sequences of
 * indices, each of which is terminated by a semicolon. The indices in those
 * vectors specify the entries in the corresponding row of the
 * table.
 *
 * @tparam R     Type for row count.
 * @paraam C     Type for column count.
 * @param it     Token sequence.
 * @param nrow   Number of rows in the table.
 * @param ncol   Number of columns in the table.
 * @return The double table.
 */
template<typename R,typename C>
std::vector<std::vector<double>>
readTable(std::istream_iterator<std::string> &it, R nrow, C ncol) {
  std::vector<std::vector<double>> tbl(nrow);
  for (R r = 0; r < nrow; r++) {
    tbl[r] = readStrings<double>(it, [](auto &s) { return std::stod(s); });
    if (static_cast<C>(tbl[r].size()) != ncol)
      throw std::runtime_error("not enough columns in file");
  }
  return tbl;
}

void readData() {
  std::string dataDir("../../data");
#ifdef _WIN32
  size_t len;
  char buffer[1024];
  if ( !getenv_s(&len, buffer, sizeof(buffer), "EXAMPLE_DATA_DIR") &&
       len && len < sizeof(buffer) )
    dataDir = buffer;
#else
  char const *envDir = std::getenv("EXAMPLE_DATA_DIR");
  if (envDir)
    dataDir = envDir;
#endif
  std::string dataFile = dataDir + "/" + DATAFILE;
  std::ifstream ifs(dataFile);
  if (!ifs)
    throw std::runtime_error("Could not open " + dataFile);
  std::stringstream data(std::string((std::istreambuf_iterator<char>(ifs)),
                                     (std::istreambuf_iterator<char>())));
  std::istream_iterator<std::string> it(data);
  while (it != std::istream_iterator<std::string>()) {
    std::string token = *it++;
    if (token == "SUPPLIERS:")
      SUPPLIERS = readStrings<int>(it, [](auto &s) { return std::stoi(s); });
    else if (token == "MAXPERC:")
      MAXPERC = readStrings<double>(it, [](auto &s) { return std::stod(s); });
    else if (token == "REQ:")
      REQ = std::stod(*it++);
    else if (token == "BREAKP:")
      BREAKP = readTable(it, SUPPLIERS.size(), NB);
    else if (token == "COST:")
      COST = readTable(it, SUPPLIERS.size(), NB);
  }
}

© 2001-2025 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.