Initializing help system before first use

Els - An economic lot-sizing problem solved by cut-and-branch and branch-and-cut heuristics


Type: Economic lot-sizing
Rating: 5 (difficult)
Description: The version 'ELS' of this example shows how to implement cut-and-branch (= cut generation at the root node of the MIP search) and 'ELSCut' implements a branch-and-cut (= cut generation at the MIP search tree nodes) algorithm using the cut manager.
File(s): ELS.cpp, ELSCut.cpp, ELSManagedCuts.cpp


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

/**
 * Economic lot sizing, ELS, problem. Solved by adding (l,S)-inequalities in
 * several rounds looping over the root node.
 *
 * ELS considers production planning over a horizon of T periods. In period t,
 * t=1,...,T, there is a given demand DEMAND[t] that must be satisfied by
 * production prod[t] in period t and by inventory carried over from previous
 * periods. There is a set-up up cost SETUPCOST[t] associated with production in
 * period t. The unit production cost in period t is PRODCOST[t]. There is no
 * inventory or stock-holding cost.
 */

#include <chrono>
#include <iostream>
#include <xpress.hpp>

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

using std::chrono::duration_cast;
using std::chrono::steady_clock;

double const EPS = 1e-6;
int const T = 6; /* Number of time periods */

/* Data */
std::vector<double> DEMAND{1, 3, 5, 3, 4, 2};       /* Demand per period */
std::vector<double> SETUPCOST{17, 16, 11, 6, 9, 6}; /* Setup cost / period */
std::vector<double> PRODCOST{5, 3, 2, 1, 3, 1};     /* Prod. cost / period */
std::vector<std::vector<double>>
    D(T, std::vector<double>(T)); /* Total demand in periods t1 - t2 */

/** Create the model in P.
 * @param p      The Xpress Solver instance in which the model is created.
 * @param prod   Array to store the production variables.
 * @param setup  Array to store the setup variables.
 */
void modEls(XpressProblem &p, std::vector<Variable> &prod,
            std::vector<Variable> &setup) {
  for (int s = 0; s < T; s++)
    for (int t = 0; t < T; t++)
      for (int k = s; k <= t; k++)
        D[s][t] += DEMAND[k];

  // Variables
  prod = p.addVariables(T)
             .withType(ColumnType::Continuous)
             .withName([](auto t) { return "prod" + std::to_string(t + 1); })
             .toArray();

  setup = p.addVariables(T)
              .withType(ColumnType::Binary)
              .withName([](auto t) { return "setup" + std::to_string(t + 1); })
              .toArray();

  // Objective: Minimize total cost
  p.setObjective(
      sum(scalarProduct(setup, SETUPCOST), scalarProduct(prod, PRODCOST)),
      ObjSense::Minimize);

  // Constraints

  // Production in period t must not exceed the total demand for the
  // remaining periods; if there is production during t then there
  // is a setup in t
  // for all t in [0,T[
  //     prod[t] <= setup[t] * D[t][T-1]
  p.addConstraints(T,
                   [&](auto t) { return prod[t] <= setup[t] * D[t][T - 1]; });

  // Production in periods 0 to t must satisfy the total demand
  // during this period of time
  // for all t in [0,T[
  //     sum(s in [0,t+1[) prod[s] >= D[0][t]
  p.addConstraints(T, [&](auto t) {
    return sum(t + 1, [&](auto s) { return prod[s]; }) >= D[0][t];
  });
  p.writeProb("ELS.lp", "l");
}

/**
 * Cut generation loop at the top node:
 * solve the LP and save the basis
 * get the solution values
 * identify and set up violated constraints
 * load the modified problem and load the saved basis
 * @param p      The Xpress Solver instance that is populated with the model.
 * @param prod   Production variables.
 * @param setup  Setup variables.
 */
