Program Listing for File ipg_cutandplay.cpp
↰ Return to documentation for file (src/games/algorithms/IPG/ipg_cutandplay.cpp
)
/* #############################################
* This file is part of
* ZERO
*
* Copyright (c) 2020
* Released under the Creative Commons
* CC BY-NC-SA 4.0 License
*
* Find out more at
* https://github.com/ds4dm/ZERO
* #############################################*/
#include "games/algorithms/IPG/ipg_cutandplay.h"
#include "coin/CglGMI.hpp"
#include "coin/CglKnapsackCover.hpp"
#include "coin/CoinPackedMatrix.hpp"
#include "coin/OsiSolverInterface.hpp"
#include <coin/CglGomory.hpp>
#include <coin/CglMixedIntegerRounding2.hpp>
#include <memory>
bool Algorithms::IPG::IPG_Player::addVertex(const arma::vec &vertex, const bool checkDuplicate) {
bool go = false;
if (checkDuplicate)
go = Utils::containsRow(this->V, vertex, this->Tolerance);
if (!go) {
int nCols = this->V.n_cols < 1 ? vertex.size() : this->V.n_cols;
this->V.resize(this->V.n_rows + 1, nCols);
this->V.row(this->V.n_rows - 1) = vertex.t();
return true;
}
return false;
}
bool Algorithms::IPG::IPG_Player::addRay(const arma::vec &ray, const bool checkDuplicate) {
bool go = false;
if (checkDuplicate)
go = Utils::containsRow(this->R, ray, this->Tolerance);
if (!go) {
int nCols = this->R.n_cols < 1 ? ray.size() : this->R.n_cols;
this->R.resize(this->R.n_rows + 1, nCols);
this->R.row(this->R.n_rows - 1) = ray.t();
return true;
}
return false;
}
bool Algorithms::IPG::IPG_Player::addCuts(const arma::sp_mat &LHS, const arma::vec &RHS) {
arma::sp_mat LHSClean = Utils::clearMatrix(LHS, 1e-6, 1 - 1e-6);
arma::vec RHSClean = Utils::clearVector(RHS, 1e-6, 1 - 1e-6);
unsigned int newCuts = LHS.n_rows;
unsigned int cuts = this->CutPool_A.n_rows;
int nCols = this->CutPool_A.n_cols < 1 ? LHSClean.size() : this->CutPool_A.n_cols;
// Add the constraints to the cut pool
this->CutPool_A.resize(cuts + newCuts, nCols);
this->CutPool_b.resize(cuts + newCuts);
this->CutPool_A.submat(cuts, 0, cuts + newCuts - 1, nCols - 1) = LHSClean;
this->CutPool_b.subvec(cuts, cuts + newCuts - 1) = RHSClean;
// Add the constraints to the Coin model
/******RECURSIVE CUT GENERATION********
//Uncomment this to enable recursive cut generation
auto convertedCuts = Utils::armaToCoinPackedVector(LHS);
try {
for (unsigned int i = 0; i < newCuts; ++i)
this->CoinModel->addRow(convertedCuts.at(i), 'L', RHS.at(i), 0);
} catch (CoinError &e) {
throw ZEROException(ZEROErrorCode::SolverError,
"Invalid Coin-OR interface response: " + e.message());
}
*******RECURSIVE CUT GENERATION*********/
// Add the constraints to the parametrized IP
this->ParametrizedIP->addConstraints(LHSClean, RHSClean);
//******DEBUG********
// LHS.print_dense("LHS");
// RHS.print("RHS");
//******DEBUG********
return true;
}
bool Algorithms::IPG::CutAndPlay::addValueCut(unsigned int player,
double RHS,
const arma::vec &xMinusI) {
//******DEBUG********
// this->Players.at(player)->Incumbent.print("Incumbent of I");
// xMinusI.print("xMinusI");
// this->IPG->PlayersIP.at(player)->getc().print("cOfI");
// this->IPG->PlayersIP.at(player)->getC().print_dense("CofI");
//******DEBUG********
arma::vec LHS = -(this->IPG->PlayersIP.at(player)->getc() +
this->IPG->PlayersIP.at(player)->getC() * xMinusI);
RHS += -arma::as_scalar(this->IPG->PlayersIP.at(player)->getd().t() * xMinusI);
// Constant!
if (Utils::isEqual(arma::max(arma::abs(LHS)), 0)) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::addValueCut: "
"Constant cut. Discarding. ";
return false;
}
//******DEBUG********
// LHS.impl_raw_print("LHS with RHS=" + std::to_string(-RHS));
//******DEBUG********
Utils::normalizeIneq(LHS, RHS, false);
//******DEBUG********
// LHS.impl_raw_print("LHS with RHS=" + std::to_string(-RHS));
//******DEBUG********
if (Utils::nonzeroDecimals(RHS, 6) >= 5) {
LOG_S(0) << "Algorithms::IPG::CutAndPlay::addValueCut: "
"Numerically unstable. Generating another cut.";
if (this->IPG->Stats.AlgorithmData.CutAggressiveness.get() !=
Data::IPG::CutsAggressiveness::NotEvenTry) {
if (this->externalCutGenerator(player, 1, false, true) != 0)
return true;
}
// Force normalization, in case it wasn't before.
RHS = Utils::round_nplaces(RHS, 6);
for (unsigned int i = 0; i < LHS.size(); ++i)
LHS.at(i) = Utils::round_nplaces(LHS.at(i), 6);
Utils::normalizeIneq(LHS, RHS, true);
LOG_S(0) << "Algorithms::IPG::CutAndPlay::addValueCut: "
"WARNING: Cannot generate another cut. Adding normalized value-cut.";
}
//******DEBUG********
// LHS.print("Value-Cut: LHS with RHS of" + std::to_string(-RHS));
//******DEBUG********
this->Cuts.at(0).second += 1;
// Again, minus sign on RHS (the inequality is >=)
return this->Players.at(player)->addCuts(
arma::sp_mat{LHS.t()},
arma::vec{-RHS});
}
bool Algorithms::IPG::CutAndPlay::checkTime(double &remaining) const {
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
const std::chrono::duration<double> timeElapsed =
std::chrono::high_resolution_clock::now() - this->IPG->InitTime;
remaining = this->IPG->Stats.AlgorithmData.TimeLimit.get() - timeElapsed.count();
if (remaining <= 0) {
LOG_S(1) << "Algorithms::IPG::CutAndPlay::checkTime: "
"Time limit hit.";
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
if (this->IPG->Stats.Status.get() == ZEROStatus::Uninitialized)
this->IPG->Stats.Status.set(ZEROStatus::TimeLimit);
return false;
} else
return true;
} else {
remaining = -1;
return true;
}
}
void Algorithms::IPG::CutAndPlay::initLCPObjective() {
this->LCP_Q.zeros(this->IPG->NumVariables, this->IPG->NumVariables);
this->LCP_c.zeros(this->IPG->NumVariables);
unsigned int varCounter = 0;
for (unsigned int p = 0; p < this->IPG->NumPlayers; ++p) {
// Fill the c vector
unsigned int playerVars = this->IPG->PlayerVariables.at(p);
this->LCP_c.subvec(varCounter, varCounter + playerVars - 1) =
this->IPG->PlayersIP.at(p)->getc();
unsigned int otherVarsCounter = 0;
for (int o = 0; o < this->IPG->NumPlayers; ++o) {
if (p != o) {
int startCol = std::accumulate(
this->IPG->PlayerVariables.begin(), this->IPG->PlayerVariables.begin() + o, 0);
unsigned int otherVars = this->IPG->PlayerVariables.at(o);
this->LCP_Q.submat(
varCounter, startCol, varCounter + playerVars - 1, startCol + otherVars - 1) =
this->IPG->PlayersIP.at(p)->getC().submat(
0, otherVarsCounter, playerVars - 1, otherVarsCounter + otherVars - 1);
otherVarsCounter += otherVars;
}
}
varCounter += playerVars;
}
//******DEBUG********
// this->LCP_c.print("This is LCP_c");
// this->LCP_Q.print_dense("This is LCP_Q");
//******DEBUG********
}
void Algorithms::IPG::CutAndPlay::solve() {
this->initialize();
bool MIPCuts = true;
auto cutLevel = this->IPG->Stats.AlgorithmData.CutAggressiveness.get();
if (cutLevel == Data::IPG::CutsAggressiveness::NoThanks ||
cutLevel == Data::IPG::CutsAggressiveness::NotEvenTry)
MIPCuts = false;
int cutsAggressiveness = 0;
if (MIPCuts) {
if (this->IPG->Stats.AlgorithmData.CutAggressiveness.get() ==
Data::IPG::CutsAggressiveness::Truculent)
cutsAggressiveness = 3;
else
cutsAggressiveness = 1;
}
if (this->Infeasible) {
this->IPG->Stats.Status.set(ZEROStatus::NashEqNotFound);
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: A Nash Equilibrium has not been "
"found. At least one of the players problem is infeasible.";
return;
}
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
if (MIPCuts) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: Adding root cuts.";
this->externalCutGenerator(i, 5, true, false);
}
}
// Main loop condition
bool solved{false};
// Which players are feasible
std::vector<int> feasible(this->IPG->NumPlayers, 0);
// Number of mip cuts added
std::vector<int> addedMIPCuts(this->IPG->NumPlayers, 0);
int Iteration = 0;
while (!solved) {
ZEROStatus status;
// Increase the number of iterations
Iteration++;
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: Iteration ########### " << Iteration
<< " ###########";
this->IPG->Stats.NumIterations.set(Iteration);
/* ************************************
* Check time and solve the LCP
**************************************/
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
double remaining;
if (this->checkTime(remaining) && remaining > 0)
status = this->equilibriumLCP(remaining * 0.95, true, true);
else
return;
} else
status = this->equilibriumLCP(-1, true, true);
/* ************************************
* Numerical issues?
**************************************/
if (status == ZEROStatus::Numerical || this->IPG->Stats.Status.get() == ZEROStatus::Numerical) {
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: Numerical errors.";
this->IPG->Stats.Status.set(ZEROStatus::Numerical);
return;
}
/* ************************************
* Support objects
**************************************/
// Is the player feasible?
std::fill(feasible.begin(), feasible.end(), 0);
// Did we add other cuts?
std::fill(addedMIPCuts.begin(), addedMIPCuts.end(), 0);
// How many cuts added in total?
unsigned int addedCuts = 0;
// Build xMinusIs
std::vector<arma::vec> xMinusI_s;
xMinusI_s = {};
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i)
xMinusI_s.push_back(this->buildXminusI(i));
if (status == ZEROStatus::NashEqFound) {
/* ************************************
* Loop until at least one cut is added, or a solution is found.
**************************************/
while (addedCuts <= 0) {
solved = true;
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
// Check only if the player hasn't proven to be feasible
if (feasible.at(i) == 0) {
// Number of added cuts.
int EO_cut = 0;
/* ************************************
* Call the EO. Put in EO_cut the number of cuts
***************************************/
int EO = this->preEquilibriumOracle(
i, EO_cut, this->Players.at(i)->Incumbent, xMinusI_s.at(i));
/* ************************************
* Numerical errors?
***************************************/
if (this->IPG->Stats.Status.get() == ZEROStatus::Numerical && EO == -1) {
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: Numerical errors.";
return;
}
/* ************************************
* If we didn't have numerical problems
***************************************/
if (EO != 1) {
// Here we have either 0 or -2 (Infeasible or iteration limit)
solved = false;
if (EO == 0) {
/* ************************************
* Infeasible
***************************************/
addedCuts += EO_cut; //+ this->separateCoinCuts(i, 5);
// if (Iteration > 5)
if (MIPCuts) {
addedCuts += this->externalCutGenerator(
i,
(Iteration > 5 || (Iteration == 1 && cutsAggressiveness > 1))
? cutsAggressiveness
: 1,
false,
false);
}
} else {
/* ************************************
* Iteration limit.
***************************************/
// If we have cutting planes turned on, add more cuts.
if (MIPCuts && addedMIPCuts.at(i) == 0) {
addedCuts += this->externalCutGenerator(i, cutsAggressiveness, false, false);
addedMIPCuts.at(i) = 1;
}
}
} else {
/* ************************************
* Player is feasible
***************************************/
feasible.at(i) = 1;
}
}
}
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
double remaining;
if (!this->checkTime(remaining) || remaining <= 0) {
return;
}
}
// If not solved but there are cuts, or it is solved and there are not cuts, exit.
if ((addedCuts > 0 && !solved) || (addedCuts == 0 && solved)) {
if (addedCuts == 0 && solved) {
this->Solved = true;
bool pure = true;
this->IPG->SocialWelfare = 0;
for (unsigned int i = 0, counter = 0; i < this->IPG->NumPlayers; ++i) {
this->IPG->Solution.at(i) = this->Players.at(i)->Incumbent;
counter += this->IPG->PlayerVariables.at(i);
this->IPG->SocialWelfare += this->Players.at(i)->Payoff;
if (!this->Players.at(i)->Pure) {
pure = false;
}
}
this->Pure = pure;
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: A Nash Equilibrium has been found ("
<< (pure == 0 ? "MNE" : "PNE") << ").";
// Return if we do not need optimality
this->IPG->Stats.Status.set(ZEROStatus::NashEqFound);
return;
} else {
// addedCuts > 0 && !solved
// Not solved but cuts. So far nothing, but need to recompute the MNE. Break from here
}
}
} // end while cuts
} else if (status == ZEROStatus::NashEqNotFound) {
/* ************************************
* What if not solved? So far, it may be a condition triggered by the cutoff of the optimal
*equilibrium
*************************************/
//@todo Implement for unbounded IPGs, where an eq may not exist at a given iteration
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::solve: No Nash Equilibrium has been found";
this->Solved = false;
this->IPG->Stats.Status.set(ZEROStatus::NashEqNotFound);
return;
}
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
double remaining;
if (!this->checkTime(remaining) || remaining <= 0)
return;
}
}
}
int Algorithms::IPG::CutAndPlay::preEquilibriumOracle(const unsigned int player,
int &addedCuts,
arma::vec &xOfI,
arma::vec &xMinusI) {
ZEROAssert(player < this->IPG->NumPlayers);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: (P" << player
<< ") The oracle has been called. Preprocessing.";
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
double remaining;
if (!this->checkTime(remaining) || remaining <= 0)
return 0;
}
unsigned int Ny = this->IPG->PlayerVariables.at(player); // Equals to numVars by definition
// Remember: the standard is minimization!
// Update working strategies with "educated guesses"
auto PureIP = this->IPG->PlayersIP.at(player)->getIPModel(xMinusI, false);
//******DEBUG********
// PureIP->write("PureIP.lp");
//******DEBUG********
PureIP->set(GRB_IntParam_SolutionLimit, 100);
PureIP->optimize();
int status = PureIP->get(GRB_IntAttr_Status);
if (status == GRB_OPTIMAL) {
// Then, we have a best response
double IP_Objective = PureIP->getObjective().getValue();
double REL_Objective = this->Players.at(player)->Payoff;
if (IP_Objective == GRB_INFINITY) {
LOG_S(1) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle (P" << player
<< ") Unbounded deviation.";
addedCuts = false;
return 0;
}
auto diff = REL_Objective - IP_Objective;
if (!Utils::isEqual(
std::abs(diff), 0, 10 * this->IPG->Stats.AlgorithmData.DeviationTolerance.get())) {
// There exists a difference between the payoffs
if (diff > this->IPG->Stats.AlgorithmData.DeviationTolerance.get()) {
// This cannot happen!
this->IPG->Stats.Status.set(ZEROStatus::Numerical);
LOG_S(WARNING)
<< "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: |NUMERICAL WARNING| Invalid "
"payoff relation (better best response) of " +
std::to_string(diff);
return -1;
} else {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: (P" << player
<< ") REL: " << REL_Objective << " vs IP: " << IP_Objective
<< ". Adding a value cut.";
// Infeasible strategy. Add a value-cut
this->addValueCut(player, IP_Objective, xMinusI);
addedCuts = 1;
return 0;
} // end abs(diff)
} else {
// No discrepancy between payoffs
int numSols = PureIP->get(GRB_IntAttr_SolCount);
// Will be set to true if any pure-best response correspond to the current strategy
bool equal = false;
// Number of best responses found
int bestResponses = 0;
for (unsigned int s = 0; s < numSols; ++s) {
PureIP->set(GRB_IntParam_SolutionNumber, s);
// Check if the strategies are the same!
arma::vec bestResponse(Ny, arma::fill::zeros);
for (unsigned int k = 0; k < Ny; ++k)
bestResponse.at(k) = PureIP->getVarByName("y_" + std::to_string(k)).get(GRB_DoubleAttr_X);
// Add the strategy to the best-responses pool as a vertex
if (!this->Players.at(player)->addVertex(bestResponse, true))
bestResponses++;
if (Utils::isZero(xOfI - bestResponse, this->Tolerance)) {
this->Players.at(player)->Pure = true;
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: (P" << player
<< ") Feasible strategy (BR)";
equal = true;
}
}
LOG_S(2) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: (P" << player << ") Found "
<< bestResponses << " best responses vertices.";
if (equal)
return 1;
else {
// In this case, we need to call the proper oracle.
unsigned int iterations = this->IPG->PlayerVariables.at(player) * 5;
return this->equilibriumOracle(player, iterations, xOfI, xMinusI, addedCuts);
}
}
} else if (status == GRB_UNBOUNDED) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::preEquilibriumOracle: (P" << player
<< ") The problem is unbounded.";
throw ZEROException(ZEROErrorCode::Numeric, "Unbounded best response.");
}
return 0;
}
bool Algorithms::IPG::CutAndPlay::isPureStrategy() const {
if (this->Solved)
return this->Pure;
else
return false;
}
int Algorithms::IPG::CutAndPlay::equilibriumOracle(const unsigned int player,
const unsigned int iterations,
const arma::vec &xOfI,
const arma::vec &xMinusI,
int &addedCuts) {
ZEROAssert(player < this->IPG->NumPlayers);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") Starting separator";
// Store Membership LP outside the loop
this->updateMembership(player, xOfI);
auto dualMembership = this->Players.at(player)->MembershipLP.get();
GRBVar y[xOfI.size()]; // Dual membership variables
for (unsigned int i = 0; i < xOfI.size(); i++)
y[i] = dualMembership->getVarByName("alpha_" + std::to_string(i));
GRBVar betaVar = dualMembership->getVarByName("beta");
// Update the objective for the membership. Avoid doing it every time
// Update normalization
GRBLinExpr expr = -betaVar;
for (int j = 0; j < xOfI.size(); ++j)
expr += xOfI.at(j) * y[j];
dualMembership->setObjective(expr, GRB_MAXIMIZE);
// Store the playerModel outside the loop
std::unique_ptr<GRBModel> playerModel =
std::unique_ptr<GRBModel>(this->IPG->PlayersIP.at(player)->getIPModel(xMinusI, false));
GRBVar l[xOfI.size()]; // Dual membership variables
for (unsigned int i = 0; i < xOfI.size(); i++)
l[i] = playerModel->getVarByName("y_" + std::to_string(i));
playerModel->set(GRB_IntParam_OutputFlag, 0);
playerModel->set(GRB_IntParam_SolutionLimit, 100);
playerModel->set(GRB_IntParam_InfUnbdInfo, 1);
// Max number of iterations
for (int k = 0; k < iterations; ++k) {
// Check time at each iteration
if (this->IPG->Stats.AlgorithmData.TimeLimit.get() > 0) {
double remaining;
if (!this->checkTime(remaining) || remaining <= 0)
return 0;
}
// First, we check whether the point is a convex combination of feasible
// KNOWN points
// First iteration is out, since we have done it before.
if (k > 0)
this->updateMembership(player, xOfI);
//******DEBUG********
// dualMembership->write("Convex_" + std::to_string(player) + ".lp");
//******DEBUG********
// Optimize the PRLP
dualMembership->optimize();
int membershipStatus = dualMembership->get(GRB_IntAttr_Status);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") Membership status is " << membershipStatus;
// We should check the inequality on the player's model
ZEROAssert(membershipStatus == GRB_OPTIMAL);
double dualObj = dualMembership->getObjective().getValue();
// First: is the point redundant in the description?
// Namely, does xOfI belongs to the V-polyhedral approximation?
if (Utils::isEqual(dualObj, 0, this->Tolerance)) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") Feasible point. ";
this->Players.at(player)->Feasible = true;
arma::vec support;
support.zeros(this->Players.at(player)->VertexCounter);
for (unsigned int v = 0; v < this->Players.at(player)->VertexCounter; ++v) {
// abs to avoid misunderstanding with sign conventions
support.at(v) =
dualMembership->getConstrByName("V_" + std::to_string(v)).get(GRB_DoubleAttr_Pi);
}
if (Utils::isEqual(support.max(), 1, this->Tolerance)) {
this->Players.at(player)->Pure = true;
}
//******DEBUG********
// support.print("MNE Support: "+std::to_string(arma::sum(support)));
//******DEBUG********
return 1;
}
// Get the separating hyperplane
arma::vec alphaVal(xOfI.size(), arma::fill::zeros);
for (unsigned int i = 0; i < xOfI.size(); i++)
alphaVal.at(i) = y[i].get(GRB_DoubleAttr_X);
// Optimize the resulting inequality over the original feasible set
expr = 0;
for (unsigned int i = 0; i < xOfI.size(); ++i)
expr += alphaVal.at(i) * l[i];
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") DualMembership has " << dualMembership->get(GRB_IntAttr_SolCount)
<< " solution(s) with objective =" << dualObj << ".";
playerModel->setObjective(expr, GRB_MAXIMIZE);
playerModel->update();
playerModel->optimize();
int leaderStatus = playerModel->get(GRB_IntAttr_Status);
int numSols = playerModel->get(GRB_IntAttr_SolCount);
ZEROAssert((leaderStatus == GRB_OPTIMAL) || (leaderStatus == GRB_SUBOPTIMAL) ||
(leaderStatus == GRB_UNBOUNDED));
if (leaderStatus == GRB_OPTIMAL || (leaderStatus == GRB_SUBOPTIMAL && numSols > 0)) {
//@todo <numSols or 1?
for (int s = 0; s < numSols; ++s) {
playerModel->set(GRB_IntParam_SolutionNumber, s);
// The separating hyperplane plane evaluated at xOfI
double betaX = arma::as_scalar(alphaVal.t() * xOfI);
// The separating hyperplane evaluated at the optimal player's model
double betaPlayer = playerModel->getObjective().getValue();
// If the violation is negative: new vertex. Namely, the leader go further than xOfI
// If the violation is positive: cutting plane. Namely, xOfI is too far away
auto violation = betaX - betaPlayer;
//******DEBUG********
// xOfI.print("xOfI");
// alphaVal.print("alpha");
// dualMembership->write("Convex_" + std::to_string(player) + ".sol");
//******DEBUG********
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") Violation of " << violation;
if (violation >= this->Tolerance) {
// We have a cut.
// Ciao Moni
Utils::normalizeIneq(alphaVal, betaPlayer, true);
//******DEBUG********
// alphaVal.print("alphaVal with RHS of" + std::to_string(betaPlayer));
//******DEBUG********
this->Cuts.at(1).second += 1;
this->Players.at(player)->addCuts(arma::sp_mat{alphaVal.t()}, arma::vec{betaPlayer});
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") Adding a cut.";
addedCuts = 1;
return 0;
} else {
// We found a new vertex
arma::vec v(this->Players.at(player)->V.n_cols, arma::fill::zeros);
for (unsigned int i = 0; i < v.size(); ++i)
v[i] = l[i].get(GRB_DoubleAttr_X);
// v.print("vertex");
auto add = this->Players.at(player)->addVertex(v, numSols > 1);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player
<< ") adding vertex for Player (" << std::to_string(add) << "). "
<< (iterations - k - 1) << "/" << iterations << " iterations left";
}
}
} // status optimal for playerModel
else if (leaderStatus == GRB_UNBOUNDED) {
// Get the extreme ray
auto relaxed = playerModel->relax();
relaxed.optimize();
for (unsigned int i = 0; i < alphaVal.size(); ++i)
alphaVal[i] = relaxed.getVarByName("y_" + std::to_string(i)).get(GRB_DoubleAttr_UnbdRay);
alphaVal = Utils::normalizeVec(alphaVal);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumOracle: (P" << player << ") new ray";
this->Players.at(player)->addRay(alphaVal);
} // status unbounded for playerModel
}
// Iteration limit
return 2;
}
void Algorithms::IPG::CutAndPlay::updateMembership(const unsigned int &player,
const arma::vec &vertex) {
ZEROAssert(player < this->IPG->NumPlayers);
ZEROAssert(vertex.size() == this->IPG->PlayerVariables.at(player));
MathOpt::getDualMembershipLP(this->Players.at(player)->MembershipLP,
this->Players.at(player)->VertexCounter,
this->Players.at(player)->V,
this->Players.at(player)->RayCounter,
this->Players.at(player)->R,
vertex,
this->Players.at(player)->containsOrigin);
}
ZEROStatus
Algorithms::IPG::CutAndPlay::equilibriumLCP(double localTimeLimit, bool build, bool firstSolution) {
if (build) {
// Empty objects for market clearing
arma::sp_mat MC(0, this->IPG->NumVariables), dumA(0, this->IPG->NumVariables);
arma::vec MCRHS(0, arma::fill::zeros), dumB(0, arma::fill::zeros);
// Downcast the IP_Param to MP_Param
std::vector<std::shared_ptr<MathOpt::MP_Param>> MPCasted;
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
auto m = std::dynamic_pointer_cast<MathOpt::MP_Param>(this->Players.at(i)->ParametrizedIP);
MPCasted.push_back(m);
}
// Build the Nash Game
this->NashGame =
std::make_unique<Game::NashGame>(this->Env, MPCasted, MC, MCRHS, 0, dumA, dumB);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::equilibriumLCP: Formulated the NashGame";
// Build the LCP from the Nash Game
this->LCP = std::make_unique<MathOpt::LCP>(this->Env, *this->NashGame);
// Record some statistics
this->IPG->Stats.NumVar = this->LCP->getNumCols();
this->IPG->Stats.NumConstraints = this->LCP->getNumRows();
}
// Discriminate between Solver type and Objective type
auto Solver = this->IPG->getStatistics().AlgorithmData.LCPSolver.get();
auto ObjectiveType = this->IPG->Stats.AlgorithmData.Objective.get();
if (Solver == Data::LCP::Algorithms::PATH &&
ObjectiveType != Data::IPG::Objectives::Feasibility) {
LOG_S(WARNING)
<< "Algorithms::IPG::CutAndPlay::equilibriumLCP: The LCP's objective is used only "
"for computing the payoff. "
"PATH does not support this optimization.";
}
switch (ObjectiveType) {
case Data::IPG::Objectives::Linear: {
this->LCP->setMIPLinearObjective(this->LCP_c);
} break;
case Data::IPG::Objectives::Quadratic:
this->LCP->setMIPQuadraticObjective(this->LCP_c, this->LCP_Q);
break;
default:
this->LCP->setMIPFeasibilityObjective();
}
arma::vec x, z;
// Try to warm-start with the previous solution.
x = this->xLast;
z = this->zLast;
// Set the value to the incumbent MNE payoff.
double obj = -GRB_INFINITY;
int solLimit = firstSolution ? 1 : GRB_MAXINT;
int workers = 1;
if (this->IPG->Stats.AlgorithmData.Threads.get() >= 8)
workers = (int)(this->IPG->Stats.AlgorithmData.Threads.get());
auto LCPSolver = this->LCP->solve(Solver, x, z, localTimeLimit, workers, obj, solLimit);
if (LCPSolver == ZEROStatus::NashEqFound) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::equilibriumLCP: tentative Equilibrium found";
// Record the primal-dual solution and reset feasibility and pure-flag
// x.print("Whole x");
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
this->Players.at(i)->Incumbent =
x.subvec(this->NashGame->getPrimalLoc(i), this->NashGame->getPrimalLoc(i + 1) - 1);
// this->Players.at(i)->Incumbent.print("x of "+std::to_string(i));
this->Players.at(i)->DualIncumbent =
x.subvec(this->NashGame->getDualLoc(i), this->NashGame->getDualLoc(i + 1) - 1);
this->Players.at(i)->Feasible = false;
this->Players.at(i)->Pure = false;
}
// Compute the payoffs
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
this->Players.at(i)->Payoff = this->IPG->PlayersIP.at(i)->computeObjective(
this->Players.at(i)->Incumbent, this->buildXminusI(i), false);
}
// Record the last responses
this->xLast = x;
this->zLast = z;
this->objLast = obj;
return ZEROStatus::NashEqFound;
} else if (LCPSolver == ZEROStatus::Numerical) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::equilibriumLCP: Numerical errors.";
return ZEROStatus::Numerical;
} else if (LCPSolver == ZEROStatus::TimeLimit) {
if (this->IPG->Stats.Status.get() == ZEROStatus::Uninitialized)
this->IPG->Stats.Status.set(ZEROStatus::TimeLimit);
return ZEROStatus::TimeLimit;
} else {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::equilibriumLCP: No tentative equilibrium found";
return ZEROStatus::NashEqNotFound;
}
}
void Algorithms::IPG::CutAndPlay::initialize() {
// Set the number of iterations to zero.
this->IPG->Stats.NumIterations.set(0);
// Create the IPG_Player-s objects
this->Players = std::vector<std::unique_ptr<IPG_Player>>(this->IPG->NumPlayers);
// Initialize the working objects with respective values
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
// Initialize the IPG_Player
this->Players.at(i) =
std::make_unique<IPG_Player>(this->IPG->PlayersIP.at(i)->getNumVars(), this->Tolerance);
//@todo be aware of variables' changes in presolve
if (this->IPG->Stats.AlgorithmData.Presolve.get())
this->IPG->PlayersIP.at(i)->presolve();
// Add the working IP
auto WorkingIP = new MathOpt::IP_Param(*this->IPG->PlayersIP.at(i).get());
this->Players.at(i)->ParametrizedIP = std::make_shared<MathOpt::IP_Param>(*WorkingIP);
// Add the working MembershipLP
this->Players.at(i)->MembershipLP = std::move(std::make_unique<GRBModel>(*this->Env));
this->initializeCoinModel(i);
}
// Initialize some "educated" guesses as best responses.
this->initializeEducatedGuesses();
// Reset cuts statistics
this->Cuts = {std::pair<std::string, int>("Value", 0),
std::pair<std::string, int>("VPoly", 0),
std::pair<std::string, int>("MIPCuts", 0)};
this->IPG->Stats.AlgorithmData.Cuts.set(this->Cuts);
// Initialize the LCP objectives for further use.
this->initLCPObjective();
}
arma::vec Algorithms::IPG::CutAndPlay::buildXminusI(const unsigned int i) {
ZEROAssert(i < this->IPG->NumPlayers);
arma::vec xMinusI(this->IPG->NumVariables - this->IPG->PlayerVariables.at(i), arma::fill::zeros);
unsigned int counter = 0;
for (unsigned int j = 0; j < this->IPG->NumPlayers; ++j) {
if (i != j) {
xMinusI.subvec(counter, counter + this->IPG->PlayerVariables.at(j) - 1) =
this->Players.at(j)->Incumbent;
counter += this->IPG->PlayerVariables.at(j);
}
}
return xMinusI;
}
unsigned int Algorithms::IPG::CutAndPlay::externalCutGenerator(unsigned int player,
int maxCuts,
bool rootNode,
bool cutOff) {
ZEROAssert(player < this->IPG->NumPlayers);
auto xOfI = this->Players.at(player)->Incumbent;
auto xOfIDual = this->Players.at(player)->DualIncumbent;
auto xMinusI = this->buildXminusI(player);
arma::vec objective;
objective = (this->Players.at(player)->ParametrizedIP->getC() * xMinusI) +
this->Players.at(player)->ParametrizedIP->getc();
auto CoinModel = this->Players.at(player)->CoinModel;
auto numVars = this->Players.at(player)->ParametrizedIP->getB(false).n_cols;
/******RECURSIVE CUT GENERATION********
Uncomment this to enable recursive cut generation. In general, not a good idea for numerical
stability auto numConstrs = this->Players.at(player)->ParametrizedIP->getB(false).n_rows;
*******RECURSIVE CUT GENERATION********/
auto numConstrs = this->IPG->PlayersIP.at(player)->getB(false).n_rows;
// Empty objects, for root node cuts
if (rootNode) {
xOfI.zeros(numVars);
xOfIDual.zeros(numConstrs);
}
auto primal = new double[numVars];
auto dual = new double[numConstrs];
auto c = new double[numVars];
for (unsigned int i = 0; i < numVars; ++i) {
c[i] = objective.at(i);
if (!rootNode)
primal[i] = xOfI.at(i);
}
for (unsigned int i = 0; i < numConstrs; ++i) {
if (!rootNode)
dual[i] = xOfIDual.at(i);
}
CoinModel->setObjective(c);
if (!rootNode) {
CoinModel->setColSolution(primal);
CoinModel->setRowPrice(dual);
}
//******DEBUG********
// CoinModel->writeLp("CoinModel");
// this->IPG->PlayersIP.at(player)->getIPModel(xMinusI)->write("GurobiModel.lp");
//******DEBUG********
try {
CoinModel->solveFromSol();
if (!rootNode) {
auto solCheck = CoinModel->getColSolution();
for (unsigned int i = 0; i < numVars; ++i) {
if (!Utils::isEqual(primal[i], solCheck[i]))
throw;
}
solCheck = CoinModel->getRowPrice();
for (unsigned int i = 0; i < numConstrs; ++i) {
if (!Utils::isEqual(dual[i], solCheck[i]))
throw;
}
}
auto candidateCuts = new OsiCuts;
CglTreeInfo info = CglTreeInfo();
info.inTree = false;
info.options = 4;
info.pass = 0;
CglKnapsackCover kpGen;
auto KPs = new OsiCuts;
kpGen.setGlobalCuts(true);
kpGen.setAggressiveness(100);
kpGen.switchOnExpensive();
kpGen.setMaxInKnapsack(xOfI.size() + 10);
kpGen.generateCuts(*CoinModel, *KPs, info);
for (int(i) = 0; (i) < KPs->sizeCuts(); ++(i))
if (KPs->rowCut(i).globallyValid())
candidateCuts->insert(KPs->rowCut(i));
CglMixedIntegerRounding2 MIRGen;
auto MIRs = new OsiCuts;
MIRGen.setGlobalCuts(true);
MIRGen.setAggressiveness(100);
MIRGen.setDoPreproc(1);
MIRGen.setMAXAGGR_(100);
MIRGen.generateCuts(*CoinModel, *MIRs, info);
for (int(i) = 0; (i) < MIRs->sizeCuts(); ++(i))
if (MIRs->rowCut(i).globallyValid())
candidateCuts->insert(MIRs->rowCut(i));
CglGMI GMIGen;
auto GMIs = new OsiCuts;
if (!cutOff) {
GMIGen.getParam().setMAX_SUPPORT(numVars);
GMIGen.getParam().setMAX_SUPPORT_REL(0.8);
GMIGen.getParam().setMAXDYN(CoinModel->getInfinity());
}
GMIGen.getParam().setENFORCE_SCALING(true);
GMIGen.setGlobalCuts(true);
GMIGen.setAggressiveness(100);
if (cutOff) {
GMIGen.getParam().setAway(1e-4);
GMIGen.getParam().setMINVIOL(1e-3);
}
GMIGen.generateCuts(*CoinModel, *GMIs, info);
for (int(i) = 0; (i) < GMIs->sizeCuts(); ++(i))
if (GMIs->rowCut(i).globallyValid())
candidateCuts->insert(GMIs->rowCut(i));
//******DEBUG********
// KPs->printCuts();
// GMIs->printCuts();
// MIRs->printCuts();
//******DEBUG********
// Sort by effectiveness
candidateCuts->sort();
if (cutOff) {
// Try also simple gomory, and verify the cutoff
CglGomory gomoryGen;
auto GMs = new OsiCuts;
gomoryGen.setAggressiveness(100);
gomoryGen.setLimit(xOfI.size() + 10);
gomoryGen.setLimitAtRoot(xOfI.size() + 10);
gomoryGen.setAway(1e-3);
gomoryGen.generateCuts(*CoinModel, *GMs, info);
for (int(i) = 0; (i) < GMs->sizeCuts(); ++(i))
if (GMs->rowCut(i).globallyValid())
candidateCuts->insert(GMs->rowCut(i));
candidateCuts->sort();
// We need to be sure to get only the cuts that are actively cutting off the solution.
for (int i = 0; i < candidateCuts->sizeCuts(); ++i) {
// Check if we have a violation. Otherwise, erase the cut.
double violation = candidateCuts->rowCut(i).violated(primal);
if (violation <= 0) {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::externalCutGenerator: (P" << player
<< ") Discarding a cut (no violation).";
candidateCuts->eraseRowCut(i);
}
}
}
// Minimum among the candidate cuts and the maximum number of cuts
auto numCuts = std::min(candidateCuts->sizeCuts(), maxCuts);
if (numCuts > 0) {
// The final cut matrix and RHS.
arma::sp_mat LHS;
LHS.zeros(numCuts, numVars);
arma::vec RHS(numCuts, arma::fill::zeros);
// Iterate over the candidate cuts (in max numCuts iterations), sorted by efficacy
for (int i = 0; i < numCuts; ++i) {
// Get the cut
auto cut = candidateCuts->rowCut(i);
// Get the row from the cut
auto row = cut.row();
// Get the indices and the elements
auto indices = row.getIndices();
auto elements = row.getElements();
// Iterate over the elements to get the index-value pair
for (int j = 0; j < row.getNumElements(); j++)
LHS.at(i, indices[j]) = elements[j];
// Discern among the cut senses
auto sense = cut.sense();
switch (cut.sense()) {
case 'E': {
// Equality. Two cuts
LHS.resize(LHS.n_rows + 1, LHS.n_cols);
RHS.resize(RHS.n_rows + 1);
LHS.row(LHS.n_rows - 1) = -LHS.row(i);
RHS.at(RHS.n_rows - 1) = -cut.rhs();
RHS.at(i) = cut.rhs();
} break;
case 'G': {
// Switch signs
RHS.at(i) = -RHS.at(i);
RHS.at(i) = -cut.rhs();
} break;
case 'L': {
// Just complete the RHS
RHS.at(i) = cut.rhs();
} break;
case 'R': {
// Ranged inequality. We have both upper and lower bound
RHS.at(i) = cut.rhs();
} break;
default: {
candidateCuts->printCuts();
throw ZEROException(ZEROErrorCode::InvalidData, "Unknown cut sense.");
}
}
}
// If we got some new cuts, add them to the working integer program
// Add the cuts
this->Players.at(player)->addCuts(LHS, RHS);
// Update the statistics
this->Cuts.at(2).second += numCuts;
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::externalCutGenerator: (P" << player << ") Added "
<< numCuts << " and generated: " << MIRs->sizeCuts() << " MIRs - "
<< KPs->sizeCuts() << " KPs - " << GMIs->sizeCuts() << " GMIs.";
} else {
LOG_S(INFO) << "Algorithms::IPG::CutAndPlay::externalCutGenerator: (P" << player
<< ") No cuts added.";
}
return numCuts;
} catch (CoinError &e) {
throw ZEROException(ZEROErrorCode::SolverError,
"Invalid Coin-OR interface response: " + e.message());
}
}
void Algorithms::IPG::CutAndPlay::initializeEducatedGuesses() {
// Reset the working strategies to a pure strategy given by the IP
// Push back the IP_Param copies in WorkingIPs
// For any player
for (unsigned int i = 0; i < this->IPG->NumPlayers; ++i) {
// Equals to NumVarsI by definition
unsigned int NumVarsI = this->IPG->PlayerVariables.at(i);
// xMinusI
arma::vec xMinusI = this->buildXminusI(i);
// Update working strategies with "educated guesses"
auto PureIP = this->IPG->PlayersIP.at(i)->getIPModel(xMinusI, false);
PureIP->optimize();
int status = PureIP->get(GRB_IntAttr_Status);
if (status == GRB_INFEASIBLE) {
// Game ended, player is infeasible
this->IPG->Stats.Status.set(ZEROStatus::NashEqNotFound);
this->Infeasible = true;
return;
} else {
// Model is not infeasible. We can have either a ray or a vertex
if (status == GRB_OPTIMAL) {
// We have a vertex
for (unsigned int k = 0; k < NumVarsI; ++k)
this->Players.at(i)->Incumbent.at(k) =
PureIP->getVarByName("y_" + std::to_string(k)).get(GRB_DoubleAttr_X);
this->Players.at(i)->addVertex(this->Players.at(i)->Incumbent, false);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::initializeEducatedGuesses: (P" << i
<< ") Adding a vertex.";
}
else if (status == GRB_UNBOUNDED) {
// Relax and get the ray
GRBModel relaxed = PureIP->relax();
relaxed.set(GRB_IntParam_InfUnbdInfo, 1);
relaxed.set(GRB_IntParam_DualReductions, 0);
relaxed.optimize();
arma::vec ray(NumVarsI, arma::fill::zeros);
for (unsigned int k = 0; k < NumVarsI; ++k) {
ray.at(k) = relaxed.getVarByName("y_" + std::to_string(k)).get(GRB_DoubleAttr_UnbdRay);
this->Players.at(i)->addRay(ray, false);
LOG_S(2) << "Algorithms::IPG::CutAndPlay::initializeEducatedGuesses(): (P" << i
<< ") Adding a ray";
}
}
// Check if the origin is feasible
arma::vec zeros(this->IPG->PlayersIP.at(i)->getNumVars(), arma::fill::zeros);
double zero = this->IPG->PlayersIP.at(i)->computeObjective(zeros, xMinusI, true);
if (zero != GRB_INFINITY)
this->Players.at(i)->containsOrigin = true;
}
}
}
void Algorithms::IPG::CutAndPlay::initializeCoinModel(const unsigned int player) {
ZEROAssert(player < this->IPG->NumPlayers);
// Source the main ingredients. Avoid getting B with bounds, since we already have the raw bounds.
auto IP_B = this->Players.at(player)->ParametrizedIP->getB(false);
auto IP_Bounds = this->Players.at(player)->ParametrizedIP->getBounds();
auto IP_b = this->Players.at(player)->ParametrizedIP->getb(false);
auto IP_Integers = this->Players.at(player)->ParametrizedIP->getIntegers();
auto IP_numVars = this->IPG->PlayersIP.at(player)->getNumVars();
auto IP_numConstrs = IP_B.n_rows;
// Convert B
auto B = Utils::armaToCoinSparse(IP_B);
// Double objects
auto c = new double[IP_numVars];
auto b = new double[IP_numConstrs];
auto lb = new double[IP_numVars];
auto ub = new double[IP_numVars];
// Filling stage
for (unsigned int i = 0; i < IP_numVars; ++i) {
c[i] = 0;
lb[i] = IP_Bounds.at(i).first > 0 ? IP_Bounds.at(i).first : 0;
ub[i] = IP_Bounds.at(i).second >= 0 ? IP_Bounds.at(i).second : GRB_INFINITY;
}
for (unsigned int i = 0; i < IP_numConstrs; ++i)
b[i] = IP_b.at(i);
// Solver interface
auto CoinModel = new OsiGrbSolverInterface();
// Remember to use the same number of threads...
GRBsetintparam(CoinModel->getEnvironmentPtr(), "Threads", this->Env->get(GRB_IntParam_Threads));
// Load the problem from the given objects
CoinModel->loadProblem(B, lb, ub, c, nullptr, b);
// Set the integer variables
for (unsigned int i = 0; i < IP_Integers.size(); ++i)
CoinModel->setInteger(IP_Integers.at(i));
// Reset the log level to zero
CoinModel->messageHandler()->setLogLevel(0);
// Move to the target Player
this->Players.at(player)->CoinModel = std::make_shared<OsiGrbSolverInterface>(*CoinModel);
}