void solveEls(XpressProblem &p, std::vector<Variable> &prod,
              std::vector<Variable> &setup) {
  p.callbacks.addMessageCallback(XpressProblem::console);
  /* Disable automatic cuts - we use our own */
  p.controls.setCutStrategy(XPRS_CUTSTRATEGY_NONE);
  /* Switch presolve off */
  p.controls.setPresolve(XPRS_PRESOLVE_NONE);

  int ncut = 0, npass = 0, npcut = 0;
  steady_clock::time_point starttime = steady_clock::now();
  std::vector<double> sol;

  do {
    p.writeProb("model" + std::to_string(npass) + ".lp");
    npass++;
    npcut = 0;
    // Solve the LP-problem
    p.lpOptimize();
    if (p.attributes.getSolStatus() != SolStatus::Optimal)
      throw std::runtime_error("failed to optimize with status " +
                               to_string(p.attributes.getSolStatus()));
    // Get the solution values:
    sol = p.getSolution();
    // Search for violated constraints:
    for (int l = 0; l < T; l++) {
      double ds = 0.0;
      for (int t = 0; t <= l; t++) {
        if (prod[t].getValue(sol) < D[t][l] * setup[t].getValue(sol) + EPS) {
          ds += prod[t].getValue(sol);
        } else {
          ds += D[t][l] * setup[t].getValue(sol);
        }
      }

      // Add the violated inequality: the minimum of the actual production
      // prod[t] and the maximum potential production D[t][l]*setup[t] in
      // periods 0 to l must at least equal the total demand in periods 0 to l.
      //    sum(t=1:l) min(prod[t], D[t][l]*setup[t]) >= D[0][l]
      if (ds < D[0][l] - EPS) {
        Expression cut = sum(l + 1, [&](auto t) {
          if (prod[t].getValue(sol) < D[t][l] * setup[t].getValue(sol) + EPS)
            return 1.0 * prod[t];
          else
            return D[t][l] * setup[t];
        });
        p.addConstraint(cut >= D[0][l]);
        ncut++;
        npcut++;
      }
    }
    steady_clock::time_point endtime = steady_clock::now();
    std::cout << "Iteration " << npass << ", "
              << duration_cast<std::chrono::milliseconds>(endtime - starttime)
                         .count() *
                     1e-3
              << " sec"
              << ", objective value: " << p.attributes.getObjVal()
              << ", cuts added: " << npcut << " (total " << ncut << ")"
              << std::endl;

    if (npcut == 0)
      std::cout << "Optimal integer solution found:" << std::endl;

  } while (npcut > 0);

  // Print out the solution:
  for (int t = 0; t < T; t++) {
    std::cout << "Period " << (t + 1) << ": prod" << prod[t].getValue(sol)
              << " (demand: " << DEMAND[t] << ", cost: " << PRODCOST[t] << ")"
              << ", setup " << setup[t].getValue(sol) << " (cost "
              << SETUPCOST[t] << ")" << std::endl;
  }
}

int main() {
  XpressProblem prob;
  std::vector<Variable> prod;
  std::vector<Variable> setup;

  modEls(prob, prod, setup);   // Model the problem
  solveEls(prob, prod, setup); // Solve the problem

  return 0;
}

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

/** Economic lot sizing, ELS, problem.
 * Solved by adding (l,S)-inequalities in a branch-and-cut heuristic
 * (using the cut manager).
 *
 * ELS considers production planning over a horizon of T periods. In period t,
 * t=1,...,T, there is a given demand DEMAND[t] that must be satisfied by
 * production prod[t] in period t and by inventory carried over from previous
 * periods. There is a set-up up cost SETUPCOST[t] associated with production in
 * period t. The unit production cost in period t is PRODCOST[t]. There is no
 * inventory or stock-holding cost.
 *
 * *** This model cannot be run with a Community Licence ***
 */

#include <iostream>
#include <xpress.hpp>

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

double const EPS = 1e-6;
int const T = 6; /* Number of time periods */

/* Data */
std::vector<double> DEMAND{1, 3, 5, 3, 4, 2};       /* Demand per period */
std::vector<double> SETUPCOST{17, 16, 11, 6, 9, 6}; /* Setup cost / period */
std::vector<double> PRODCOST{5, 3, 2, 1, 3, 1};     /* Prod. cost / period */
std::vector<std::vector<double>>
    D(T, std::vector<double>(T)); /* Total demand in periods t1 - t2 */

void printProblemStatus(XpressProblem const &prob) {
  std::cout << "Problem status:" << std::endl
            << "\tSolve status: " << prob.attributes.getSolveStatus()
            << std::endl
            << "\tLP status: " << prob.attributes.getLpStatus() << std::endl
            << "\tMIP status: " << prob.attributes.getMipStatus() << std::endl
            << "\tSol status: " << prob.attributes.getSolStatus() << std::endl;
}

/**
 * Cut generation algorithm:
 * - get the solution values
 * - identify and set up violated constraints
 * - add cuts to the problem
 * @param p      Xpress solver instance as passed to the callback.
 * @param prod   Production variables.
 * @param setup  Setup variables.
 */
void separate(XpressProblem &p, std::vector<Variable> &prod,
              std::vector<Variable> &setup) {
  int ncut = 0;
  // Add cut only to optimal relaxations
  if (p.attributes.getLpStatus() != LPStatus::Optimal)
    return;

  auto sol = p.getCallbackSolution();
  auto slack = p.getCallbackSlacks();
  auto duals = p.getCallbackDuals();
  auto djs = p.getCallbackRedCosts();
  // Search for violated constraints:
  for (int l = 0; l < T; l++) {
    double ds = 0.0;
    for (int t = 0; t <= l; t++) {
      if (prod[t].getValue(sol) < D[t][l] * setup[t].getValue(sol) + EPS) {
        ds += prod[t].getValue(sol);
      } else {
        ds += D[t][l] * setup[t].getValue(sol);
      }
    }

    // Add the violated inequality: the minimum of the actual production
    // prod[t] and the maximum potential production D[t][l]*setup[t]
    // in periods 0 to l must at least equal the total demand in periods
    // 0 to l.
    // sum(t=1:l) min(prod[t], D[t][l]*setup[t]) >= D[0][l] */
    if (ds < D[0][l] - EPS) {
      Expression cut = sum(l + 1, [&](auto t) {
        if (prod[t].getValue(sol) < D[t][l] * setup[t].getValue(sol) + EPS)
          return 1.0 * prod[t];
        else
          return D[t][l] * setup[t];
      });
      p.addCut(0, cut >= D[0][1]);
      ncut++;
    }
  }

  if (ncut > 0) {
    std::cout << "Cuts added: " << ncut << "(depth "
              << p.attributes.getNodeDepth() << ", node "
              << p.attributes.getNodes() << ")" << std::endl;
  }
}

/** Create the model in P.
 * @param p      The Xpress Solver instance in which the model is created.
 * @param prod   Array to store the production variables.
 * @param setup  Array to store the setup variables.
 */
void modEls(XpressProblem &p, std::vector<Variable> &prod,
            std::vector<Variable> &setup) {
  for (int s = 0; s < T; s++)
    for (int t = 0; t < T; t++)
      for (int k = s; k <= t; k++)
        D[s][t] += DEMAND[k];

  // Variables
  prod = p.addVariables(T)
             .withType(ColumnType::Continuous)
             .withName([](auto t) { return "prod" + std::to_string(t + 1); })
             .toArray();

  setup = p.addVariables(T)
              .withType(ColumnType::Binary)
              .withName([](auto t) { return "setup" + std::to_string(t + 1); })
              .toArray();

  // Objective: Minimize total cost
  p.setObjective(
      sum(scalarProduct(setup, SETUPCOST), scalarProduct(prod, PRODCOST)),
      ObjSense::Minimize);

  // Constraints

  // Production in period t must not exceed the total demand for the
  // remaining periods; if there is production during t then there
  // is a setup in t
  // for all t in [0,T[
  // prod[t] <= setup[t] * D[t][T-1]
  p.addConstraints(T,
                   [&](auto t) { return prod[t] <= setup[t] * D[t][T - 1]; });

  // Production in periods 0 to t must satisfy the total demand
  // during this period of time
  // for all t in [0,T[
  // sum(s in [0,t+1[) prod[s] >= D[0][t]
  p.addConstraints(T, [&](auto t) {
    return sum(t + 1, [&](auto s) { return prod[s]; }) >= D[0][t];
  });
  p.writeProb("ELSCut.lp", "l");
}

/** Solve the model.
 * Cuts are added dynamically with the optnode callback.
 * @param p      The Xpress Solver instance that is populated with the model.
 * @param prod   Production variables.
 * @param setup  Setup variables.
 */
void solveEls(XpressProblem &p, std::vector<Variable> &prod,
              std::vector<Variable> &setup) {
  p.callbacks.addMessageCallback(XpressProblem::console);
  p.controls.setLpLog(0);
  p.controls.setMipLog(3);
  // Disable automatic cuts - we use our own
  p.controls.setCutStrategy(XPRS_CUTSTRATEGY_NONE);
  // Switch presolve off
  p.controls.setPresolve(XPRS_PRESOLVE_NONE);
  p.controls.setMipPresolve(0);

  p.callbacks.addOptnodeCallback(
      [&](auto &prob, auto) { separate(prob, prod, setup); });

  // Solve the MIP
  p.optimize();
  if (p.attributes.getSolStatus() != SolStatus::Optimal)
    throw std::runtime_error("optimization failed with status " +
                             to_string(p.attributes.getSolStatus()));
  // Get the solution values:
  auto sol = p.getSolution();

  // Print out the solution:
  for (int t = 0; t < T; t++) {
    std::cout << "Period " << (t + 1) << ": prod" << prod[t].getValue(sol)
              << " (demand: " << DEMAND[t] << ", cost: " << PRODCOST[t] << ")"
              << ", setup " << setup[t].getValue(sol) << " (cost "
              << SETUPCOST[t] << ")" << std::endl;
  }
  printProblemStatus(p);
}

int main() {
  XpressProblem prob;
  std::vector<Variable> prod;
  std::vector<Variable> setup;
  modEls(prob, prod, setup);   // Model the problem
  solveEls(prob, prod, setup); // Solve the problem
  return 0;
}

ELSManagedCuts.cpp
// (c) 2023-2025 Fair Isaac Corporation
/** Demonstrates how to implement cutting planes as part of a MIP
 * branch-and-bound search using the cutround callback.
 *
 * Cuts are added as user cuts using XpressProblem.addManagedCut().
 *
 * Economic lot sizing problem. Solved by adding (l,S)-inequalities
 * in a branch-and-cut heuristic (using the cutround callback).
 *
 * ELS considers production planning over a horizon of T periods. In period t,
 * t=1,...,T, there is a given demand DEMAND[p,t] that must be satisfied by
 * production produce[p,t] in period t and by inventory carried over
 * from previous periods.
 * There is a set-up cost SETUPCOST[t] associated with production in
 * period t and the total production capacity per period is limited. The unit
 * production cost in period t is PRODCOST[p,t]. There is no
 * inventory or stock-holding cost.
 *
 * A well-known class of valid inequalities for ELS are the
 * (l,S)-inequalities.  Let D(p,q) denote the demand in periods p
 * to q and y(t) be a binary variable indicating whether there is any
 * production in period t.  For each period l and each subset of periods S
 * of 1 to l, the (l,S)-inequality is
 * <pre>
 *      sum (t=1:l | t in S) x(t) + sum (t=1:l | t not in S) D(t,l) * y(t)
 *          &gt;= D(1,l)
 * </pre>
 *
 * It says that actual production x(t) in periods included S plus maximum
 * potential production D(t,l)*y(t) in the remaining periods (those not in
 * S) must at least equal total demand in periods 1 to l.  Note that in
 * period t at most D(t,l) production is required to meet demand up to
 * period l.
 *
 * Based on the observation that
 * <pre>
 *      sum (t=1:l | t in S) x(t) + sum (t=1:l | t not in S) D(t,l) * y(t)
 *          &gt;= sum (t=1:l) min(x(t), D(t,l) * y(t))
 *          &gt;= D(1,l)
 * </pre>
 * it is easy to develop a separation algorithm and thus a cutting plane
 * algorithm based on these (l,S)-inequalities.
 */

#include <xpress.hpp>
#include <xpress_objects.hpp>
#include <iostream>

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

/** Tolerance for satisfiability. */
#define EPS 1e-6

/** Number of time periods. */
#define DIM_TIMES 15
/** Number of products to produce. */
#define DIM_PRODUCTS  4

/** Demand per product (first dim) and time period (second dim). */
static int const DEMAND[DIM_PRODUCTS][DIM_TIMES] = {
  {2, 3, 5, 3, 4, 2, 5, 4, 1, 3, 4, 2, 3, 5, 2},
  {3, 1, 2, 3, 5, 3, 1, 2, 3, 3, 4, 5, 1, 4, 1},
  {3, 5, 2, 1, 2, 1, 3, 3, 5, 2, 2, 1, 3, 2, 3},
  {2, 2, 1, 3, 2, 1, 2, 2, 3, 3, 2, 2, 3, 1, 2}
};
/** Setup cost. */
static double const SETUPCOST[DIM_TIMES] = {
  17, 14, 11, 6, 9, 6, 15, 10, 8, 7, 12, 9, 10, 8, 12
};
/** Production cost per product (first dim) and time period (second dim). */
static double const PRODCOST[DIM_PRODUCTS][DIM_TIMES] = {
  {5, 3, 2, 1, 3, 1, 4, 3, 2, 2, 3, 1, 2, 3, 2},
  {1, 4, 2, 3, 1, 3, 1, 2, 3, 3, 3, 4, 4, 2, 2},
  {3, 3, 3, 4, 4, 3, 3, 3, 2, 2, 1, 1, 3, 3, 3},
  {2, 2, 2, 3, 3, 3, 4, 4, 4, 3, 3, 2, 2, 2, 3}
};
/** Capacity. */
static const int CAP[DIM_TIMES] = {
  12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12
};

/** Cut round callback for separating our cutting planes. */
class Callback {
  std::vector<std::vector<Variable>> const &produce;
  std::vector<std::vector<Variable>> const &setup;
  double const (&sumDemand)[DIM_PRODUCTS][DIM_TIMES][DIM_TIMES];
 public:
  Callback(std::vector<std::vector<Variable>> const &p,
           std::vector<std::vector<Variable>> const &s,
           double const (&d)[DIM_PRODUCTS][DIM_TIMES][DIM_TIMES])
  : produce(p)
  , setup(s)
  , sumDemand(d)
  {
  }
  void operator()(XpressProblem &prob, int, int *) const {
    // Apply only one round of cutting on each node.
    // Because the CutRound callback is fired *before* a round of
    // cutting, the CUTROUNDS attribute will start from 0 on the first
    // invocation of the callback.
    if (prob.attributes.getCutRounds() >= 1)
      return;

    // Get the solution vector.
    // Xpress will only fire the CutRound callback when a solution is
    // available, so there is no need to check whether a solution is
    // available.
    auto sol = prob.getCallbackSolution();

    // Search for violated constraints : the minimum of the actual
    // production produce[p][t] and the maximum potential production
    // D[p][t][l]*setup[p][t] in periods 0 to l must at least equal
    // the total demand in periods 0 to l.
    //    sum(t=1:l) min(prod[p][t], D[p][t][l]*setup[p][t]) >= D[p][0][l]
    int nCutsAdded = 0;
    for (int p = 0; p < DIM_PRODUCTS; p++) {
      for (int l = 0; l < DIM_TIMES; l++) {
        double sum = 0.0;
        for (int t = 0; t <= l; t++) {
          if (produce[p][t].getValue(sol) < sumDemand[p][t][l] * setup[p][t].getValue(sol) + EPS) {
            sum += produce[p][t].getValue(sol);
          }
          else {
            sum += sumDemand[p][t][l] * setup[p][t].getValue(sol);
          }
        }
        if (sum < sumDemand[p][0][l] - EPS) {
          // Create the violated cut.
          LinExpression cut = LinExpression::create();
          for (int t = 0; t <= l; t++) {
            if (produce[p][t].getValue(sol) < sumDemand[p][t][l] * setup[p][t].getValue(sol)) {
              cut.addTerm(1.0, produce[p][t]);
            }
            else {
              cut.addTerm(sumDemand[p][t][l], setup[p][t]);
            }
          }

          // Give the cut to Xpress to manage.
          // It will automatically be presolved.
          prob.addManagedCut(true, cut >= sumDemand[p][0][l]);
          ++nCutsAdded;

          // If we modified the problem in the callback, Xpress
          // will automatically trigger another roound of cuts,
          // so there is no need to set the action return
          // argument.
        }
      }
    }
    if (nCutsAdded > 0) {
      std::cout << "Cuts added: " << nCutsAdded << std::endl;
    }
  }
};

int main(void) {
  XpressProblem prob;
  prob.callbacks.addMessageCallback(XpressProblem::console);

  // Create an economic lot sizing problem.
  // Calculate demand D(p, s, t) as the demand for product p from
  // time s to time t(inclusive).
  double sumDemand[DIM_PRODUCTS][DIM_TIMES][DIM_TIMES];
  for (int p = 0; p < DIM_PRODUCTS; p++) {
    for (int s = 0; s < DIM_TIMES; s++) {
      double thisSumDemand = 0.0;
      for (int t = s; t < DIM_TIMES; t++) {
        thisSumDemand += DEMAND[p][t];
        sumDemand[p][s][t] = thisSumDemand;
      }
    }
  }

  auto produce = prob.addVariables(DIM_PRODUCTS, DIM_TIMES)
    .withName([](auto p, auto t){ return xpress::format("produce(%d,%d)", p, t); })
    .toArray();
  auto setup = prob.addVariables(DIM_PRODUCTS, DIM_TIMES)
    .withType(ColumnType::Binary)
    .withName([](auto p,auto t){ return xpress::format("setup(%d,%d)", p, t); })
    .toArray();

  // Add the objective function :
  // MinCost:= sum(t in TIMES) (SETUPCOST(t) * sum(p in PRODUCTS) setup(p,t) +
  //           sum(p in PRODUCTS) PRODCOST(p, t) *produce(p, t) )
  prob.setObjective(sum(DIM_TIMES, DIM_PRODUCTS,
                        [&](auto t, auto p){
                          return SETUPCOST[t] * setup[p][t] + PRODCOST[p][t] * produce[p][t];
                        }));

  // Add constraints.
  // Production in periods 0 to t must satisfy the total demand
  // during this period of time, for all t in [0,T[
  //    forall(p in PRODUCTS, t in TIMES)
  //      Dem(t) : = sum(s in 1..t) produce(p,s) >= sum(s in 1..t) DEMAND(s)
  prob.addConstraints(DIM_PRODUCTS, DIM_TIMES,
                      [&](auto p, auto t){
                        return sum(t + 1,
                                   [&](auto s){
                                     return produce[p][s];
                                   })
                          >= sumDemand[p][0][t];
                      });

  // If there is production during t then there is a setup in t :
  //    forall(p in PRODUCTS, t in TIMES)
  //       ProdSetup(t) : = produce(t) <= D(t,TIMES) * setup(t)
  for (int p = 0; p < DIM_PRODUCTS; ++p) {
    for (int t = DIM_TIMES - 1; t >= 0; --t) {
      prob.addConstraint(produce[p][t] <= sumDemand[p][t][DIM_TIMES - 1] * setup[p][t]);
    }
  }

  // Capacity limits :
  //    forall(t in TIMES) Capacity(t) : = sum(p in PRODUCTS) produce(p, t) <= CAP(t)
  prob.addConstraints(DIM_TIMES,
                      [&](auto t){
                        return sum(DIM_PRODUCTS,
                                   [&](auto p) {
                                     return produce[p][t];
                                   })
                          <= CAP[t];
                      });

  // Add a CutRound callback for separating our cuts.
  Callback cb(produce, setup, sumDemand);
  prob.callbacks.addCutRoundCallback(cb);

  prob.writeProb("elsmanagedcuts.lp");

  prob.optimize();

  if (prob.attributes.getSolveStatus() == SolveStatus::Completed &&
      prob.attributes.getSolStatus() == SolStatus::Optimal) {
    std::cout << "Solved problem to optimality." << std::endl;
  }
  else {
    std::cout << "Failed to solve problem with solvestatus "
              << to_string(prob.attributes.getSolveStatus())
              << " and solstatus "
              << to_string(prob.attributes.getSolStatus())
              << std::endl;
  }

  return 0;
}

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