From a6a4439c322b3d18d635afe8dbce4dfb58825f47 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 08:58:54 -0400 Subject: [PATCH 01/14] Draft support for arbitrary-precision numerical data in (B)AGGs --- src/games/agg/agg.cc | 176 ++++++++++++++++++++++++++++++++++--- src/games/agg/agg.h | 82 +++++++++++++++-- src/games/agg/bagg.cc | 107 ++++++++++++++++++++-- src/games/agg/bagg.h | 15 +++- src/games/agg/trie_map.cc | 3 + src/games/agg/trie_map.imp | 6 +- src/games/gameagg.cc | 53 +++++++---- src/games/gameagg.h | 4 +- src/games/gamebagg.cc | 51 +++++++---- src/games/gamebagg.h | 4 +- src/games/number.h | 7 ++ tests/test_agg.py | 92 +++++++++++++++++++ tests/test_nash.py | 5 +- 13 files changed, 533 insertions(+), 72 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 91e495c88c..5995f6984e 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -37,12 +37,15 @@ AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNod vector> &_actionSets, vector> &neighb, vector &projTypes, vector> &projS, vector>> &proj, vector> &projF, - vector>> &Po, vector &P, vector &_payoffs) + vector>> &Po, vector &P, vector &_payoffs, + vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), actionSets(_actionSets), neighbors(neighb), projectionTypes(projTypes), payoffs(_payoffs), - projection(proj), projectedStrat(projS), fullProjectedStrat(projS), projFunctions(projF), - Porder(Po), Pr(P), isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), - cache(numPlayers + 1), player2Class(numPlayers), kSymStrategyOffset(1, 0) + exactPayoffs(_exactPayoffs), projection(proj), projectedStrat(projS), + exactProjectedStrat(numANodes, vector(numPlayers)), exactPr(numPlayers), + fullProjectedStrat(projS), projFunctions(projF), Porder(Po), Pr(P), isPure(numANodes, true), + node2Action(numANodes, vector(numPlayers)), cache(numPlayers + 1), + player2Class(numPlayers), kSymStrategyOffset(1, 0) { // actions @@ -236,7 +239,8 @@ std::shared_ptr AGG::makeAGG(istream &in) vector>> Po(n); vector Pr(n); - vector pays(S); // payoffs + vector pays(S); // payoffs + vector exactPays(S); // exact (arbitrary-precision) counterpart of pays set> doneASets; for (i = 0; i < n; i++) { @@ -279,10 +283,10 @@ std::shared_ptr AGG::makeAGG(istream &in) } switch (t) { case COMPLETE: - AGG::makeCOMPLETEpayoff(in, pays[i]); + AGG::makeCOMPLETEpayoff(in, pays[i], exactPays[i]); break; case MAPPING: - AGG::makeMAPPINGpayoff(in, pays[i], neighb[i].size()); + AGG::makeMAPPINGpayoff(in, pays[i], exactPays[i], neighb[i].size()); break; case ADDITIVE: default: @@ -290,7 +294,7 @@ std::shared_ptr AGG::makeAGG(istream &in) } } return std::make_shared(n, size, S, P, ASets, neighb, projTypes, projS, proj, projF, Po, Pr, - pays); + pays, exactPays); } void AGG::setProjections(vector> &projS, vector>> &proj, @@ -434,6 +438,53 @@ void AGG::doProjection(int Node, int i, AggNumber *s) } } +// Exact (Rational) counterparts of computeP/doProjection above. The action graph structure +// (projection, Porder, projFunctions, actionSets, neighbors) is purely combinatorial -- built +// once from integer data, independent of payoff/probability values -- so it's shared as-is; +// only the numeric working state (Pr/projectedStrat) needs a separate Rational-valued copy. +void AGG::computeExactP(int player, int act, int player2, int act2) +{ + exactPr[0].reset(); + exactPr[0].insert(make_pair(projection[actionSets[player][act]][player][act], Rational(1))); + + const int numNei = neighbors[actionSets[player][act]].size(); + for (int k = 1; k < numPlayers; k++) { + exactPr[k].reset(); + if (Porder[player][act][k] == player2) { + if (act2 == -1) { + exactPr[k] = exactPr[k - 1]; + } + else { + exactdistrib temp; + temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], Rational(1))); + exactPr[k].multiply(exactPr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); + } + } + else { + exactPr[k].multiply(exactPr[k - 1], + exactProjectedStrat[actionSets[player][act]][Porder[player][act][k]], + numNei, projFunctions[actionSets[player][act]]); + } + } +} + +void AGG::doExactProjection(int Node, Rational *s) +{ + for (int i = 0; i < numPlayers; i++) { + doExactProjection(Node, i, &(s[firstAction(i)])); + } +} + +void AGG::doExactProjection(int Node, int i, Rational *s) +{ + exactProjectedStrat[Node][i].reset(); + for (int j = 0; j < actions[i]; j++) { + if (s[j] > Rational(0)) { + exactProjectedStrat[Node][i] += make_pair(projection[Node][i][j], s[j]); + } + } +} + AggNumber AGG::getPurePayoff(int player, const std::vector &s) { assert(player >= 0 && player < numPlayers); @@ -457,6 +508,29 @@ AggNumber AGG::getPurePayoff(int player, const std::vector &s) return p->second; } +Number AGG::getExactPurePayoff(int player, const std::vector &s) const +{ + assert(player >= 0 && player < numPlayers); + const int Node = actionSets[player][s[player]]; + const int keylen = neighbors[Node].size(); + config pureprofile(projection[Node][0][s[0]]); + for (int i = 1; i < numPlayers; i++) { + for (int j = 0; j < keylen; j++) { + pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); + } + } + auto p = exactPayoffs[Node].find(pureprofile); + if (p == exactPayoffs[Node].end()) { + std::stringstream str; + str << "AGG::getExactPurePayoff ERROR: unable to find the following configuration "; + str << "["; + copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); + str << "] in payoffs of action node #" << Node; + throw std::runtime_error(str.str()); + } + return p->second; +} + AggNumber AGG::getMixedPayoff(int player, StrategyProfile &s) { AggNumber result = 0.0; @@ -492,6 +566,45 @@ AggNumber AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfil return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); } +Rational AGG::exactInnerProd(int node, const exactdistrib &dist) const +{ + Rational result(0); + for (const auto &entry : dist) { + const auto it = exactPayoffs[node].find(entry.first); + if (it != exactPayoffs[node].end() && entry.second != Rational(0)) { + result += entry.second * (Rational)it->second; + } + } + return result; +} + +Rational AGG::getExactV(int player, int act, const ExactStrategyProfile &s) +{ + doExactProjection(actionSets.at(player).at(act), s); + computeExactP(player, act); + return exactInnerProd(actionSets[player][act], exactPr[numPlayers - 1]); +} + +Rational AGG::getExactJ(int player1, int act1, int player2, int act2, + const ExactStrategyProfile &s) +{ + doExactProjection(actionSets[player1][act1], s); + computeExactP(player1, act1, player2, act2); + return exactInnerProd(actionSets[player1][act1], exactPr[numPlayers - 1]); +} + +Rational AGG::getExactMixedPayoff(int player, const ExactStrategyProfile &s) +{ + Rational result(0); + assert(player >= 0 && player < numPlayers); + for (int act = 0; act < actions[player]; ++act) { + if (s[act + firstAction(player)] > Rational(0)) { + result += s[act + firstAction(player)] * getExactV(player, act, s); + } + } + return result; +} + // getSymMixedPayoff: compute expected payoff under a symmetric mixed strat, // for a symmetric game. // parameter: s is the mixed strategy of one player. It is a vector of @@ -559,7 +672,7 @@ AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) GrayComposition gc(numPlayers - 1, support.size()); AggNumber prob = - pow((support.at(0) >= 0) ? s[neighbors[node][support[0]]] : null_prob, numPlayers - 1); + std::pow((support.at(0) >= 0) ? s[neighbors[node][support[0]]] : null_prob, numPlayers - 1); while (true) { const vector &comp = gc.get(); @@ -680,7 +793,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int const AggNumber prob0 = (support.at(0) >= 0) ? s[node2Action[neighbors[node].at(support[0])][p]] : null_prob; - AggNumber prob = pow(prob0, numPl); + AggNumber prob = std::pow(prob0, numPl); while (true) { const vector &comp = gc.get(); @@ -801,11 +914,10 @@ AggNumber AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act return d.inner_prod(payoffs[uniqueActionSets[pClass1][act1]]); } -void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) +void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int numNei) { int num; char c; - AggNumber u; aggpayoff temp; // temp.swap(pay); temp = pay; @@ -850,13 +962,16 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) " expected. Instead, got " + std::to_string(c)); } - in >> u; // get payoff + std::string word; + in >> word; // get payoff if (!in.good()) { std::stringstream str; str << "Error trying to read the utility value for configuration "; copy(key.begin(), key.end(), ostream_iterator(str, " ")); throw std::runtime_error(str.str()); } + const Number num(word); + const AggNumber u = (double)num; // insert const pair::iterator, bool> r = pay.insert(make_pair(key, u)); @@ -867,6 +982,13 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) str << "]"; throw std::runtime_error(str.str()); } + if (!exactPay.insert(make_pair(key, num)).second) { + std::stringstream str; + str << "ERROR: overwriting utility at ["; + copy(key.begin(), key.end(), ostream_iterator(str, " ")); + str << "]"; + throw std::runtime_error(str.str()); + } } // check for (auto &it : temp) { @@ -904,4 +1026,32 @@ AggNumber AGG::getMinPayoff() const return result; } +Number AGG::getExactMaxPayoff() const +{ + assert(numActionNodes > 0); + Number result = exactPayoffs[0].begin()->second; + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + if ((Rational)it.second > (Rational)result) { + result = it.second; + } + } + } + return result; +} + +Number AGG::getExactMinPayoff() const +{ + assert(numActionNodes > 0); + Number result = exactPayoffs[0].begin()->second; + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + if ((Rational)it.second < (Rational)result) { + result = it.second; + } + } + } + return result; +} + } // namespace Gambit::agg diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index efe01cb560..c6d232d354 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -25,10 +25,12 @@ #define GAMBIT_AGG_AGG_H #include +#include #include #include #include "proj_func.h" #include "trie_map.h" +#include "games/number.h" namespace Gambit { @@ -46,9 +48,23 @@ using AggNumberVector = std::vector; // data structure for payoff function: using aggpayoff = trie_map; +// exact (arbitrary-precision) counterpart of a payoff function, populated alongside aggpayoff +// at parse time. Only used for storage/lookup (find/insert/iterate), never for the +// floating-point convolution machinery, so a plain std::map suffices -- no need to fight +// trie_map's explicit whole-class instantiation, which requires V to support arithmetic +// operators that Number intentionally does not provide. +using exactpayoff = std::map, Number>; + // data struct for prob distribution over configurations: using aggdistrib = trie_map; +// exact (arbitrary-precision) counterparts of the above, used by the exact mixed-strategy +// payoff computation (getExactMixedPayoff/getExactV/getExactJ). Unlike exactpayoff, these +// participate in the convolution algorithm (multiply/inner_prod), so they need Rational's +// arithmetic -- which, unlike Number, it provides -- and so can reuse trie_map directly. +using ExactStrategyProfile = std::vector; +using exactdistrib = trie_map; + // types of input formats for payoff func using payofftype = enum { COMPLETE, MAPPING, ADDITIVE }; @@ -74,7 +90,8 @@ class AGG { std::vector &projTypes, std::vector> &projS, std::vector>> &proj, std::vector> &projF, std::vector>> &Po, - std::vector &P, std::vector &payoffs); + std::vector &P, std::vector &payoffs, + std::vector &exactPayoffs); ~AGG() = default; @@ -100,7 +117,15 @@ class AGG { AggNumber getV(int player, int action, const StrategyProfile &s); AggNumber getJ(int player, int action, int player2, int action2, StrategyProfile &s); + // exact counterparts of the above three, computing via the same convolution algorithm but + // in Rational arithmetic throughout, rather than double. + Rational getExactMixedPayoff(int player, const ExactStrategyProfile &s); + Rational getExactV(int player, int action, const ExactStrategyProfile &s); + Rational getExactJ(int player, int action, int player2, int action2, + const ExactStrategyProfile &s); + AggNumber getPurePayoff(int player, const std::vector &s); + Number getExactPurePayoff(int player, const std::vector &s) const; bool isSymmetric() const { @@ -143,11 +168,17 @@ class AGG { AggNumber getMaxPayoff() const; AggNumber getMinPayoff() const; + Number getExactMaxPayoff() const; + Number getExactMinPayoff() const; void printPayoffs(std::ostream &s, int node) const { - s << payoffs.at(node).size() << std::endl; - s << payoffs[node]; + s << exactPayoffs.at(node).size() << std::endl; + for (const auto &entry : exactPayoffs.at(node)) { + s << "[ "; + std::copy(entry.first.begin(), entry.first.end(), std::ostream_iterator(s, " ")); + s << "] " << entry.second << std::endl; + } } void printActionGraph(std::ostream &s) const @@ -188,6 +219,9 @@ class AGG { // payoff function for each action node \in S std::vector payoffs; + // exact (arbitrary-precision) counterpart of payoffs, for each action node \in S + std::vector exactPayoffs; + // auxiliary data structures // originally: @@ -199,6 +233,13 @@ class AGG { // which is a prob distribution over the set of 'contributions' std::vector> projectedStrat; + // exact (Rational) counterpart of projectedStrat/Pr below, used only by + // getExactMixedPayoff/getExactV/getExactJ. Independent working state from the double + // versions, since both may be in use (e.g. from different MixedStrategyProfile instantiations + // over the same AGG) and each is rebuilt fresh per call regardless. + std::vector> exactProjectedStrat; + std::vector exactPr; + // foreach s in S, i in N, the full set of projected actions. std::vector> fullProjectedStrat; @@ -244,15 +285,26 @@ class AGG { // input functor struct input { - explicit input(std::istream &i) : in(i) {} - void operator()(aggpayoff::iterator p) { in >> (*p).second; } + input(std::istream &i, exactpayoff &e) : in(i), exact(e) {} + void operator()(aggpayoff::iterator p) + { + std::string word; + in >> word; + const Number num(word); + p->second = (double)num; + exact.insert(std::make_pair(p->first, num)); + } std::istream ∈ + exactpayoff &exact; }; // private static methods: - static void makeCOMPLETEpayoff(std::istream &in, aggpayoff &pay) { pay.in_order(input(in)); } - static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int); + static void makeCOMPLETEpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay) + { + pay.in_order(input(in, exactPay)); + } + static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int); static void setProjections(std::vector> &projS, std::vector>> &proj, int N, int S, @@ -278,6 +330,22 @@ class AGG { void doProjection(int Node, AggNumber *s); void doProjection(int Node, int player, AggNumber *s); + // exact (Rational) counterparts, mirroring the above precisely. exactPayoffs is a + // std::map<..., Number>, not a trie_map, so trie_map's own inner_prod can't be + // used directly against it -- this does the equivalent sum manually. + Rational exactInnerProd(int node, const exactdistrib &dist) const; + void computeExactP(int player, int act, int player2 = -1, int act2 = -1); + void doExactProjection(int Node, const ExactStrategyProfile &s) + { + doExactProjection(Node, &(const_cast(s)[0])); + } + void doExactProjection(int Node, int player, const ExactStrategyProfile &s) + { + doExactProjection(Node, player, &(const_cast(s)[firstAction(player)])); + } + void doExactProjection(int Node, Rational *s); + void doExactProjection(int Node, int player, Rational *s); + void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, aggdistrib &dest, int plClass2 = -1, int act2 = -1); }; diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index 12d36de06a..c46c5db15f 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -11,10 +11,11 @@ using namespace std; namespace Gambit::agg { BAGG::BAGG(int N, int S, vector &numTypes, vector &TDist, - vector>> &typeActionSets, vector>> &ta2a, - std::shared_ptr aggPtr) + vector> &exactTDist, vector>> &typeActionSets, + vector>> &ta2a, std::shared_ptr aggPtr) : numPlayers(N), numActionNodes(S), numTypes(numTypes), indepTypeDist(TDist), - typeActionSets(typeActionSets), typeAction2ActionIndex(ta2a), aggPtr(aggPtr) + exactIndepTypeDist(exactTDist), typeActionSets(typeActionSets), typeAction2ActionIndex(ta2a), + aggPtr(aggPtr) { typeOffset = std::vector(numPlayers + 1); typeOffset[0] = 0; @@ -94,13 +95,18 @@ std::shared_ptr BAGG::makeBAGG(istream &in) // input the type distributions stripComment(in); vector TDist; + vector> exactTDist(N); for (int i = 0; i < N; ++i) { TDist.emplace_back(numTypes[i]); for (int j = 0; j < numTypes[i]; ++j) { - in >> TDist[i][j]; + std::string word; + in >> word; if (!in) { throw std::runtime_error("Error in game file: number expected for type distribution"); } + const Number num(word); + TDist[i][j] = (double)num; + exactTDist[i].push_back(num); } } @@ -175,8 +181,8 @@ std::shared_ptr BAGG::makeBAGG(istream &in) } const std::shared_ptr aggPtr = AGG::makeAGG(aggss); - return std::make_shared(N, S, numTypes, TDist, typeActionSets, typeAction2ActionIndex, - aggPtr); + return std::make_shared(N, S, numTypes, TDist, exactTDist, typeActionSets, + typeAction2ActionIndex, aggPtr); } AggNumber BAGG::getMixedPayoff(int player, StrategyProfile &s) @@ -236,6 +242,49 @@ AggNumber BAGG::getV(int player, int tp, int action, const StrategyProfile &s) return aggPtr->getV(player, typeAction2ActionIndex[player][tp][action], as); } +// Exact (Rational) counterparts of getAGGStrat/getV/getMixedPayoff above. +void BAGG::getExactAGGStrat(ExactStrategyProfile &as, const ExactStrategyProfile &s, int player, + int tp, int action) const +{ + for (int i = 0; i < aggPtr->getNumActions(); ++i) { + as[i] = Rational(0); + } + + for (int pl = 0; pl < numPlayers; ++pl) { + if (pl != player) { + for (int t = 0; t < numTypes[pl]; ++t) { + for (size_t act = 0; act < typeActionSets[pl][t].size(); ++act) { + const int aact = typeAction2ActionIndex[pl][t][act]; + as[aact + aggPtr->firstAction(pl)] += + (Rational)exactIndepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + } + } + } + else { + const int aact = typeAction2ActionIndex[player][tp][action]; + as[aact + aggPtr->firstAction(player)] = Rational(1); + } + } +} + +Rational BAGG::getExactV(int player, int tp, int action, const ExactStrategyProfile &s) const +{ + ExactStrategyProfile as(aggPtr->getNumActions()); + getExactAGGStrat(as, s, player, tp, action); + return aggPtr->getExactV(player, typeAction2ActionIndex[player][tp][action], as); +} + +Rational BAGG::getExactMixedPayoff(int player, int tp, const ExactStrategyProfile &s) const +{ + Rational res(0); + for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { + if (s[act + firstAction(player, tp)] > Rational(0)) { + res += s[act + firstAction(player, tp)] * getExactV(player, tp, act, s); + } + } + return res; +} + AggNumber BAGG::getPurePayoff(int player, int tp, std::vector &ps) { StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); @@ -248,6 +297,50 @@ AggNumber BAGG::getPurePayoff(int player, int tp, std::vector &ps) return getMixedPayoff(player, tp, st); } +// Unlike getPurePayoff() above, this does NOT delegate to the general mixed-payoff convolution +// engine. A BAGG pure profile only leaves the OTHER players' realized types uncertain (each +// independent, per exactIndepTypeDist), not their actions -- so the expectation is a small, +// exact, finite sum over the Cartesian product of the other players' types (bounded by the +// product of their type counts, not their action counts), each term an exact AGG-level pure +// payoff via AGG::getExactPurePayoff. +Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps) const +{ + std::vector others; + for (int pl = 0; pl < numPlayers; ++pl) { + if (pl != player) { + others.push_back(pl); + } + } + + std::vector sAGG(numPlayers); + sAGG[player] = typeAction2ActionIndex[player][tp][ps[typeOffset[player] + tp]]; + + std::vector typeIndex(others.size(), 0); + Rational total(0); + while (true) { + Rational weight(1); + for (size_t k = 0; k < others.size(); ++k) { + const int pl = others[k]; + const int t = typeIndex[k]; + sAGG[pl] = typeAction2ActionIndex[pl][t][ps[typeOffset[pl] + t]]; + weight *= (Rational)exactIndepTypeDist[pl][t]; + } + total += weight * (Rational)aggPtr->getExactPurePayoff(player, sAGG); + + size_t k = 0; + for (; k < others.size(); ++k) { + if (++typeIndex[k] < numTypes[others[k]]) { + break; + } + typeIndex[k] = 0; + } + if (k == others.size()) { + break; + } + } + return total; +} + void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) { for (int i = 0; i < aggPtr->getNumActionNodes(); ++i) { @@ -309,7 +402,7 @@ ostream &operator<<(ostream &s, const BAGG &g) // type distrib for (int i = 0; i < g.getNumPlayers(); ++i) { for (int j = 0; j < g.getNumTypes(i); ++j) { - s << g.indepTypeDist[i][j] << " "; + s << g.exactIndepTypeDist[i][j] << " "; } s << endl; } diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 21a51e54fc..cc1fa54d71 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -38,14 +38,15 @@ using ProbDist = std::vector; class BAGG { public: - friend class Gambit::BAGGPureStrategyProfileRep; - template friend class Gambit::BAGGMixedStrategyProfileRep; + friend class BAGGPureStrategyProfileRep; + template friend class BAGGMixedStrategyProfileRep; friend std::ostream &operator<<(std::ostream &s, const BAGG &g); static std::shared_ptr makeBAGG(std::istream &in); BAGG(int N, int S, std::vector &numTypes, std::vector &TDist, + std::vector> &exactTDist, std::vector>> &typeActionSets, std::vector>> &ta2a, std::shared_ptr aggPtr); @@ -64,6 +65,8 @@ class BAGG { AggNumber getMaxPayoff() const { return aggPtr->getMaxPayoff(); } AggNumber getMinPayoff() const { return aggPtr->getMinPayoff(); } + Number getExactMaxPayoff() const { return aggPtr->getExactMaxPayoff(); } + Number getExactMinPayoff() const { return aggPtr->getExactMinPayoff(); } // exp. payoff under mixed strat profile AggNumber getMixedPayoff(int player, StrategyProfile &s); @@ -73,6 +76,10 @@ class BAGG { void getPayoffVector(AggNumberVector &dest, int player, int tp, const StrategyProfile &s); AggNumber getV(int player, int tp, int action, const StrategyProfile &s); + // exact counterparts, computing via Rational arithmetic throughout (see agg.h) + Rational getExactMixedPayoff(int player, int tp, const ExactStrategyProfile &s) const; + Rational getExactV(int player, int tp, int action, const ExactStrategyProfile &s) const; + AggNumber getPurePayoff(int player, int tp, std::vector &s); AggNumber getPurePayoff(int player, std::vector &s) { @@ -82,6 +89,7 @@ class BAGG { } return r; } + Rational getExactPurePayoff(int player, int tp, const std::vector &ps) const; AggNumber getSymMixedPayoff(StrategyProfile &s); @@ -96,6 +104,7 @@ class BAGG { int numActionNodes; std::vector numTypes; std::vector indepTypeDist; + std::vector> exactIndepTypeDist; // for each player, each of her types, the set of actions std::vector>> typeActionSets; @@ -113,6 +122,8 @@ class BAGG { void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, int tp = -1, int action = -1); void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); + void getExactAGGStrat(ExactStrategyProfile &as, const ExactStrategyProfile &s, int player, + int tp, int action) const; }; std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/games/agg/trie_map.cc b/src/games/agg/trie_map.cc index 779d911741..f325401309 100644 --- a/src/games/agg/trie_map.cc +++ b/src/games/agg/trie_map.cc @@ -29,4 +29,7 @@ namespace Gambit::agg { template class trie_map; template std::ostream &operator<<(std::ostream &s, const trie_map &t); +template class trie_map; +template std::ostream &operator<<(std::ostream &s, const trie_map &t); + } // namespace Gambit::agg diff --git a/src/games/agg/trie_map.imp b/src/games/agg/trie_map.imp index acf6cb544b..f28a7d02a3 100644 --- a/src/games/agg/trie_map.imp +++ b/src/games/agg/trie_map.imp @@ -207,7 +207,7 @@ void trie_map::square(trie_map &dest, size_t keylen, std::vector } v.second = (V)((*p1).second * (*p2).second); if (p1 != p2) { - v.second *= 2; + v.second *= (V)2; } dest += v; } @@ -234,7 +234,7 @@ template void trie_map::square(size_t keylen, std::vector } v.second = (V)((*p1).second * (*p2).second); if (p1 != p2) { - v.second *= 2; + v.second *= (V)2; } (*this) += v; } @@ -443,7 +443,7 @@ template void trie_map::div_helper_mul::add(const std::vector throw std::runtime_error(str.str()); } if (ptr->val->second < (V)0) { - ptr->val->second = 0; + ptr->val->second = (V)0; } } diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index db6bbb0357..70b72dfa67 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -56,7 +56,7 @@ Rational AGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const for (int i = 1; i <= aggPtr->getNumPlayers(); i++) { s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } - return Rational(aggPtr->getPurePayoff(p_player->GetNumber() - 1, s)); + return (Rational)aggPtr->getExactPurePayoff(p_player->GetNumber() - 1, s); } Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -68,7 +68,7 @@ Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strat s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } s[player - 1] = p_strategy->GetNumber() - 1; - return Rational(aggPtr->getPurePayoff(player - 1, s)); + return (Rational)aggPtr->getExactPurePayoff(player - 1, s); } //======================================================================== @@ -93,43 +93,56 @@ template class AGGMixedStrategyProfileRep : public MixedStrategyProfil T GetPayoffDeriv(int pl, const GameStrategy &, const GameStrategy &) const override; }; +// A single dispatch point: T=double goes through AGG's original floating-point convolution +// engine (getMixedPayoff), T=Rational goes through its exact Rational-arithmetic counterpart +// (getExactMixedPayoff) -- same algorithm, different numeric type throughout, per agg.h. +template T CallMixedPayoff(agg::AGG &g, int pl, std::vector &s) +{ + if constexpr (std::is_same_v) { + return g.getExactMixedPayoff(pl, s); + } + else { + return g.getMixedPayoff(pl, s); + } +} + template T AGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return CallMixedPayoff(*g.aggPtr, pl - 1, s); } template T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { if (i + 1 == ps->GetPlayer()->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int &ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return CallMixedPayoff(*g.aggPtr, pl - 1, s); } template @@ -139,34 +152,34 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1, const auto player1 = ps1->GetPlayer().get(); const auto player2 = ps2->GetPlayer().get(); if (player1 == player2) { - return (T)0; + return T(0); } auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { if (i + 1 == player1->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps1->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps1->GetNumber() - 1) = T(1); } else if (i + 1 == player2->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps2->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps2->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return CallMixedPayoff(*g.aggPtr, pl - 1, s); } template class AGGMixedStrategyProfileRep; @@ -215,6 +228,10 @@ MixedStrategyProfile GameAGGRep::NewMixedStrategyProfile(double) const MixedStrategyProfile GameAGGRep::NewMixedStrategyProfile(const Rational &) const { + // Constructing the profile itself is fine (e.g. to exactly represent a pure-strategy + // profile as a degenerate mixed one, as EnumPureStrategySolve does) -- it's specifically + // *payoff computation* on it that AGG cannot do exactly; see + // AGGMixedStrategyProfileRep::GetPayoff. return MixedStrategyProfile(std::make_unique>( StrategySupportProfile(std::const_pointer_cast(shared_from_this())))); } diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 0ae07ff46d..af6ecf1e88 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -83,11 +83,11 @@ class GameAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return Rational(aggPtr->getMinPayoff()); } + Rational GetMinPayoff() const override { return (Rational)aggPtr->getExactMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return Rational(aggPtr->getMaxPayoff()); } + Rational GetMaxPayoff() const override { return (Rational)aggPtr->getExactMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index b1fd41c9ae..196c9b17f1 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -57,7 +57,7 @@ Rational BAGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const } const int bp = dynamic_cast(*m_game).agent2baggPlayer[p_player->GetNumber()]; const int tp = p_player->GetNumber() - 1 - baggPtr->typeOffset[bp - 1]; - return Rational(baggPtr->getPurePayoff(bp - 1, tp, s)); + return baggPtr->getExactPurePayoff(bp - 1, tp, s); } Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -71,7 +71,7 @@ Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_stra s[player - 1] = p_strategy->GetNumber() - 1; const int bp = dynamic_cast(*m_game).agent2baggPlayer[player]; const int tp = player - 1 - baggPtr->typeOffset[bp - 1]; - return Rational(baggPtr->getPurePayoff(bp - 1, tp, s)); + return baggPtr->getExactPurePayoff(bp - 1, tp, s); } //======================================================================== @@ -96,10 +96,23 @@ template class BAGGMixedStrategyProfileRep : public MixedStrategyProfi T GetPayoffDeriv(int pl, const GameStrategy &, const GameStrategy &) const override; }; +// A single dispatch point: T=double goes through BAGG's original floating-point convolution +// engine (getMixedPayoff), T=Rational goes through its exact Rational-arithmetic counterpart +// (getExactMixedPayoff) -- same algorithm, different numeric type throughout, per agg.h/bagg.h. +template T CallMixedPayoff(agg::BAGG &g, int bplayer, int btype, std::vector &s) +{ + if constexpr (std::is_same_v) { + return g.getExactMixedPayoff(bplayer, btype, s); + } + else { + return g.getMixedPayoff(bplayer, btype, s); + } +} + template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); const auto ns = g.GetStrategies().shape(); int bplayer = -1, btype = -1; for (int i = 0, offs = 0; i < g.baggPtr->getNumPlayers(); ++i) { @@ -113,18 +126,18 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s.at(offs) = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s.at(offs) = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); } template T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -134,9 +147,9 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } if (g.baggPtr->typeOffset[i] + tp + 1 == ps->GetPlayer()->GetNumber()) { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.baggPtr->getNumActions(i, tp); ++j) { @@ -150,7 +163,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); } template @@ -160,11 +173,11 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 const auto player1 = ps1->GetPlayer().get(); const auto player2 = ps2->GetPlayer().get(); if (player1 == player2) { - return (T)0; + return T(0); } auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -175,15 +188,15 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 if (g.baggPtr->typeOffset[i] + tp + 1 == player1->GetNumber()) { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps1->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps1->GetNumber() - 1) = T(1); } else if (g.baggPtr->typeOffset[i] + tp + 1 == player2->GetNumber()) { for (int j = 0; j < g.baggPtr->getNumActions(i, tp); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps2->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps2->GetNumber() - 1) = T(1); } else { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { @@ -192,12 +205,12 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 ->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); s.at(g.baggPtr->firstAction(i, tp) + j) = - static_cast((ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]); + (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); } template class BAGGMixedStrategyProfileRep; @@ -287,6 +300,10 @@ MixedStrategyProfile GameBAGGRep::NewMixedStrategyProfile(double) const MixedStrategyProfile GameBAGGRep::NewMixedStrategyProfile(const Rational &) const { + // Constructing the profile itself is fine (e.g. to exactly represent a pure-strategy + // profile as a degenerate mixed one, as EnumPureStrategySolve does) -- it's specifically + // *payoff computation* on it that BAGG cannot do exactly; see + // BAGGMixedStrategyProfileRep::GetPayoff. return MixedStrategyProfile(std::make_unique>( StrategySupportProfile(std::const_pointer_cast(shared_from_this())))); } diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 23ceb5e28b..6eed97b286 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -90,11 +90,11 @@ class GameBAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return Rational(baggPtr->getMinPayoff()); } + Rational GetMinPayoff() const override { return (Rational)baggPtr->getExactMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return Rational(baggPtr->getMaxPayoff()); } + Rational GetMaxPayoff() const override { return (Rational)baggPtr->getExactMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/games/number.h b/src/games/number.h index 9c3f4963c2..9e508444c1 100644 --- a/src/games/number.h +++ b/src/games/number.h @@ -23,6 +23,11 @@ #ifndef LIBGAMBIT_NUMBER_H #define LIBGAMBIT_NUMBER_H +#include +#include +#include "core/util.h" +#include "core/rational.h" + namespace Gambit { /// This class represents a numerical data value in a game. @@ -84,6 +89,8 @@ class Number { explicit operator const double &() const { return m_double; } explicit operator const Rational &() const { return m_rational; } explicit operator const std::string &() const { return m_text; } + + friend std::ostream &operator<<(std::ostream &s, const Number &n) { return s << n.m_text; } }; } // namespace Gambit diff --git a/tests/test_agg.py b/tests/test_agg.py index 1f6b071fbd..3c9529d6c1 100644 --- a/tests/test_agg.py +++ b/tests/test_agg.py @@ -3,6 +3,8 @@ import pytest +import pygambit as gbt + from . import games AGG_BAGG_GAME_PATHS = [ @@ -38,3 +40,93 @@ def test_agg_bagg_to_nfg(game_path): game = games.read_from_file(game_path) serialized_game = game.to_nfg() assert serialized_game[:3] == "NFG" + + +def test_agg_fraction_and_long_decimal_payoffs_parsed_exactly(): + """Payoffs written in an .agg file as a fraction ("1/3") or a decimal needing more than + double's ~15-17 significant digits of precision to round-trip are parsed into exact + Rationals (via Number, agg::AGG::exactPayoffs), not silently rounded through a bare + `istream >> double` read. + + 2x2_fraction_payoffs.agg is 2x2.agg with two of its four payoffs replaced: 35 -> 1/3 and + 95 -> 0.123456789012345. + """ + game = games.read_from_file("2x2_fraction_payoffs.agg") + assert game.max_payoff == gbt.Rational(1, 3) + assert game.min_payoff == -10 + for player in game.players: + assert player.max_payoff == gbt.Rational(1, 3) + assert player.min_payoff == -10 + + # pure-strategy payoff lookup (AGGPureStrategyProfileRep::GetPayoff) also reports the + # exact value, not a double-rounded approximation + profile = game.mixed_strategy_profile(rational=True) + p0, p1 = game.players + s0 = list(p0.strategies) + s1 = list(p1.strategies) + profile[s0[0]] = gbt.Rational(1) + profile[s0[1]] = gbt.Rational(0) + profile[s1[0]] = gbt.Rational(0) + profile[s1[1]] = gbt.Rational(1) + assert profile.payoff(p0) == gbt.Rational(1, 3) + + +def test_bagg_fraction_type_distribution_parsed_exactly(): + """BAGG type-distribution probabilities written as a fraction ("1/3") are parsed exactly + (agg::BAGG::exactIndepTypeDist) and the exact weighted-sum payoff computation over them + (agg::BAGG::getExactMixedPayoff) agrees with the double engine on the same profile. + + 2x2_fraction_types.bagg: player 1 has two types (weights 1/3, 2/3), player 2 has one. + """ + game = games.read_from_file("2x2_fraction_types.bagg") + p1t0, p1t1, p2 = game.players + exact = game.mixed_strategy_profile(rational=True) + dbl = game.mixed_strategy_profile(rational=False) + for profile, one, zero in [(exact, gbt.Rational(1), gbt.Rational(0)), (dbl, 1.0, 0.0)]: + s0, s1, s2 = list(p1t0.strategies), list(p1t1.strategies), list(p2.strategies) + profile[s0[0]], profile[s0[1]] = one, zero + profile[s1[0]], profile[s1[1]] = zero, one + profile[s2[0]], profile[s2[1]] = one, zero + assert exact.payoff(p2) == gbt.Rational(22) + assert float(exact.payoff(p2)) == dbl.payoff(p2) + + +@pytest.mark.parametrize("game_path", ["2x2.agg", "2x2.bagg"]) +def test_agg_bagg_mixed_strategy_profile_rational_exact_payoff(game_path): + """AGG/BAGG mixed-strategy payoffs support exact (rational) computation: the convolution + algorithm (agg::AGG::getMixedPayoff et al.) is generic in its numeric type, so it also runs + with Rational arithmetic throughout (agg::AGG::getExactMixedPayoff), not just double. + + 2x2.agg has a known mixed equilibrium at (10/11, 1/11) for both players, with payoff + exactly -5/11 to each -- computed here directly (not via a solver), confirming the exact + engine reproduces it with zero regret. + """ + game = games.read_from_file(game_path) + profile = game.mixed_strategy_profile(rational=True) + for player in game.players: + strategies = list(player.strategies) + profile[strategies[0]] = gbt.Rational(10, 11) + profile[strategies[1]] = gbt.Rational(1, 11) + for player in game.players: + assert profile.payoff(player) == gbt.Rational(-5, 11) + assert profile.max_regret() == 0 + + +@pytest.mark.parametrize("game_path", ["2x2.agg", "2x2.bagg"]) +def test_agg_bagg_rational_algorithms_find_exact_mixed_equilibrium(game_path): + """lcp_solve(rational=True) and enummixed_solve(rational=True) find the (10/11, 1/11) + mixed equilibrium of 2x2.agg exactly. See test_nash.py::test_lcp_strategy_double_12 for the + equivalent full-precision check via the standard solver-test harness. + """ + game = games.read_from_file(game_path) + results = [ + gbt.nash.lcp_solve(game, rational=True, use_strategic=True), + gbt.nash.enummixed_solve(game, rational=True), + ] + for result in results: + mixed = [eq for eq in result.equilibria if any(0 < eq[s] < 1 for s in game.strategies)] + assert len(mixed) == 1 + for player in game.players: + for strategy in player.strategies: + assert mixed[0][strategy] in (gbt.Rational(10, 11), gbt.Rational(1, 11)) + assert mixed[0].max_regret() == 0 diff --git a/tests/test_nash.py b/tests/test_nash.py index a98f3e7199..1397ed38f3 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -928,7 +928,10 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_double_11", ), - # Action graph game + # Action graph game. Exact throughout: pure-strategy payoffs via + # agg::AGG::getExactPurePayoff, and the genuinely mixed equilibrium found here via + # agg::AGG::getExactMixedPayoff (Rational arithmetic through the same convolution + # algorithm the double engine uses). pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "2x2.agg"), From 7e7835c79be1815cdd9c3563bd2013cd95960f14 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 09:17:41 -0400 Subject: [PATCH 02/14] Remove AggNumber typedef --- src/games/agg/agg.cc | 106 ++++++++++++++++------------------ src/games/agg/agg.h | 52 ++++++++--------- src/games/agg/bagg.cc | 36 ++++++------ src/games/agg/bagg.h | 30 +++++----- src/games/agg/trie_map.cc | 4 +- src/solvers/gtracer/aggame.cc | 40 ++++++------- src/solvers/gtracer/aggame.h | 12 ++-- 7 files changed, 134 insertions(+), 146 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 5995f6984e..58811a9d7b 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -421,18 +421,18 @@ void AGG::computeP(int player, int act, int player2, int act2) } } -void AGG::doProjection(int Node, AggNumber *s) +void AGG::doProjection(int Node, double *s) { for (int i = 0; i < numPlayers; i++) { doProjection(Node, i, &(s[firstAction(i)])); } } -void AGG::doProjection(int Node, int i, AggNumber *s) +void AGG::doProjection(int Node, int i, double *s) { projectedStrat[Node][i].reset(); for (int j = 0; j < actions[i]; j++) { - if (s[j] > (AggNumber)0.0) { + if (s[j] > (double)0.0) { projectedStrat[Node][i] += make_pair(projection[Node][i][j], s[j]); } } @@ -485,7 +485,7 @@ void AGG::doExactProjection(int Node, int i, Rational *s) } } -AggNumber AGG::getPurePayoff(int player, const std::vector &s) +double AGG::getPurePayoff(int player, const std::vector &s) { assert(player >= 0 && player < numPlayers); const int Node = actionSets[player][s[player]]; @@ -531,19 +531,19 @@ Number AGG::getExactPurePayoff(int player, const std::vector &s) const return p->second; } -AggNumber AGG::getMixedPayoff(int player, StrategyProfile &s) +double AGG::getMixedPayoff(int player, StrategyProfile &s) { - AggNumber result = 0.0; + double result = 0.0; assert(player >= 0 && player < numPlayers); for (int act = 0; act < actions[player]; ++act) { - if (s[act + firstAction(player)] > (AggNumber)0.0) { + if (s[act + firstAction(player)] > (double)0.0) { result += s[act + firstAction(player)] * getV(player, act, s); } } return result; } -void AGG::getPayoffVector(AggNumberVector &dest, int player, const StrategyProfile &s) +void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) { assert(player >= 0 && player < numPlayers); for (int act = 0; act < actions[player]; ++act) { @@ -551,7 +551,7 @@ void AGG::getPayoffVector(AggNumberVector &dest, int player, const StrategyProfi } } -AggNumber AGG::getV(int player, int act, const StrategyProfile &s) +double AGG::getV(int player, int act, const StrategyProfile &s) { // project s to the projectedStrat doProjection(actionSets.at(player).at(act), s); @@ -559,7 +559,7 @@ AggNumber AGG::getV(int player, int act, const StrategyProfile &s) return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); } -AggNumber AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) +double AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) { doProjection(actionSets[player1][act1], s); computeP(player1, act1, player2, act2); @@ -610,22 +610,22 @@ Rational AGG::getExactMixedPayoff(int player, const ExactStrategyProfile &s) // parameter: s is the mixed strategy of one player. It is a vector of // probabilities, indexed by the action node. -AggNumber AGG::getSymMixedPayoff(StrategyProfile &s) +double AGG::getSymMixedPayoff(StrategyProfile &s) { - AggNumber result = 0; + double result = 0; if (!isSymmetric()) { throw std::runtime_error("AGG::getSymMixedPayoff: the game is not symmetric"); } for (int node = 0; node < numActionNodes; ++node) { - if (s[node] > (AggNumber)0.0) { + if (s[node] > (double)0.0) { result += s[node] * getSymMixedPayoff(node, s); } } return result; } -void AGG::getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s) +void AGG::getSymPayoffVector(std::vector &dest, StrategyProfile &s) { if (!isSymmetric()) { throw std::runtime_error("AGG::getSymMixedPayoff: the game is not symmetric"); @@ -635,7 +635,7 @@ void AGG::getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s) } } -AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) +double AGG::getSymMixedPayoff(int node, StrategyProfile &s) { const int numNei = neighbors[node].size(); @@ -650,28 +650,28 @@ AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) return dest.inner_prod(projection[node][0][node], numNei, projFunctions[node], payoffs[node]); } - AggNumber V = 0.0; + double V = 0.0; vector support; - AggNumber null_prob = 1; + double null_prob = 1; // do projection & get support int self = -1; for (int i = 0; i < numNei; ++i) { if (neighbors[node][i] == node) { self = i; } - if (s[neighbors[node][i]] > (AggNumber)0) { + if (s[neighbors[node][i]] > (double)0) { support.push_back(i); null_prob -= s[neighbors[node][i]]; } } - if (numNei < numActionNodes && null_prob > (AggNumber)0) { + if (numNei < numActionNodes && null_prob > (double)0) { support.push_back(-1); } // gray code GrayComposition gc(numPlayers - 1, support.size()); - AggNumber prob = + double prob = std::pow((support.at(0) >= 0) ? s[neighbors[node][support[0]]] : null_prob, numPlayers - 1); while (true) { @@ -694,13 +694,10 @@ AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) break; } // update prob - const AggNumber i_prob = - (support.at(gc.i) != -1) ? s[neighbors[node][support[gc.i]]] : null_prob; - const AggNumber d_prob = - (support.at(gc.d) != -1) ? s[neighbors[node][support[gc.d]]] : null_prob; - assert(i_prob > (AggNumber)0 && d_prob > (AggNumber)0); - prob *= - ((AggNumber)(gc.get().at(gc.d) + 1)) * i_prob / (AggNumber)(gc.get().at(gc.i)) / d_prob; + const double i_prob = (support.at(gc.i) != -1) ? s[neighbors[node][support[gc.i]]] : null_prob; + const double d_prob = (support.at(gc.d) != -1) ? s[neighbors[node][support[gc.d]]] : null_prob; + assert(i_prob > (double)0 && d_prob > (double)0); + prob *= ((double)(gc.get().at(gc.d) + 1)) * i_prob / (double)(gc.get().at(gc.i)) / d_prob; } // end while @@ -732,7 +729,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int projectedStrat[node][player].reset(); if (numPl > 0) { for (int j = 0; j < actions[player]; j++) { - if (s[j] > (AggNumber)0.0) { + if (s[j] > (double)0.0) { projectedStrat[node][player] += make_pair(projection[node][player][j], s[j]); } } @@ -763,9 +760,9 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int return; } - // AggNumber V = 0.0; + // double V = 0.0; vector support; - AggNumber null_prob = 1; + double null_prob = 1; // do projection & get support int self = -1; // index of self in the neighbor list int ind2 = -1; // index of act2 in the neighbor list @@ -779,21 +776,21 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int } const int a = node2Action.at(neighbors[node][i]).at(p); - if (a >= 0 && s[a] > (AggNumber)0) { + if (a >= 0 && s[a] > (double)0) { support.push_back(i); null_prob -= s[a]; } } - if (null_prob > (AggNumber)0) { + if (null_prob > (double)0) { support.push_back(-1); } // gray code GrayComposition gc(numPl, support.size()); - const AggNumber prob0 = + const double prob0 = (support.at(0) >= 0) ? s[node2Action[neighbors[node].at(support[0])][p]] : null_prob; - AggNumber prob = std::pow(prob0, numPl); + double prob = std::pow(prob0, numPl); while (true) { const vector &comp = gc.get(); @@ -821,23 +818,22 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int break; } // update prob - const AggNumber i_prob = + const double i_prob = (support.at(gc.i) != -1) ? s[node2Action[neighbors[node][support[gc.i]]][p]] : null_prob; - const AggNumber d_prob = + const double d_prob = (support.at(gc.d) != -1) ? s[node2Action[neighbors[node][support[gc.d]]][p]] : null_prob; - assert(i_prob > (AggNumber)0 && d_prob > (AggNumber)0); - prob *= - ((AggNumber)(gc.get().at(gc.d) + 1)) * i_prob / (AggNumber)(gc.get().at(gc.i)) / d_prob; + assert(i_prob > (double)0 && d_prob > (double)0); + prob *= ((double)(gc.get().at(gc.d) + 1)) * i_prob / (double)(gc.get().at(gc.i)) / d_prob; } // end while } -AggNumber AGG::getKSymMixedPayoff(int playerClass, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, vector &s) { - AggNumber result = 0.0; + double result = 0.0; for (int act = 0; act < (int)uniqueActionSets[playerClass].size(); act++) { - if (s[playerClass][act] > (AggNumber)0.0) { + if (s[playerClass][act] > (double)0.0) { result += s[playerClass][act] * getKSymMixedPayoff(playerClass, act, s); } @@ -845,12 +841,12 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, vector &s) return result; } -AggNumber AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) +double AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) { - AggNumber result = 0.0; + double result = 0.0; for (int act = 0; act < (int)uniqueActionSets[playerClass].size(); act++) { - if (s[firstKSymAction(playerClass) + act] > (AggNumber)0.0) { + if (s[firstKSymAction(playerClass) + act] > (double)0.0) { result += s[firstKSymAction(playerClass) + act] * getKSymMixedPayoff(s, playerClass, act); } @@ -858,14 +854,14 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) return result; } -void AGG::getKSymPayoffVector(AggNumberVector &dest, int playerClass, StrategyProfile &s) +void AGG::getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s) { for (size_t act = 0; act < uniqueActionSets[playerClass].size(); ++act) { dest[act] = getKSymMixedPayoff(s, playerClass, act); } } -AggNumber AGG::getKSymMixedPayoff(int playerClass, int act, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, int act, vector &s) { const int numPC = playerClasses.size(); @@ -883,8 +879,8 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, int act, vector::iterator, bool> r = pay.insert(make_pair(key, u)); + const pair::iterator, bool> r = pay.insert(make_pair(key, u)); if (!r.second) { std::stringstream str; str << "ERROR: overwriting utility at ["; @@ -1002,10 +998,10 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exact } } -AggNumber AGG::getMaxPayoff() const +double AGG::getMaxPayoff() const { assert(numActionNodes > 0); - AggNumber result = payoffs[0].begin()->second; + double result = payoffs[0].begin()->second; for (int i = 1; i < numActionNodes; i++) { for (const auto &it : payoffs[i]) { result = max(result, it.second); @@ -1014,10 +1010,10 @@ AggNumber AGG::getMaxPayoff() const return result; } -AggNumber AGG::getMinPayoff() const +double AGG::getMinPayoff() const { assert(numActionNodes > 0); - AggNumber result = payoffs[0].begin()->second; + double result = payoffs[0].begin()->second; for (int i = 1; i < numActionNodes; i++) { for (const auto &it : payoffs[i]) { result = min(result, it.second); diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index c6d232d354..b3a715bd53 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -41,12 +41,10 @@ class aggame; namespace agg { // data structure for mixed strategy profile -using AggNumber = double; -using StrategyProfile = std::vector; -using AggNumberVector = std::vector; +using StrategyProfile = std::vector; // data structure for payoff function: -using aggpayoff = trie_map; +using aggpayoff = trie_map; // exact (arbitrary-precision) counterpart of a payoff function, populated alongside aggpayoff // at parse time. Only used for storage/lookup (find/insert/iterate), never for the @@ -56,7 +54,7 @@ using aggpayoff = trie_map; using exactpayoff = std::map, Number>; // data struct for prob distribution over configurations: -using aggdistrib = trie_map; +using aggdistrib = trie_map; // exact (arbitrary-precision) counterparts of the above, used by the exact mixed-strategy // payoff computation (getExactMixedPayoff/getExactV/getExactJ). Unlike exactpayoff, these @@ -112,10 +110,10 @@ class AGG { int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } // exp. payoff under mixed strat profile - AggNumber getMixedPayoff(int player, StrategyProfile &s); - void getPayoffVector(AggNumberVector &dest, int player, const StrategyProfile &s); - AggNumber getV(int player, int action, const StrategyProfile &s); - AggNumber getJ(int player, int action, int player2, int action2, StrategyProfile &s); + double getMixedPayoff(int player, StrategyProfile &s); + void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); + double getV(int player, int action, const StrategyProfile &s); + double getJ(int player, int action, int player2, int action2, StrategyProfile &s); // exact counterparts of the above three, computing via the same convolution algorithm but // in Rational arithmetic throughout, rather than double. @@ -124,7 +122,7 @@ class AGG { Rational getExactJ(int player, int action, int player2, int action2, const ExactStrategyProfile &s); - AggNumber getPurePayoff(int player, const std::vector &s); + double getPurePayoff(int player, const std::vector &s); Number getExactPurePayoff(int player, const std::vector &s) const; bool isSymmetric() const @@ -136,19 +134,19 @@ class AGG { } return true; } - AggNumber getSymMixedPayoff(StrategyProfile &s); - AggNumber getSymMixedPayoff(int actnode, StrategyProfile &s); - void getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s); - AggNumber getKSymMixedPayoff(int playerClass, std::vector &s); - AggNumber getKSymMixedPayoff(int playerClass, StrategyProfile &s); - AggNumber getKSymMixedPayoff(int playerClass, int act, std::vector &s); - AggNumber getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, int pClass2 = -1, - int act2 = -1); - void getKSymPayoffVector(AggNumberVector &dest, int playerClass, StrategyProfile &s); - - AggNumberVector getExpectedConfig(StrategyProfile &s) + double getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(int actnode, StrategyProfile &s); + void getSymPayoffVector(std::vector &dest, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, std::vector &s); + double getKSymMixedPayoff(int playerClass, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, int act, std::vector &s); + double getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, int pClass2 = -1, + int act2 = -1); + void getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s); + + std::vector getExpectedConfig(StrategyProfile &s) { - AggNumberVector res(numActionNodes, 0); + std::vector res(numActionNodes, 0); for (int i = 0; i < numPlayers; ++i) { for (int j = 0; j < actions[i]; ++j) { res[actionSets[i][j]] += s[firstAction(i) + j]; @@ -166,8 +164,8 @@ class AGG { const std::vector &getActionSet(int player) { return actionSets.at(player); } const aggpayoff &getPayoffMap(int node) { return payoffs.at(node); } - AggNumber getMaxPayoff() const; - AggNumber getMinPayoff() const; + double getMaxPayoff() const; + double getMinPayoff() const; Number getExactMaxPayoff() const; Number getExactMinPayoff() const; @@ -266,7 +264,7 @@ class AGG { std::vector> node2Action; // cache of jacobian entries. - trie_map cache; + trie_map cache; // the unique action sets std::vector uniqueActionSets; @@ -327,8 +325,8 @@ class AGG { { doProjection(Node, player, &(const_cast(s)[firstAction(player)])); } - void doProjection(int Node, AggNumber *s); - void doProjection(int Node, int player, AggNumber *s); + void doProjection(int Node, double *s); + void doProjection(int Node, int player, double *s); // exact (Rational) counterparts, mirroring the above precisely. exactPayoffs is a // std::map<..., Number>, not a trie_map, so trie_map's own inner_prod can't be diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index c46c5db15f..884cba9b75 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -185,27 +185,27 @@ std::shared_ptr BAGG::makeBAGG(istream &in) typeAction2ActionIndex, aggPtr); } -AggNumber BAGG::getMixedPayoff(int player, StrategyProfile &s) +double BAGG::getMixedPayoff(int player, StrategyProfile &s) { - AggNumber res(0); + double res(0); for (int tp = 0; tp < numTypes[player]; ++tp) { res += indepTypeDist[player][tp] * getMixedPayoff(player, tp, s); } return res; } -AggNumber BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) +double BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) { - AggNumber res(0); + double res(0); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { - if (s[act + firstAction(player, tp)] > AggNumber(0.0)) { + if (s[act + firstAction(player, tp)] > double(0.0)) { res += s[act + firstAction(player, tp)] * getV(player, tp, act, s); } } return res; } -void BAGG::getPayoffVector(AggNumberVector &dest, int player, int tp, const StrategyProfile &s) +void BAGG::getPayoffVector(std::vector &dest, int player, int tp, const StrategyProfile &s) { assert(player >= 0 && player < getNumPlayers() && tp >= 0 && tp < getNumTypes(player)); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { @@ -217,7 +217,7 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player int action) { for (int i = 0; i < aggPtr->getNumActions(); ++i) { - as[i] = AggNumber(0.0); + as[i] = double(0.0); } for (int pl = 0; pl < numPlayers; ++pl) { @@ -235,7 +235,7 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player } } } -AggNumber BAGG::getV(int player, int tp, int action, const StrategyProfile &s) +double BAGG::getV(int player, int tp, int action, const StrategyProfile &s) { StrategyProfile as(aggPtr->getNumActions()); getAGGStrat(as, s, player, tp, action); @@ -285,14 +285,14 @@ Rational BAGG::getExactMixedPayoff(int player, int tp, const ExactStrategyProfil return res; } -AggNumber BAGG::getPurePayoff(int player, int tp, std::vector &ps) +double BAGG::getPurePayoff(int player, int tp, std::vector &ps) { StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); for (int i = 0; i < strategyOffset[typeOffset[numPlayers]]; i++) { - st[i] = (AggNumber)0.0; + st[i] = (double)0.0; } for (int i = 0; i < typeOffset[numPlayers]; i++) { - st[strategyOffset[i] + ps[i]] = (AggNumber)1.0; + st[strategyOffset[i] + ps[i]] = (double)1.0; } return getMixedPayoff(player, tp, st); } @@ -344,7 +344,7 @@ Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) { for (int i = 0; i < aggPtr->getNumActionNodes(); ++i) { - as[i] = AggNumber(0.0); + as[i] = double(0.0); } for (int t = 0; t < numTypes[0]; ++t) { for (size_t act = 0; act < typeActionSets[0][t].size(); ++act) { @@ -354,27 +354,27 @@ void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) } } -AggNumber BAGG::getSymMixedPayoff(StrategyProfile &s) +double BAGG::getSymMixedPayoff(StrategyProfile &s) { - AggNumber res(0); + double res(0); for (int tp = 0; tp < numTypes[0]; ++tp) { res += indepTypeDist[0][tp] * getSymMixedPayoff(tp, s); } return res; } -AggNumber BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) { - AggNumber res(0); + double res(0); for (size_t act = 0; act < typeActionSets[0][tp].size(); ++act) { - if (s[act + firstAction(0, tp)] > AggNumber(0.0)) { + if (s[act + firstAction(0, tp)] > double(0.0)) { res += s[act + firstAction(0, tp)] * getSymMixedPayoff(tp, act, s); } } return res; } -AggNumber BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) { StrategyProfile as(aggPtr->getNumActionNodes()); getSymAGGStrat(as, s); diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index cc1fa54d71..f5d5930520 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -34,12 +34,12 @@ template class BAGGMixedStrategyProfileRep; namespace agg { -using ProbDist = std::vector; +using ProbDist = std::vector; class BAGG { public: - friend class BAGGPureStrategyProfileRep; - template friend class BAGGMixedStrategyProfileRep; + friend class Gambit::BAGGPureStrategyProfileRep; + template friend class Gambit::BAGGMixedStrategyProfileRep; friend std::ostream &operator<<(std::ostream &s, const BAGG &g); @@ -63,27 +63,27 @@ class BAGG { int firstAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t]; } int lastAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t + 1]; } - AggNumber getMaxPayoff() const { return aggPtr->getMaxPayoff(); } - AggNumber getMinPayoff() const { return aggPtr->getMinPayoff(); } + double getMaxPayoff() const { return aggPtr->getMaxPayoff(); } + double getMinPayoff() const { return aggPtr->getMinPayoff(); } Number getExactMaxPayoff() const { return aggPtr->getExactMaxPayoff(); } Number getExactMinPayoff() const { return aggPtr->getExactMinPayoff(); } // exp. payoff under mixed strat profile - AggNumber getMixedPayoff(int player, StrategyProfile &s); + double getMixedPayoff(int player, StrategyProfile &s); // exp payoff for player, conditioned on her receiving type tp. - AggNumber getMixedPayoff(int player, int tp, StrategyProfile &s); + double getMixedPayoff(int player, int tp, StrategyProfile &s); - void getPayoffVector(AggNumberVector &dest, int player, int tp, const StrategyProfile &s); - AggNumber getV(int player, int tp, int action, const StrategyProfile &s); + void getPayoffVector(std::vector &dest, int player, int tp, const StrategyProfile &s); + double getV(int player, int tp, int action, const StrategyProfile &s); // exact counterparts, computing via Rational arithmetic throughout (see agg.h) Rational getExactMixedPayoff(int player, int tp, const ExactStrategyProfile &s) const; Rational getExactV(int player, int tp, int action, const ExactStrategyProfile &s) const; - AggNumber getPurePayoff(int player, int tp, std::vector &s); - AggNumber getPurePayoff(int player, std::vector &s) + double getPurePayoff(int player, int tp, std::vector &s); + double getPurePayoff(int player, std::vector &s) { - AggNumber r = 0; + double r = 0; for (int i = 0; i < numTypes[player]; ++i) { r += indepTypeDist[player][i] * getPurePayoff(player, i, s); } @@ -91,11 +91,11 @@ class BAGG { } Rational getExactPurePayoff(int player, int tp, const std::vector &ps) const; - AggNumber getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(StrategyProfile &s); - AggNumber getSymMixedPayoff(int tp, StrategyProfile &s); + double getSymMixedPayoff(int tp, StrategyProfile &s); - AggNumber getSymMixedPayoff(int tp, int act, StrategyProfile &s); + double getSymMixedPayoff(int tp, int act, StrategyProfile &s); bool isSymmetric() const { return symmetric; } diff --git a/src/games/agg/trie_map.cc b/src/games/agg/trie_map.cc index f325401309..b6eee4ddb0 100644 --- a/src/games/agg/trie_map.cc +++ b/src/games/agg/trie_map.cc @@ -26,8 +26,8 @@ namespace Gambit::agg { -template class trie_map; -template std::ostream &operator<<(std::ostream &s, const trie_map &t); +template class trie_map; +template std::ostream &operator<<(std::ostream &s, const trie_map &t); template class trie_map; template std::ostream &operator<<(std::ostream &s, const trie_map &t); diff --git a/src/solvers/gtracer/aggame.cc b/src/solvers/gtracer/aggame.cc index 6d9da55457..3ecc85c772 100644 --- a/src/solvers/gtracer/aggame.cc +++ b/src/solvers/gtracer/aggame.cc @@ -32,7 +32,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t const int numNei = aggPtr->neighbors[Node].size(); // assert(aggPtr->isPure[Node]||tasks.size()==0); - std::vector strat(numNei); + std::vector strat(numNei); agg::AGG::config a(numNei, 0); // compute the full distrib aggPtr->computeP(player1, act1); @@ -54,7 +54,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t } else { strat[j] = pp->second; - if (strat[j] > (agg::AggNumber)0) { + if (strat[j] > (double)0) { NullOnly = false; } } @@ -65,7 +65,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t cout << "dividing " << endl; P.print_in_order(); cout << endl << "by ["; - copy(strat.begin(), strat.end(), ostream_iterator(cout, " ")); + copy(strat.begin(), strat.end(), ostream_iterator(cout, " ")); cout << "]\n"; #endif if (!NullOnly) { @@ -137,7 +137,7 @@ void aggame::computePartialP_bisect(int player1, int act1, std::vector::ite } } -void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) const +void aggame::payoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const { // compute jacobian // s: mixed strat @@ -145,7 +145,7 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) #ifdef AGGDEBUG cout << "calling payoffMatrix with stratety s=" << endl << s << endl; #endif - agg::AggNumber fuzzcount; + double fuzzcount; int rown, coln, rowi, coli, act1, act2, currNode, numNei; static std::vector::iterator p; static std::vector tasks, spares, nontasks; @@ -300,7 +300,7 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) } #endif // compute entries - agg::AggNumber undisturbedPayoff; + double undisturbedPayoff; bool hasUndisturbed = false; if (!spares.empty()) { // for players in spares, we compute one undisturbed payoff @@ -327,8 +327,8 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) } // end for(rown } -void aggame::computeUndisturbedPayoff(agg::AggNumber &undisturbedPayoff, bool &has, int player1, - int act1, int player2) const +void aggame::computeUndisturbedPayoff(double &undisturbedPayoff, bool &has, int player1, int act1, + int player2) const { if (has) { return; @@ -347,17 +347,15 @@ void aggame::computeUndisturbedPayoff(agg::AggNumber &undisturbedPayoff, bool &h has = true; } -void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - agg::AggNumber result, agg::trie_map &cache, - bool partial) const +void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, double result, + agg::trie_map &cache, bool partial) const { const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); if (!partial) { - std::pair, agg::AggNumber> pair1(aggPtr->projection[Node][player2][act2], - result); + std::pair, double> pair1(aggPtr->projection[Node][player2][act2], result); pair1.first.reserve(numNei + 3); pair1.first.push_back(player1); pair1.first.push_back(act1); @@ -367,8 +365,7 @@ void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int a if (aggPtr->node2Action[Node][player2] != -1 && aggPtr->fullProjectedStrat[Node][player1].count(aggPtr->projection[Node][player2][act2])) { - std::pair, agg::AggNumber> pair2(aggPtr->projection[Node][player2][act2], - result); + std::pair, double> pair2(aggPtr->projection[Node][player2][act2], result); pair2.first.reserve(numNei + 3); pair2.first.push_back(player2); pair2.first.push_back(aggPtr->node2Action[Node][player2]); @@ -379,17 +376,17 @@ void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int a } void aggame::computePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - agg::trie_map &cache) const + agg::trie_map &cache) const { const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); - std::pair, agg::AggNumber> insPair(aggPtr->projection[Node][player2][act2], 0); + std::pair, double> insPair(aggPtr->projection[Node][player2][act2], 0); insPair.first.reserve(numNei + 3); insPair.first.push_back(player1); insPair.first.push_back(act1); insPair.first.push_back(player2); - const std::pair::iterator, bool> r = cache.insert(insPair); + const std::pair::iterator, bool> r = cache.insert(insPair); if (!r.second) { dest(act1 + firstAction(player1), act2 + firstAction(player2)) = r.first->second; } @@ -401,7 +398,7 @@ void aggame::computePayoff(cmatrix &dest, int player1, int act1, int player2, in } } -void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) const +void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const { const std::vector sp(s.values(), s.values() + s.getm()); // simple implementation using expected payoffs: @@ -415,14 +412,13 @@ void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fu for (int cola = 0; cola < getNumKSymActions(colcls); ++cola) { dest(rowa + firstKSymAction(rowcls), cola + firstKSymAction(colcls)) = - (agg::AggNumber)multiplier * - aggPtr->getKSymMixedPayoff(sp, rowcls, rowa, colcls, cola); + (double)multiplier * aggPtr->getKSymMixedPayoff(sp, rowcls, rowa, colcls, cola); } } } } - agg::AggNumber fuzzcount; + double fuzzcount; for (int rown = 0; rown < getNumPlayerClasses(); ++rown) { fuzzcount = fuzz; for (int rowi = firstKSymAction(rown); rowi < lastKSymAction(rown); rowi++) { diff --git a/src/solvers/gtracer/aggame.h b/src/solvers/gtracer/aggame.h index ee9731d1ae..120d1f776c 100644 --- a/src/solvers/gtracer/aggame.h +++ b/src/solvers/gtracer/aggame.h @@ -95,13 +95,11 @@ class aggame : public gnmgame { void computePartialP_bisect(int player, int act, std::vector::iterator f, std::vector::iterator l, Gambit::agg::aggdistrib &temp) const; void computePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - Gambit::agg::trie_map &cache) const; - void savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - Gambit::agg::AggNumber result, - Gambit::agg::trie_map &cache, - bool partial = false) const; - void computeUndisturbedPayoff(Gambit::agg::AggNumber &undisturbedPayoff, bool &has, int player1, - int act1, int player2) const; + Gambit::agg::trie_map &cache) const; + void savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, double result, + Gambit::agg::trie_map &cache, bool partial = false) const; + void computeUndisturbedPayoff(double &undisturbedPayoff, bool &has, int player1, int act1, + int player2) const; }; } // end namespace Gambit::gametracer From 3138c34b4ded3711a93522d9c8f26d74a966163b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 09:50:47 -0400 Subject: [PATCH 03/14] Consolidate data structure names --- src/games/agg/agg.cc | 84 ++++++++++++++--------------- src/games/agg/agg.h | 99 ++++++++++++++++++----------------- src/games/agg/bagg.cc | 37 ++++++------- src/games/agg/bagg.h | 29 +++++----- src/solvers/gtracer/aggame.cc | 5 +- src/solvers/gtracer/aggame.h | 3 +- 6 files changed, 132 insertions(+), 125 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 58811a9d7b..77c2790237 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -35,17 +35,17 @@ inline int select2nd(const pair &x) { return x.second; } AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNodes, vector> &_actionSets, vector> &neighb, - vector &projTypes, vector> &projS, + vector &projTypes, vector>> &projS, vector>> &proj, vector> &projF, - vector>> &Po, vector &P, vector &_payoffs, - vector &_exactPayoffs) + vector>> &Po, vector> &P, + vector &_payoffs, vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), actionSets(_actionSets), neighbors(neighb), projectionTypes(projTypes), payoffs(_payoffs), exactPayoffs(_exactPayoffs), projection(proj), projectedStrat(projS), - exactProjectedStrat(numANodes, vector(numPlayers)), exactPr(numPlayers), - fullProjectedStrat(projS), projFunctions(projF), Porder(Po), Pr(P), isPure(numANodes, true), - node2Action(numANodes, vector(numPlayers)), cache(numPlayers + 1), - player2Class(numPlayers), kSymStrategyOffset(1, 0) + exactProjectedStrat(numANodes, vector>(numPlayers)), + exactPr(numPlayers), fullProjectedStrat(projS), projFunctions(projF), Porder(Po), Pr(P), + isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), + cache(numPlayers + 1), player2Class(numPlayers), kSymStrategyOffset(1, 0) { // actions @@ -223,7 +223,7 @@ std::shared_ptr AGG::makeAGG(istream &in) projTypes[i] = make_proj_func((TypeEnum)pt, in, S, P); } - vector> projS; + vector>> projS; vector>> proj; setProjections(projS, proj, n, S, P, ASets, neighb, projTypes); @@ -238,7 +238,7 @@ std::shared_ptr AGG::makeAGG(istream &in) } vector>> Po(n); - vector Pr(n); + vector> Pr(n); vector pays(S); // payoffs vector exactPays(S); // exact (arbitrary-precision) counterpart of pays @@ -297,8 +297,9 @@ std::shared_ptr AGG::makeAGG(istream &in) pays, exactPays); } -void AGG::setProjections(vector> &projS, vector>> &proj, - int N, int S, int P, vector> &AS, vector> &neighb, +void AGG::setProjections(vector>> &projS, + vector>> &proj, int N, int S, int P, + vector> &AS, vector> &neighb, vector &projTypes) { int Node, i, j, k, numNei, actions; @@ -374,7 +375,7 @@ void AGG::getAn(multiset &dest, vector> &neighb, vector &Po, int i, int N, vector &projS) +void AGG::initPorder(vector &Po, int i, int N, vector> &projS) { vector> order; int k; @@ -409,7 +410,7 @@ void AGG::computeP(int player, int act, int player2, int act2) } else { // apply player2's pure strat - aggdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], 1.0)); Pr[k].multiply(Pr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); } @@ -455,7 +456,7 @@ void AGG::computeExactP(int player, int act, int player2, int act2) exactPr[k] = exactPr[k - 1]; } else { - exactdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], Rational(1))); exactPr[k].multiply(exactPr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); } @@ -531,7 +532,7 @@ Number AGG::getExactPurePayoff(int player, const std::vector &s) const return p->second; } -double AGG::getMixedPayoff(int player, StrategyProfile &s) +double AGG::getMixedPayoff(int player, StrategyProfile &s) { double result = 0.0; assert(player >= 0 && player < numPlayers); @@ -543,7 +544,7 @@ double AGG::getMixedPayoff(int player, StrategyProfile &s) return result; } -void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) +void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) { assert(player >= 0 && player < numPlayers); for (int act = 0; act < actions[player]; ++act) { @@ -551,7 +552,7 @@ void AGG::getPayoffVector(std::vector &dest, int player, const StrategyP } } -double AGG::getV(int player, int act, const StrategyProfile &s) +double AGG::getV(int player, int act, const StrategyProfile &s) { // project s to the projectedStrat doProjection(actionSets.at(player).at(act), s); @@ -559,14 +560,14 @@ double AGG::getV(int player, int act, const StrategyProfile &s) return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); } -double AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) +double AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) { doProjection(actionSets[player1][act1], s); computeP(player1, act1, player2, act2); return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); } -Rational AGG::exactInnerProd(int node, const exactdistrib &dist) const +Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) const { Rational result(0); for (const auto &entry : dist) { @@ -578,7 +579,7 @@ Rational AGG::exactInnerProd(int node, const exactdistrib &dist) const return result; } -Rational AGG::getExactV(int player, int act, const ExactStrategyProfile &s) +Rational AGG::getExactV(int player, int act, const StrategyProfile &s) { doExactProjection(actionSets.at(player).at(act), s); computeExactP(player, act); @@ -586,14 +587,14 @@ Rational AGG::getExactV(int player, int act, const ExactStrategyProfile &s) } Rational AGG::getExactJ(int player1, int act1, int player2, int act2, - const ExactStrategyProfile &s) + const StrategyProfile &s) { doExactProjection(actionSets[player1][act1], s); computeExactP(player1, act1, player2, act2); return exactInnerProd(actionSets[player1][act1], exactPr[numPlayers - 1]); } -Rational AGG::getExactMixedPayoff(int player, const ExactStrategyProfile &s) +Rational AGG::getExactMixedPayoff(int player, const StrategyProfile &s) { Rational result(0); assert(player >= 0 && player < numPlayers); @@ -610,7 +611,7 @@ Rational AGG::getExactMixedPayoff(int player, const ExactStrategyProfile &s) // parameter: s is the mixed strategy of one player. It is a vector of // probabilities, indexed by the action node. -double AGG::getSymMixedPayoff(StrategyProfile &s) +double AGG::getSymMixedPayoff(StrategyProfile &s) { double result = 0; if (!isSymmetric()) { @@ -625,7 +626,7 @@ double AGG::getSymMixedPayoff(StrategyProfile &s) return result; } -void AGG::getSymPayoffVector(std::vector &dest, StrategyProfile &s) +void AGG::getSymPayoffVector(std::vector &dest, StrategyProfile &s) { if (!isSymmetric()) { throw std::runtime_error("AGG::getSymMixedPayoff: the game is not symmetric"); @@ -635,16 +636,16 @@ void AGG::getSymPayoffVector(std::vector &dest, StrategyProfile &s) } } -double AGG::getSymMixedPayoff(int node, StrategyProfile &s) +double AGG::getSymMixedPayoff(int node, StrategyProfile &s) { const int numNei = neighbors[node].size(); if (!isPure[node]) { // then compute EU using trie_map::power() doProjection(node, 0, s); assert(numPlayers > 1); - // aggdistrib *dest; + // ConfigDistribution *dest; // projectedStrat[node][0].power(numPlayers-1, dest, Pr, numNei,projFunctions[node]); - aggdistrib &dest = Pr[numPlayers - 1]; + ConfigDistribution &dest = Pr[numPlayers - 1]; projectedStrat[node][0].power(numPlayers - 1, dest, Pr[numPlayers - 2], numNei, projFunctions[node]); return dest.inner_prod(projection[node][0][node], numNei, projFunctions[node], payoffs[node]); @@ -708,8 +709,8 @@ double AGG::getSymMixedPayoff(int node, StrategyProfile &s) // plClass: the index for the player class // s: mixed strat for that player class -void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, - aggdistrib &dest, int plClass2, int act2) +void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, + ConfigDistribution &dest, int plClass2, int act2) { const int node = uniqueActionSets.at(ownPlClass).at(act); int numPl = playerClasses.at(plClass).size(); @@ -736,7 +737,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int projectedStrat[node][player].power(numPl, dest, Pr[0], numNei, projFunctions[node]); } if (plClass == ownPlClass) { - aggdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[node][player].at(act), 1.0)); if (dest.size() > 0) { dest.multiply(temp, numNei, projFunctions[node]); @@ -747,7 +748,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int } } if (plClass == plClass2) { - aggdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[node][player].at(act2), 1.0)); if (dest.size() > 0) { dest.multiply(temp, numNei, projFunctions[node]); @@ -828,7 +829,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int } // end while } -double AGG::getKSymMixedPayoff(int playerClass, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, vector> &s) { double result = 0.0; @@ -841,7 +842,7 @@ double AGG::getKSymMixedPayoff(int playerClass, vector &s) return result; } -double AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) +double AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) { double result = 0.0; @@ -854,21 +855,22 @@ double AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) return result; } -void AGG::getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s) +void AGG::getKSymPayoffVector(std::vector &dest, int playerClass, + StrategyProfile &s) { for (size_t act = 0; act < uniqueActionSets[playerClass].size(); ++act) { dest[act] = getKSymMixedPayoff(s, playerClass, act); } } -double AGG::getKSymMixedPayoff(int playerClass, int act, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, int act, vector> &s) { const int numPC = playerClasses.size(); const int numNei = neighbors[uniqueActionSets[playerClass][act]].size(); - static aggdistrib d, temp; + static ConfigDistribution d, temp; d.reset(); temp.reset(); getSymConfigProb(0, s[0], playerClass, act, d); @@ -879,18 +881,18 @@ double AGG::getKSymMixedPayoff(int playerClass, int act, vector return d.inner_prod(payoffs[uniqueActionSets[playerClass][act]]); } -double AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, int pClass2, - int act2) +double AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, + int pClass2, int act2) { const int numPC = playerClasses.size(); const int numNei = neighbors[uniqueActionSets[pClass1][act1]].size(); - static aggdistrib d, temp; + static ConfigDistribution d, temp; if (pClass2 >= 0 && pClass1 == pClass2 && playerClasses.at(pClass1).size() <= 1) { return 0; } d.reset(); temp.reset(); - StrategyProfile s0(getNumKSymActions(0), 0.0); + StrategyProfile s0(getNumKSymActions(0), 0.0); // if (0==pClass2) s0[act2]=1; // else for (int a = firstKSymAction(0); a < lastKSymAction(0); ++a) { @@ -898,7 +900,7 @@ double AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, } getSymConfigProb(0, s0, pClass1, act1, d, pClass2, act2); for (int pc = 1; pc < numPC; pc++) { - StrategyProfile ss(getNumKSymActions(pc), 0.0); + StrategyProfile ss(getNumKSymActions(pc), 0.0); // if (pc==pClass2)ss[act2]=1; // else for (int a = 0; a < getNumKSymActions(pc); ++a) { diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index b3a715bd53..2f070a10e3 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -40,8 +40,9 @@ class aggame; namespace agg { -// data structure for mixed strategy profile -using StrategyProfile = std::vector; +// data structure for a mixed strategy profile: one probability per action, using numeric type V +// (double for the floating-point engine, Rational for the exact-arithmetic counterpart). +template using StrategyProfile = std::vector; // data structure for payoff function: using aggpayoff = trie_map; @@ -53,15 +54,12 @@ using aggpayoff = trie_map; // operators that Number intentionally does not provide. using exactpayoff = std::map, Number>; -// data struct for prob distribution over configurations: -using aggdistrib = trie_map; - -// exact (arbitrary-precision) counterparts of the above, used by the exact mixed-strategy -// payoff computation (getExactMixedPayoff/getExactV/getExactJ). Unlike exactpayoff, these -// participate in the convolution algorithm (multiply/inner_prod), so they need Rational's -// arithmetic -- which, unlike Number, it provides -- and so can reuse trie_map directly. -using ExactStrategyProfile = std::vector; -using exactdistrib = trie_map; +// data struct for a probability distribution over configurations, using numeric type V (double +// for the floating-point engine, Rational for the exact-arithmetic counterpart). Unlike +// aggpayoff/exactpayoff above, this participates in the convolution algorithm +// (multiply/inner_prod), so V must support arithmetic -- which Rational, unlike Number, +// provides -- letting both instantiations reuse trie_map directly. +template using ConfigDistribution = trie_map; // types of input formats for payoff func using payofftype = enum { COMPLETE, MAPPING, ADDITIVE }; @@ -85,10 +83,11 @@ class AGG { // constructor AGG(int numPlayers, std::vector &actions, int numANodes, int numPNodes, std::vector> &actionSets, std::vector> &neighbors, - std::vector &projTypes, std::vector> &projS, + std::vector &projTypes, + std::vector>> &projS, std::vector>> &proj, std::vector> &projF, std::vector>> &Po, - std::vector &P, std::vector &payoffs, + std::vector> &P, std::vector &payoffs, std::vector &exactPayoffs); ~AGG() = default; @@ -110,17 +109,17 @@ class AGG { int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } // exp. payoff under mixed strat profile - double getMixedPayoff(int player, StrategyProfile &s); - void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); - double getV(int player, int action, const StrategyProfile &s); - double getJ(int player, int action, int player2, int action2, StrategyProfile &s); + double getMixedPayoff(int player, StrategyProfile &s); + void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); + double getV(int player, int action, const StrategyProfile &s); + double getJ(int player, int action, int player2, int action2, StrategyProfile &s); // exact counterparts of the above three, computing via the same convolution algorithm but // in Rational arithmetic throughout, rather than double. - Rational getExactMixedPayoff(int player, const ExactStrategyProfile &s); - Rational getExactV(int player, int action, const ExactStrategyProfile &s); + Rational getExactMixedPayoff(int player, const StrategyProfile &s); + Rational getExactV(int player, int action, const StrategyProfile &s); Rational getExactJ(int player, int action, int player2, int action2, - const ExactStrategyProfile &s); + const StrategyProfile &s); double getPurePayoff(int player, const std::vector &s); Number getExactPurePayoff(int player, const std::vector &s) const; @@ -134,17 +133,17 @@ class AGG { } return true; } - double getSymMixedPayoff(StrategyProfile &s); - double getSymMixedPayoff(int actnode, StrategyProfile &s); - void getSymPayoffVector(std::vector &dest, StrategyProfile &s); - double getKSymMixedPayoff(int playerClass, std::vector &s); - double getKSymMixedPayoff(int playerClass, StrategyProfile &s); - double getKSymMixedPayoff(int playerClass, int act, std::vector &s); - double getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, int pClass2 = -1, - int act2 = -1); - void getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s); - - std::vector getExpectedConfig(StrategyProfile &s) + double getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(int actnode, StrategyProfile &s); + void getSymPayoffVector(std::vector &dest, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, std::vector> &s); + double getKSymMixedPayoff(int playerClass, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, int act, std::vector> &s); + double getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, + int pClass2 = -1, int act2 = -1); + void getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s); + + std::vector getExpectedConfig(StrategyProfile &s) { std::vector res(numActionNodes, 0); for (int i = 0; i < numPlayers; ++i) { @@ -229,17 +228,17 @@ class AGG { // foreach s \in S, foreach i \in N, the projected mixed strat // which is a prob distribution over the set of 'contributions' - std::vector> projectedStrat; + std::vector>> projectedStrat; // exact (Rational) counterpart of projectedStrat/Pr below, used only by // getExactMixedPayoff/getExactV/getExactJ. Independent working state from the double // versions, since both may be in use (e.g. from different MixedStrategyProfile instantiations // over the same AGG) and each is rebuilt fresh per call regardless. - std::vector> exactProjectedStrat; - std::vector exactPr; + std::vector>> exactProjectedStrat; + std::vector> exactPr; // foreach s in S, i in N, the full set of projected actions. - std::vector> fullProjectedStrat; + std::vector>> fullProjectedStrat; // foreach s in S, foreach neighbor of s, its projection function std::vector> projFunctions; @@ -255,7 +254,7 @@ class AGG { // when computing the partial distributions for the payoff jacobian: // foreach j \in N, // the partial distribution induced by all agents except j. - std::vector Pr; + std::vector> Pr; // foreach s in S, whether s's neighbors are all action nodes std::vector isPure; @@ -304,7 +303,7 @@ class AGG { } static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int); - static void setProjections(std::vector> &projS, + static void setProjections(std::vector>> &projS, std::vector>> &proj, int N, int S, int P, std::vector> &AS, std::vector> &neighb, @@ -313,17 +312,18 @@ class AGG { static void getAn(std::multiset &dest, std::vector> &neighb, std::vector &projTypes, int S, int Node, std::vector &path); - static void initPorder(std::vector &Po, int i, int N, std::vector &projS); + static void initPorder(std::vector &Po, int i, int N, + std::vector> &projS); // private methods: void computeP(int player, int act, int player2 = -1, int act2 = -1); - void doProjection(int Node, const StrategyProfile &s) + void doProjection(int Node, const StrategyProfile &s) { - doProjection(Node, &(const_cast(s)[0])); + doProjection(Node, &(const_cast &>(s)[0])); } - void doProjection(int Node, int player, const StrategyProfile &s) + void doProjection(int Node, int player, const StrategyProfile &s) { - doProjection(Node, player, &(const_cast(s)[firstAction(player)])); + doProjection(Node, player, &(const_cast &>(s)[firstAction(player)])); } void doProjection(int Node, double *s); void doProjection(int Node, int player, double *s); @@ -331,21 +331,22 @@ class AGG { // exact (Rational) counterparts, mirroring the above precisely. exactPayoffs is a // std::map<..., Number>, not a trie_map, so trie_map's own inner_prod can't be // used directly against it -- this does the equivalent sum manually. - Rational exactInnerProd(int node, const exactdistrib &dist) const; + Rational exactInnerProd(int node, const ConfigDistribution &dist) const; void computeExactP(int player, int act, int player2 = -1, int act2 = -1); - void doExactProjection(int Node, const ExactStrategyProfile &s) + void doExactProjection(int Node, const StrategyProfile &s) { - doExactProjection(Node, &(const_cast(s)[0])); + doExactProjection(Node, &(const_cast &>(s)[0])); } - void doExactProjection(int Node, int player, const ExactStrategyProfile &s) + void doExactProjection(int Node, int player, const StrategyProfile &s) { - doExactProjection(Node, player, &(const_cast(s)[firstAction(player)])); + doExactProjection(Node, player, + &(const_cast &>(s)[firstAction(player)])); } void doExactProjection(int Node, Rational *s); void doExactProjection(int Node, int player, Rational *s); - void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, aggdistrib &dest, - int plClass2 = -1, int act2 = -1); + void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, + ConfigDistribution &dest, int plClass2 = -1, int act2 = -1); }; } // namespace agg diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index 884cba9b75..b75e10a7ae 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -185,7 +185,7 @@ std::shared_ptr BAGG::makeBAGG(istream &in) typeAction2ActionIndex, aggPtr); } -double BAGG::getMixedPayoff(int player, StrategyProfile &s) +double BAGG::getMixedPayoff(int player, StrategyProfile &s) { double res(0); for (int tp = 0; tp < numTypes[player]; ++tp) { @@ -194,7 +194,7 @@ double BAGG::getMixedPayoff(int player, StrategyProfile &s) return res; } -double BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) +double BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) { double res(0); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { @@ -205,7 +205,8 @@ double BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) return res; } -void BAGG::getPayoffVector(std::vector &dest, int player, int tp, const StrategyProfile &s) +void BAGG::getPayoffVector(std::vector &dest, int player, int tp, + const StrategyProfile &s) { assert(player >= 0 && player < getNumPlayers() && tp >= 0 && tp < getNumTypes(player)); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { @@ -213,8 +214,8 @@ void BAGG::getPayoffVector(std::vector &dest, int player, int tp, const } } -void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, int tp, - int action) +void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, + int tp, int action) { for (int i = 0; i < aggPtr->getNumActions(); ++i) { as[i] = double(0.0); @@ -235,16 +236,16 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player } } } -double BAGG::getV(int player, int tp, int action, const StrategyProfile &s) +double BAGG::getV(int player, int tp, int action, const StrategyProfile &s) { - StrategyProfile as(aggPtr->getNumActions()); + StrategyProfile as(aggPtr->getNumActions()); getAGGStrat(as, s, player, tp, action); return aggPtr->getV(player, typeAction2ActionIndex[player][tp][action], as); } // Exact (Rational) counterparts of getAGGStrat/getV/getMixedPayoff above. -void BAGG::getExactAGGStrat(ExactStrategyProfile &as, const ExactStrategyProfile &s, int player, - int tp, int action) const +void BAGG::getExactAGGStrat(StrategyProfile &as, const StrategyProfile &s, + int player, int tp, int action) const { for (int i = 0; i < aggPtr->getNumActions(); ++i) { as[i] = Rational(0); @@ -267,14 +268,14 @@ void BAGG::getExactAGGStrat(ExactStrategyProfile &as, const ExactStrategyProfile } } -Rational BAGG::getExactV(int player, int tp, int action, const ExactStrategyProfile &s) const +Rational BAGG::getExactV(int player, int tp, int action, const StrategyProfile &s) const { - ExactStrategyProfile as(aggPtr->getNumActions()); + StrategyProfile as(aggPtr->getNumActions()); getExactAGGStrat(as, s, player, tp, action); return aggPtr->getExactV(player, typeAction2ActionIndex[player][tp][action], as); } -Rational BAGG::getExactMixedPayoff(int player, int tp, const ExactStrategyProfile &s) const +Rational BAGG::getExactMixedPayoff(int player, int tp, const StrategyProfile &s) const { Rational res(0); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { @@ -287,7 +288,7 @@ Rational BAGG::getExactMixedPayoff(int player, int tp, const ExactStrategyProfil double BAGG::getPurePayoff(int player, int tp, std::vector &ps) { - StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); + StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); for (int i = 0; i < strategyOffset[typeOffset[numPlayers]]; i++) { st[i] = (double)0.0; } @@ -341,7 +342,7 @@ Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps return total; } -void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) +void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) { for (int i = 0; i < aggPtr->getNumActionNodes(); ++i) { as[i] = double(0.0); @@ -354,7 +355,7 @@ void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) } } -double BAGG::getSymMixedPayoff(StrategyProfile &s) +double BAGG::getSymMixedPayoff(StrategyProfile &s) { double res(0); for (int tp = 0; tp < numTypes[0]; ++tp) { @@ -363,7 +364,7 @@ double BAGG::getSymMixedPayoff(StrategyProfile &s) return res; } -double BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) { double res(0); for (size_t act = 0; act < typeActionSets[0][tp].size(); ++act) { @@ -374,9 +375,9 @@ double BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) return res; } -double BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) { - StrategyProfile as(aggPtr->getNumActionNodes()); + StrategyProfile as(aggPtr->getNumActionNodes()); getSymAGGStrat(as, s); return aggPtr->getSymMixedPayoff(typeActionSets[0][tp][act], as); } diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index f5d5930520..149470e1d0 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -69,16 +69,17 @@ class BAGG { Number getExactMinPayoff() const { return aggPtr->getExactMinPayoff(); } // exp. payoff under mixed strat profile - double getMixedPayoff(int player, StrategyProfile &s); + double getMixedPayoff(int player, StrategyProfile &s); // exp payoff for player, conditioned on her receiving type tp. - double getMixedPayoff(int player, int tp, StrategyProfile &s); + double getMixedPayoff(int player, int tp, StrategyProfile &s); - void getPayoffVector(std::vector &dest, int player, int tp, const StrategyProfile &s); - double getV(int player, int tp, int action, const StrategyProfile &s); + void getPayoffVector(std::vector &dest, int player, int tp, + const StrategyProfile &s); + double getV(int player, int tp, int action, const StrategyProfile &s); // exact counterparts, computing via Rational arithmetic throughout (see agg.h) - Rational getExactMixedPayoff(int player, int tp, const ExactStrategyProfile &s) const; - Rational getExactV(int player, int tp, int action, const ExactStrategyProfile &s) const; + Rational getExactMixedPayoff(int player, int tp, const StrategyProfile &s) const; + Rational getExactV(int player, int tp, int action, const StrategyProfile &s) const; double getPurePayoff(int player, int tp, std::vector &s); double getPurePayoff(int player, std::vector &s) @@ -91,11 +92,11 @@ class BAGG { } Rational getExactPurePayoff(int player, int tp, const std::vector &ps) const; - double getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(StrategyProfile &s); - double getSymMixedPayoff(int tp, StrategyProfile &s); + double getSymMixedPayoff(int tp, StrategyProfile &s); - double getSymMixedPayoff(int tp, int act, StrategyProfile &s); + double getSymMixedPayoff(int tp, int act, StrategyProfile &s); bool isSymmetric() const { return symmetric; } @@ -119,11 +120,11 @@ class BAGG { bool symmetric; - void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, int tp = -1, - int action = -1); - void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); - void getExactAGGStrat(ExactStrategyProfile &as, const ExactStrategyProfile &s, int player, - int tp, int action) const; + void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, + int tp = -1, int action = -1); + void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); + void getExactAGGStrat(StrategyProfile &as, const StrategyProfile &s, + int player, int tp, int action) const; }; std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/solvers/gtracer/aggame.cc b/src/solvers/gtracer/aggame.cc index 3ecc85c772..eedc0eb9e2 100644 --- a/src/solvers/gtracer/aggame.cc +++ b/src/solvers/gtracer/aggame.cc @@ -41,7 +41,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t aggPtr->Pr[player1].swap(aggPtr->Pr[numPlayers - 1]); for (i = 0; i < (int)tasks.size(); i++) { // assert(tasks[i]!=player1); - agg::aggdistrib &P = aggPtr->Pr[tasks[i]]; + agg::ConfigDistribution &P = aggPtr->Pr[tasks[i]]; // P.clear(); // to get ready for division, we need clear() P = aggPtr->Pr[player1]; @@ -79,7 +79,8 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t } void aggame::computePartialP_bisect(int player1, int act1, std::vector::iterator start, - std::vector::iterator endp, agg::aggdistrib &temp) const + std::vector::iterator endp, + agg::ConfigDistribution &temp) const { #ifdef AGGDEBUG cout << "calling computePartialP_bisect with player1=" << player1 << ", act1=" << act1 diff --git a/src/solvers/gtracer/aggame.h b/src/solvers/gtracer/aggame.h index 120d1f776c..246523ee06 100644 --- a/src/solvers/gtracer/aggame.h +++ b/src/solvers/gtracer/aggame.h @@ -93,7 +93,8 @@ class aggame : public gnmgame { // helper functions for computing jacobian void computePartialP_PureNode(int player, int act, std::vector &tasks) const; void computePartialP_bisect(int player, int act, std::vector::iterator f, - std::vector::iterator l, Gambit::agg::aggdistrib &temp) const; + std::vector::iterator l, + Gambit::agg::ConfigDistribution &temp) const; void computePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, Gambit::agg::trie_map &cache) const; void savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, double result, From 4c81e81eee5b4945297d14a0b1856c0b03ee803a Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 09:55:20 -0400 Subject: [PATCH 04/14] Standardise type name --- src/games/agg/agg.cc | 14 +++++++------- src/games/agg/agg.h | 10 +++++----- src/solvers/gtracer/aggame.cc | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 77c2790237..7454757989 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -36,7 +36,7 @@ inline int select2nd(const pair &x) { return x.second; } AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNodes, vector> &_actionSets, vector> &neighb, vector &projTypes, vector>> &projS, - vector>> &proj, vector> &projF, + vector>> &proj, vector> &projF, vector>> &Po, vector> &P, vector &_payoffs, vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), @@ -224,7 +224,7 @@ std::shared_ptr AGG::makeAGG(istream &in) } vector>> projS; - vector>> proj; + vector>> proj; setProjections(projS, proj, n, S, P, ASets, neighb, projTypes); vector> projF(S); @@ -298,7 +298,7 @@ std::shared_ptr AGG::makeAGG(istream &in) } void AGG::setProjections(vector>> &projS, - vector>> &proj, int N, int S, int P, + vector>> &proj, int N, int S, int P, vector> &AS, vector> &neighb, vector &projTypes) { @@ -491,7 +491,7 @@ double AGG::getPurePayoff(int player, const std::vector &s) assert(player >= 0 && player < numPlayers); const int Node = actionSets[player][s[player]]; const int keylen = neighbors[Node].size(); - config pureprofile(projection[Node][0][s[0]]); + Config pureprofile(projection[Node][0][s[0]]); for (int i = 1; i < numPlayers; i++) { for (int j = 0; j < keylen; j++) { pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); @@ -514,7 +514,7 @@ Number AGG::getExactPurePayoff(int player, const std::vector &s) const assert(player >= 0 && player < numPlayers); const int Node = actionSets[player][s[player]]; const int keylen = neighbors[Node].size(); - config pureprofile(projection[Node][0][s[0]]); + Config pureprofile(projection[Node][0][s[0]]); for (int i = 1; i < numPlayers; i++) { for (int j = 0; j < keylen; j++) { pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); @@ -677,7 +677,7 @@ double AGG::getSymMixedPayoff(int node, StrategyProfile &s) while (true) { const vector &comp = gc.get(); - config c(numNei, 0); + Config c(numNei, 0); for (size_t j = 0; j < support.size(); ++j) { if (support[j] != -1) { c[support[j]] = comp[j]; @@ -795,7 +795,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlCla while (true) { const vector &comp = gc.get(); - config c(numNei, 0); + Config c(numNei, 0); for (size_t j = 0; j < support.size(); ++j) { if (support[j] != -1) { c[support[j]] = comp[j]; diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index 2f070a10e3..28fdc090a9 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -67,7 +67,7 @@ using payofftype = enum { COMPLETE, MAPPING, ADDITIVE }; class AGG { public: - using config = std::vector; + using Config = std::vector; using ActionSet = std::vector; using PlayerSet = std::vector; @@ -85,7 +85,7 @@ class AGG { std::vector> &actionSets, std::vector> &neighbors, std::vector &projTypes, std::vector>> &projS, - std::vector>> &proj, + std::vector>> &proj, std::vector> &projF, std::vector>> &Po, std::vector> &P, std::vector &payoffs, std::vector &exactPayoffs); @@ -159,7 +159,7 @@ class AGG { { return Porder.at(player).at(action); } - const std::vector> &getProjection(int node) { return projection.at(node); } + const std::vector> &getProjection(int node) { return projection.at(node); } const std::vector &getActionSet(int player) { return actionSets.at(player); } const aggpayoff &getPayoffMap(int node) { return payoffs.at(node); } @@ -224,7 +224,7 @@ class AGG { // originally: // foreach s \in S, foreach i \in N, foreach s_i \in S_i, // the 'contribution' of s_i to D^(s) - std::vector>> projection; + std::vector>> projection; // foreach s \in S, foreach i \in N, the projected mixed strat // which is a prob distribution over the set of 'contributions' @@ -304,7 +304,7 @@ class AGG { static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int); static void setProjections(std::vector>> &projS, - std::vector>> &proj, int N, int S, + std::vector>> &proj, int N, int S, int P, std::vector> &AS, std::vector> &neighb, std::vector &projTypes); diff --git a/src/solvers/gtracer/aggame.cc b/src/solvers/gtracer/aggame.cc index eedc0eb9e2..e1996ad64c 100644 --- a/src/solvers/gtracer/aggame.cc +++ b/src/solvers/gtracer/aggame.cc @@ -33,7 +33,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t // assert(aggPtr->isPure[Node]||tasks.size()==0); std::vector strat(numNei); - agg::AGG::config a(numNei, 0); + agg::AGG::Config a(numNei, 0); // compute the full distrib aggPtr->computeP(player1, act1); From c8db2794c00524da427390f76a2957bcff386c9f Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 10:22:30 -0400 Subject: [PATCH 05/14] More type name cleanup --- src/games/agg/agg.cc | 11 +++++----- src/games/agg/agg.h | 51 ++++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 7454757989..b69682cc0f 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -38,7 +38,7 @@ AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNod vector &projTypes, vector>> &projS, vector>> &proj, vector> &projF, vector>> &Po, vector> &P, - vector &_payoffs, vector &_exactPayoffs) + vector &_payoffs, vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), actionSets(_actionSets), neighbors(neighb), projectionTypes(projTypes), payoffs(_payoffs), exactPayoffs(_exactPayoffs), projection(proj), projectedStrat(projS), @@ -239,8 +239,8 @@ std::shared_ptr AGG::makeAGG(istream &in) vector>> Po(n); vector> Pr(n); - vector pays(S); // payoffs - vector exactPays(S); // exact (arbitrary-precision) counterpart of pays + vector pays(S); // payoffs + vector exactPays(S); // exact (arbitrary-precision) counterpart of pays set> doneASets; for (i = 0; i < n; i++) { @@ -912,11 +912,12 @@ double AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, in return d.inner_prod(payoffs[uniqueActionSets[pClass1][act1]]); } -void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int numNei) +void AGG::makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay, + int numNei) { int num; char c; - aggpayoff temp; + PayoffTable temp; // temp.swap(pay); temp = pay; pay.clear(); diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index 28fdc090a9..cd6e4c1872 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -44,25 +44,33 @@ namespace agg { // (double for the floating-point engine, Rational for the exact-arithmetic counterpart). template using StrategyProfile = std::vector; -// data structure for payoff function: -using aggpayoff = trie_map; - -// exact (arbitrary-precision) counterpart of a payoff function, populated alongside aggpayoff -// at parse time. Only used for storage/lookup (find/insert/iterate), never for the -// floating-point convolution machinery, so a plain std::map suffices -- no need to fight -// trie_map's explicit whole-class instantiation, which requires V to support arithmetic -// operators that Number intentionally does not provide. -using exactpayoff = std::map, Number>; +// the local payoff function for one action node: maps a neighbor configuration to the payoff of +// playing that node under it. Populated at parse time; consumed by the floating-point +// convolution engine (getMixedPayoff/getV/getJ) via trie_map's own inner_prod, so it has to stay +// a trie_map -- unlike StrategyProfile/ConfigDistribution above, this is deliberately not +// a template (see ExactPayoffTable below for why a Rational instantiation would be unsound). +using PayoffTable = trie_map; + +// exact (arbitrary-precision) counterpart of PayoffTable, populated alongside it at parse time +// from the same source text. Deliberately not a trie_map, for two independent reasons: +// (1) trie_map's own inner_prod has an epsilon-tolerance discard/throw policy +// (trie_map::THRESH = 1e-12) that is meaningful for double but unsound for exact arithmetic -- +// there is no notion of "too small to matter" once payoffs are exact; and (2) this stores Number, +// not Rational, to preserve the original entered text for exact serialization (printPayoffs), +// which a bare Rational cannot do. As a result this is only ever used for storage/lookup +// (find/insert/iterate) and a hand-written inner product (AGG::exactInnerProd), never trie_map's +// own arithmetic. +using ExactPayoffTable = std::map, Number>; // data struct for a probability distribution over configurations, using numeric type V (double // for the floating-point engine, Rational for the exact-arithmetic counterpart). Unlike -// aggpayoff/exactpayoff above, this participates in the convolution algorithm +// PayoffTable/ExactPayoffTable above, this participates in the convolution algorithm // (multiply/inner_prod), so V must support arithmetic -- which Rational, unlike Number, // provides -- letting both instantiations reuse trie_map directly. template using ConfigDistribution = trie_map; // types of input formats for payoff func -using payofftype = enum { COMPLETE, MAPPING, ADDITIVE }; +using PayoffType = enum { COMPLETE, MAPPING, ADDITIVE }; class AGG { @@ -87,8 +95,8 @@ class AGG { std::vector>> &projS, std::vector>> &proj, std::vector> &projF, std::vector>> &Po, - std::vector> &P, std::vector &payoffs, - std::vector &exactPayoffs); + std::vector> &P, std::vector &payoffs, + std::vector &exactPayoffs); ~AGG() = default; @@ -161,7 +169,7 @@ class AGG { } const std::vector> &getProjection(int node) { return projection.at(node); } const std::vector &getActionSet(int player) { return actionSets.at(player); } - const aggpayoff &getPayoffMap(int node) { return payoffs.at(node); } + const PayoffTable &getPayoffMap(int node) { return payoffs.at(node); } double getMaxPayoff() const; double getMinPayoff() const; @@ -214,10 +222,10 @@ class AGG { std::vector projectionTypes; // payoff function for each action node \in S - std::vector payoffs; + std::vector payoffs; // exact (arbitrary-precision) counterpart of payoffs, for each action node \in S - std::vector exactPayoffs; + std::vector exactPayoffs; // auxiliary data structures @@ -282,8 +290,8 @@ class AGG { // input functor struct input { - input(std::istream &i, exactpayoff &e) : in(i), exact(e) {} - void operator()(aggpayoff::iterator p) + input(std::istream &i, ExactPayoffTable &e) : in(i), exact(e) {} + void operator()(PayoffTable::iterator p) { std::string word; in >> word; @@ -292,16 +300,17 @@ class AGG { exact.insert(std::make_pair(p->first, num)); } std::istream ∈ - exactpayoff &exact; + ExactPayoffTable &exact; }; // private static methods: - static void makeCOMPLETEpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay) + static void makeCOMPLETEpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay) { pay.in_order(input(in, exactPay)); } - static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, exactpayoff &exactPay, int); + static void makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay, + int); static void setProjections(std::vector>> &projS, std::vector>> &proj, int N, int S, From ee07c35cf60e2726668fe2d4eea918d4ffd10d46 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 10:35:33 -0400 Subject: [PATCH 06/14] More type name cleanup --- src/games/agg/agg.cc | 94 +++++++++++++++++++++---------------------- src/games/agg/agg.h | 23 +++++------ src/games/agg/bagg.cc | 86 ++++++++++++++++----------------------- src/games/agg/bagg.h | 24 +++++------ src/games/gameagg.cc | 19 ++------- src/games/gamebagg.cc | 19 ++------- 6 files changed, 108 insertions(+), 157 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index b69682cc0f..b5e5c4973e 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -24,6 +24,7 @@ #include #include #include +#include #include "games/agg/gray.h" #include "games/agg/agg.h" @@ -532,41 +533,6 @@ Number AGG::getExactPurePayoff(int player, const std::vector &s) const return p->second; } -double AGG::getMixedPayoff(int player, StrategyProfile &s) -{ - double result = 0.0; - assert(player >= 0 && player < numPlayers); - for (int act = 0; act < actions[player]; ++act) { - if (s[act + firstAction(player)] > (double)0.0) { - result += s[act + firstAction(player)] * getV(player, act, s); - } - } - return result; -} - -void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) -{ - assert(player >= 0 && player < numPlayers); - for (int act = 0; act < actions[player]; ++act) { - dest[act] = getV(player, act, s); - } -} - -double AGG::getV(int player, int act, const StrategyProfile &s) -{ - // project s to the projectedStrat - doProjection(actionSets.at(player).at(act), s); - computeP(player, act); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); -} - -double AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) -{ - doProjection(actionSets[player1][act1], s); - computeP(player1, act1, player2, act2); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); -} - Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) const { Rational result(0); @@ -579,33 +545,65 @@ Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) return result; } -Rational AGG::getExactV(int player, int act, const StrategyProfile &s) +template V AGG::getV(int player, int act, const StrategyProfile &s) { - doExactProjection(actionSets.at(player).at(act), s); - computeExactP(player, act); - return exactInnerProd(actionSets[player][act], exactPr[numPlayers - 1]); + if constexpr (std::is_same_v) { + doExactProjection(actionSets.at(player).at(act), s); + computeExactP(player, act); + return exactInnerProd(actionSets[player][act], exactPr[numPlayers - 1]); + } + else { + // project s to the projectedStrat + doProjection(actionSets.at(player).at(act), s); + computeP(player, act); + return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); + } } -Rational AGG::getExactJ(int player1, int act1, int player2, int act2, - const StrategyProfile &s) +template +V AGG::getJ(int player1, int act1, int player2, int act2, const StrategyProfile &s) { - doExactProjection(actionSets[player1][act1], s); - computeExactP(player1, act1, player2, act2); - return exactInnerProd(actionSets[player1][act1], exactPr[numPlayers - 1]); + if constexpr (std::is_same_v) { + doExactProjection(actionSets[player1][act1], s); + computeExactP(player1, act1, player2, act2); + return exactInnerProd(actionSets[player1][act1], exactPr[numPlayers - 1]); + } + else { + doProjection(actionSets[player1][act1], s); + computeP(player1, act1, player2, act2); + return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); + } } -Rational AGG::getExactMixedPayoff(int player, const StrategyProfile &s) +template V AGG::getMixedPayoff(int player, const StrategyProfile &s) { - Rational result(0); + V result(0); assert(player >= 0 && player < numPlayers); for (int act = 0; act < actions[player]; ++act) { - if (s[act + firstAction(player)] > Rational(0)) { - result += s[act + firstAction(player)] * getExactV(player, act, s); + if (s[act + firstAction(player)] > V(0)) { + result += s[act + firstAction(player)] * getV(player, act, s); } } return result; } +void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) +{ + assert(player >= 0 && player < numPlayers); + for (int act = 0; act < actions[player]; ++act) { + dest[act] = getV(player, act, s); + } +} + +template double AGG::getV(int player, int act, const StrategyProfile &s); +template Rational AGG::getV(int player, int act, const StrategyProfile &s); +template double AGG::getJ(int player1, int act1, int player2, int act2, + const StrategyProfile &s); +template Rational AGG::getJ(int player1, int act1, int player2, int act2, + const StrategyProfile &s); +template double AGG::getMixedPayoff(int player, const StrategyProfile &s); +template Rational AGG::getMixedPayoff(int player, const StrategyProfile &s); + // getSymMixedPayoff: compute expected payoff under a symmetric mixed strat, // for a symmetric game. // parameter: s is the mixed strategy of one player. It is a vector of diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index cd6e4c1872..f3288491c7 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -116,18 +116,15 @@ class AGG { int firstKSymAction(int i) const { return kSymStrategyOffset[i]; } int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } - // exp. payoff under mixed strat profile - double getMixedPayoff(int player, StrategyProfile &s); + // exp. payoff under mixed strat profile, via the convolution algorithm. V=double runs the + // floating-point engine; V=Rational runs the exact-arithmetic counterpart throughout + // (doProjection/computeP/inner_prod vs doExactProjection/computeExactP/exactInnerProd) -- + // dispatched at compile time in the .cc, not overloaded, since it's the same algorithm. + template V getMixedPayoff(int player, const StrategyProfile &s); void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); - double getV(int player, int action, const StrategyProfile &s); - double getJ(int player, int action, int player2, int action2, StrategyProfile &s); - - // exact counterparts of the above three, computing via the same convolution algorithm but - // in Rational arithmetic throughout, rather than double. - Rational getExactMixedPayoff(int player, const StrategyProfile &s); - Rational getExactV(int player, int action, const StrategyProfile &s); - Rational getExactJ(int player, int action, int player2, int action2, - const StrategyProfile &s); + template V getV(int player, int action, const StrategyProfile &s); + template + V getJ(int player, int action, int player2, int action2, const StrategyProfile &s); double getPurePayoff(int player, const std::vector &s); Number getExactPurePayoff(int player, const std::vector &s) const; @@ -238,8 +235,8 @@ class AGG { // which is a prob distribution over the set of 'contributions' std::vector>> projectedStrat; - // exact (Rational) counterpart of projectedStrat/Pr below, used only by - // getExactMixedPayoff/getExactV/getExactJ. Independent working state from the double + // exact (Rational) counterpart of projectedStrat/Pr below, used only by the Rational + // instantiations of getMixedPayoff/getV/getJ. Independent working state from the double // versions, since both may be in use (e.g. from different MixedStrategyProfile instantiations // over the same AGG) and each is rebuilt fresh per call regardless. std::vector>> exactProjectedStrat; diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index b75e10a7ae..fcce093d3c 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include "games/agg/bagg.h" using namespace std; @@ -194,11 +195,11 @@ double BAGG::getMixedPayoff(int player, StrategyProfile &s) return res; } -double BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) +template V BAGG::getMixedPayoff(int player, int tp, const StrategyProfile &s) const { - double res(0); + V res(0); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { - if (s[act + firstAction(player, tp)] > double(0.0)) { + if (s[act + firstAction(player, tp)] > V(0)) { res += s[act + firstAction(player, tp)] * getV(player, tp, act, s); } } @@ -214,11 +215,12 @@ void BAGG::getPayoffVector(std::vector &dest, int player, int tp, } } -void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, - int tp, int action) +template +void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, int tp, + int action) const { for (int i = 0; i < aggPtr->getNumActions(); ++i) { - as[i] = double(0.0); + as[i] = V(0); } for (int pl = 0; pl < numPlayers; ++pl) { @@ -226,65 +228,45 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfilefirstAction(pl)] += indepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + if constexpr (std::is_same_v) { + as[aact + aggPtr->firstAction(pl)] += + (Rational)exactIndepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + } + else { + as[aact + aggPtr->firstAction(pl)] += + indepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + } } } } else { const int aact = typeAction2ActionIndex[player][tp][action]; - as[aact + aggPtr->firstAction(player)] = 1; + as[aact + aggPtr->firstAction(player)] = V(1); } } } -double BAGG::getV(int player, int tp, int action, const StrategyProfile &s) + +template V BAGG::getV(int player, int tp, int action, const StrategyProfile &s) const { - StrategyProfile as(aggPtr->getNumActions()); + StrategyProfile as(aggPtr->getNumActions()); getAGGStrat(as, s, player, tp, action); return aggPtr->getV(player, typeAction2ActionIndex[player][tp][action], as); } -// Exact (Rational) counterparts of getAGGStrat/getV/getMixedPayoff above. -void BAGG::getExactAGGStrat(StrategyProfile &as, const StrategyProfile &s, - int player, int tp, int action) const -{ - for (int i = 0; i < aggPtr->getNumActions(); ++i) { - as[i] = Rational(0); - } - - for (int pl = 0; pl < numPlayers; ++pl) { - if (pl != player) { - for (int t = 0; t < numTypes[pl]; ++t) { - for (size_t act = 0; act < typeActionSets[pl][t].size(); ++act) { - const int aact = typeAction2ActionIndex[pl][t][act]; - as[aact + aggPtr->firstAction(pl)] += - (Rational)exactIndepTypeDist[pl][t] * s[act + firstAction(pl, t)]; - } - } - } - else { - const int aact = typeAction2ActionIndex[player][tp][action]; - as[aact + aggPtr->firstAction(player)] = Rational(1); - } - } -} - -Rational BAGG::getExactV(int player, int tp, int action, const StrategyProfile &s) const -{ - StrategyProfile as(aggPtr->getNumActions()); - getExactAGGStrat(as, s, player, tp, action); - return aggPtr->getExactV(player, typeAction2ActionIndex[player][tp][action], as); -} - -Rational BAGG::getExactMixedPayoff(int player, int tp, const StrategyProfile &s) const -{ - Rational res(0); - for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { - if (s[act + firstAction(player, tp)] > Rational(0)) { - res += s[act + firstAction(player, tp)] * getExactV(player, tp, act, s); - } - } - return res; -} +template void BAGG::getAGGStrat(StrategyProfile &as, + const StrategyProfile &s, int player, int tp, + int action) const; +template void BAGG::getAGGStrat(StrategyProfile &as, + const StrategyProfile &s, int player, int tp, + int action) const; +template double BAGG::getV(int player, int tp, int action, + const StrategyProfile &s) const; +template Rational BAGG::getV(int player, int tp, int action, + const StrategyProfile &s) const; +template double BAGG::getMixedPayoff(int player, int tp, + const StrategyProfile &s) const; +template Rational BAGG::getMixedPayoff(int player, int tp, + const StrategyProfile &s) const; double BAGG::getPurePayoff(int player, int tp, std::vector &ps) { diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 149470e1d0..a7a86dfab6 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -68,18 +68,19 @@ class BAGG { Number getExactMaxPayoff() const { return aggPtr->getExactMaxPayoff(); } Number getExactMinPayoff() const { return aggPtr->getExactMinPayoff(); } - // exp. payoff under mixed strat profile + // exp. payoff under mixed strat profile, summed over player's types. No Rational counterpart -- + // unused outside this class; getPurePayoff and GameBAGGRep only ever go through the per-type + // overload below. double getMixedPayoff(int player, StrategyProfile &s); - // exp payoff for player, conditioned on her receiving type tp. - double getMixedPayoff(int player, int tp, StrategyProfile &s); + + // exp payoff for player, conditioned on her receiving type tp, via the convolution algorithm. + // V=double runs the floating-point engine; V=Rational runs the exact-arithmetic counterpart + // throughout -- see agg.h::getMixedPayoff for how the dispatch works. + template V getMixedPayoff(int player, int tp, const StrategyProfile &s) const; void getPayoffVector(std::vector &dest, int player, int tp, const StrategyProfile &s); - double getV(int player, int tp, int action, const StrategyProfile &s); - - // exact counterparts, computing via Rational arithmetic throughout (see agg.h) - Rational getExactMixedPayoff(int player, int tp, const StrategyProfile &s) const; - Rational getExactV(int player, int tp, int action, const StrategyProfile &s) const; + template V getV(int player, int tp, int action, const StrategyProfile &s) const; double getPurePayoff(int player, int tp, std::vector &s); double getPurePayoff(int player, std::vector &s) @@ -120,11 +121,10 @@ class BAGG { bool symmetric; - void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, - int tp = -1, int action = -1); + template + void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, + int tp = -1, int action = -1) const; void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); - void getExactAGGStrat(StrategyProfile &as, const StrategyProfile &s, - int player, int tp, int action) const; }; std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 70b72dfa67..1ed79d1e28 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -93,19 +93,6 @@ template class AGGMixedStrategyProfileRep : public MixedStrategyProfil T GetPayoffDeriv(int pl, const GameStrategy &, const GameStrategy &) const override; }; -// A single dispatch point: T=double goes through AGG's original floating-point convolution -// engine (getMixedPayoff), T=Rational goes through its exact Rational-arithmetic counterpart -// (getExactMixedPayoff) -- same algorithm, different numeric type throughout, per agg.h. -template T CallMixedPayoff(agg::AGG &g, int pl, std::vector &s) -{ - if constexpr (std::is_same_v) { - return g.getExactMixedPayoff(pl, s); - } - else { - return g.getMixedPayoff(pl, s); - } -} - template T AGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); @@ -118,7 +105,7 @@ template T AGGMixedStrategyProfileRep::GetPayoff(int pl) const s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } - return CallMixedPayoff(*g.aggPtr, pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template @@ -142,7 +129,7 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } } } - return CallMixedPayoff(*g.aggPtr, pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template @@ -179,7 +166,7 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1, } } } - return CallMixedPayoff(*g.aggPtr, pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template class AGGMixedStrategyProfileRep; diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 196c9b17f1..21b25ab925 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -96,19 +96,6 @@ template class BAGGMixedStrategyProfileRep : public MixedStrategyProfi T GetPayoffDeriv(int pl, const GameStrategy &, const GameStrategy &) const override; }; -// A single dispatch point: T=double goes through BAGG's original floating-point convolution -// engine (getMixedPayoff), T=Rational goes through its exact Rational-arithmetic counterpart -// (getExactMixedPayoff) -- same algorithm, different numeric type throughout, per agg.h/bagg.h. -template T CallMixedPayoff(agg::BAGG &g, int bplayer, int btype, std::vector &s) -{ - if constexpr (std::is_same_v) { - return g.getExactMixedPayoff(bplayer, btype, s); - } - else { - return g.getMixedPayoff(bplayer, btype, s); - } -} - template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); @@ -130,7 +117,7 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const } } } - return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template @@ -163,7 +150,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } } } - return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template @@ -210,7 +197,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 } } } - return CallMixedPayoff(*g.baggPtr, bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template class BAGGMixedStrategyProfileRep; From 2bb6bbce3a0e9a0938288e9c36a04065c790fbff Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 10:58:38 -0400 Subject: [PATCH 07/14] More type name cleanup --- src/games/agg/agg.cc | 127 +++++++++++++++++------------------ src/games/agg/agg.h | 18 +++-- src/games/agg/bagg.cc | 10 +-- src/games/agg/bagg.h | 6 +- src/games/gameagg.cc | 4 +- src/games/gameagg.h | 4 +- src/games/gamebagg.h | 4 +- src/solvers/gtracer/aggame.h | 2 +- 8 files changed, 85 insertions(+), 90 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index b5e5c4973e..a0df7771f2 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -487,7 +487,7 @@ void AGG::doExactProjection(int Node, int i, Rational *s) } } -double AGG::getPurePayoff(int player, const std::vector &s) +template V AGG::getPurePayoff(int player, const std::vector &s) const { assert(player >= 0 && player < numPlayers); const int Node = actionSets[player][s[player]]; @@ -498,48 +498,42 @@ double AGG::getPurePayoff(int player, const std::vector &s) pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); } } - auto p = payoffs[Node].find(pureprofile); - if (p == payoffs[Node].end()) { - std::stringstream str; - str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; - str << "["; - copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); - str << "] in payoffs of action node #" << Node; - throw std::runtime_error(str.str()); - } - return p->second; -} - -Number AGG::getExactPurePayoff(int player, const std::vector &s) const -{ - assert(player >= 0 && player < numPlayers); - const int Node = actionSets[player][s[player]]; - const int keylen = neighbors[Node].size(); - Config pureprofile(projection[Node][0][s[0]]); - for (int i = 1; i < numPlayers; i++) { - for (int j = 0; j < keylen; j++) { - pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); + if constexpr (std::is_same_v) { + auto p = exactPayoffs[Node].find(pureprofile); + if (p == exactPayoffs[Node].end()) { + std::stringstream str; + str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; + str << "["; + copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); + str << "] in payoffs of action node #" << Node; + throw std::runtime_error(str.str()); } + return static_cast(p->second); } - auto p = exactPayoffs[Node].find(pureprofile); - if (p == exactPayoffs[Node].end()) { - std::stringstream str; - str << "AGG::getExactPurePayoff ERROR: unable to find the following configuration "; - str << "["; - copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); - str << "] in payoffs of action node #" << Node; - throw std::runtime_error(str.str()); + else { + auto p = payoffs[Node].find(pureprofile); + if (p == payoffs[Node].end()) { + std::stringstream str; + str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; + str << "["; + copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); + str << "] in payoffs of action node #" << Node; + throw std::runtime_error(str.str()); + } + return p->second; } - return p->second; } +template double AGG::getPurePayoff(int player, const std::vector &s) const; +template Rational AGG::getPurePayoff(int player, const std::vector &s) const; + Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) const { Rational result(0); for (const auto &entry : dist) { const auto it = exactPayoffs[node].find(entry.first); if (it != exactPayoffs[node].end() && entry.second != Rational(0)) { - result += entry.second * (Rational)it->second; + result += entry.second * static_cast(it->second); } } return result; @@ -968,7 +962,7 @@ void AGG::makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable throw std::runtime_error(str.str()); } const Number num(word); - const double u = (double)num; + const double u = static_cast(num); // insert const pair::iterator, bool> r = pay.insert(make_pair(key, u)); @@ -999,56 +993,55 @@ void AGG::makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable } } -double AGG::getMaxPayoff() const +template V AGG::getMaxPayoff() const { assert(numActionNodes > 0); - double result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { - for (const auto &it : payoffs[i]) { - result = max(result, it.second); + if constexpr (std::is_same_v) { + Rational result = static_cast(exactPayoffs[0].begin()->second); + for (int i = 1; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + result = max(result, static_cast(it.second)); + } } + return result; } - return result; -} - -double AGG::getMinPayoff() const -{ - assert(numActionNodes > 0); - double result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { - for (const auto &it : payoffs[i]) { - result = min(result, it.second); + else { + double result = payoffs[0].begin()->second; + for (int i = 1; i < numActionNodes; i++) { + for (const auto &it : payoffs[i]) { + result = max(result, it.second); + } } + return result; } - return result; } -Number AGG::getExactMaxPayoff() const +template V AGG::getMinPayoff() const { assert(numActionNodes > 0); - Number result = exactPayoffs[0].begin()->second; - for (int i = 0; i < numActionNodes; i++) { - for (const auto &it : exactPayoffs[i]) { - if ((Rational)it.second > (Rational)result) { - result = it.second; + if constexpr (std::is_same_v) { + Rational result = static_cast(exactPayoffs[0].begin()->second); + for (int i = 1; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + result = min(result, static_cast(it.second)); } } + return result; } - return result; -} - -Number AGG::getExactMinPayoff() const -{ - assert(numActionNodes > 0); - Number result = exactPayoffs[0].begin()->second; - for (int i = 0; i < numActionNodes; i++) { - for (const auto &it : exactPayoffs[i]) { - if ((Rational)it.second < (Rational)result) { - result = it.second; + else { + double result = payoffs[0].begin()->second; + for (int i = 1; i < numActionNodes; i++) { + for (const auto &it : payoffs[i]) { + result = min(result, it.second); } } + return result; } - return result; } +template double AGG::getMaxPayoff() const; +template Rational AGG::getMaxPayoff() const; +template double AGG::getMinPayoff() const; +template Rational AGG::getMinPayoff() const; + } // namespace Gambit::agg diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index f3288491c7..a7f8c745cf 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -126,8 +126,12 @@ class AGG { template V getJ(int player, int action, int player2, int action2, const StrategyProfile &s); - double getPurePayoff(int player, const std::vector &s); - Number getExactPurePayoff(int player, const std::vector &s) const; + // payoff of the pure profile s: a direct lookup, no floating-point computation involved. + // V=double looks up PayoffTable; V=Rational looks up ExactPayoffTable and casts the stored + // Number to Rational (see ExactPayoffTable's comment for why the table itself stays + // Number-valued). V is never deducible from s (a plain vector of action indices, not + // parametrized by V), so callers must specify it explicitly, e.g. getPurePayoff(...). + template V getPurePayoff(int player, const std::vector &s) const; bool isSymmetric() const { @@ -168,10 +172,10 @@ class AGG { const std::vector &getActionSet(int player) { return actionSets.at(player); } const PayoffTable &getPayoffMap(int node) { return payoffs.at(node); } - double getMaxPayoff() const; - double getMinPayoff() const; - Number getExactMaxPayoff() const; - Number getExactMinPayoff() const; + // largest/smallest payoff in any pure profile. Same V dispatch as getPurePayoff above; likewise + // never deducible (no arguments at all), so callers write getMaxPayoff(), etc. + template V getMaxPayoff() const; + template V getMinPayoff() const; void printPayoffs(std::ostream &s, int node) const { @@ -293,7 +297,7 @@ class AGG { std::string word; in >> word; const Number num(word); - p->second = (double)num; + p->second = static_cast(num); exact.insert(std::make_pair(p->first, num)); } std::istream ∈ diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index fcce093d3c..dfb2970c4e 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -106,7 +106,7 @@ std::shared_ptr BAGG::makeBAGG(istream &in) throw std::runtime_error("Error in game file: number expected for type distribution"); } const Number num(word); - TDist[i][j] = (double)num; + TDist[i][j] = static_cast(num); exactTDist[i].push_back(num); } } @@ -230,7 +230,7 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int const int aact = typeAction2ActionIndex[pl][t][act]; if constexpr (std::is_same_v) { as[aact + aggPtr->firstAction(pl)] += - (Rational)exactIndepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + static_cast(exactIndepTypeDist[pl][t]) * s[act + firstAction(pl, t)]; } else { as[aact + aggPtr->firstAction(pl)] += @@ -285,7 +285,7 @@ double BAGG::getPurePayoff(int player, int tp, std::vector &ps) // independent, per exactIndepTypeDist), not their actions -- so the expectation is a small, // exact, finite sum over the Cartesian product of the other players' types (bounded by the // product of their type counts, not their action counts), each term an exact AGG-level pure -// payoff via AGG::getExactPurePayoff. +// payoff via AGG::getPurePayoff. Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps) const { std::vector others; @@ -306,9 +306,9 @@ Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps const int pl = others[k]; const int t = typeIndex[k]; sAGG[pl] = typeAction2ActionIndex[pl][t][ps[typeOffset[pl] + t]]; - weight *= (Rational)exactIndepTypeDist[pl][t]; + weight *= static_cast(exactIndepTypeDist[pl][t]); } - total += weight * (Rational)aggPtr->getExactPurePayoff(player, sAGG); + total += weight * aggPtr->getPurePayoff(player, sAGG); size_t k = 0; for (; k < others.size(); ++k) { diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index a7a86dfab6..3dd426ba6e 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -63,10 +63,8 @@ class BAGG { int firstAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t]; } int lastAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t + 1]; } - double getMaxPayoff() const { return aggPtr->getMaxPayoff(); } - double getMinPayoff() const { return aggPtr->getMinPayoff(); } - Number getExactMaxPayoff() const { return aggPtr->getExactMaxPayoff(); } - Number getExactMinPayoff() const { return aggPtr->getExactMinPayoff(); } + template V getMaxPayoff() const { return aggPtr->getMaxPayoff(); } + template V getMinPayoff() const { return aggPtr->getMinPayoff(); } // exp. payoff under mixed strat profile, summed over player's types. No Rational counterpart -- // unused outside this class; getPurePayoff and GameBAGGRep only ever go through the per-type diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 1ed79d1e28..7a787a59a2 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -56,7 +56,7 @@ Rational AGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const for (int i = 1; i <= aggPtr->getNumPlayers(); i++) { s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } - return (Rational)aggPtr->getExactPurePayoff(p_player->GetNumber() - 1, s); + return aggPtr->getPurePayoff(p_player->GetNumber() - 1, s); } Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -68,7 +68,7 @@ Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strat s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } s[player - 1] = p_strategy->GetNumber() - 1; - return (Rational)aggPtr->getExactPurePayoff(player - 1, s); + return aggPtr->getPurePayoff(player - 1, s); } //======================================================================== diff --git a/src/games/gameagg.h b/src/games/gameagg.h index af6ecf1e88..fed423d672 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -83,11 +83,11 @@ class GameAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return (Rational)aggPtr->getExactMinPayoff(); } + Rational GetMinPayoff() const override { return aggPtr->getMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return (Rational)aggPtr->getExactMaxPayoff(); } + Rational GetMaxPayoff() const override { return aggPtr->getMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 6eed97b286..2cb29d677b 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -90,11 +90,11 @@ class GameBAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return (Rational)baggPtr->getExactMinPayoff(); } + Rational GetMinPayoff() const override { return baggPtr->getMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return (Rational)baggPtr->getExactMaxPayoff(); } + Rational GetMaxPayoff() const override { return baggPtr->getMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/solvers/gtracer/aggame.h b/src/solvers/gtracer/aggame.h index 246523ee06..b8bf4dbfbd 100644 --- a/src/solvers/gtracer/aggame.h +++ b/src/solvers/gtracer/aggame.h @@ -73,7 +73,7 @@ class aggame : public gnmgame { } double getPurePayoff(int player, const std::vector &s) const override { - return aggPtr->getPurePayoff(player, s); + return aggPtr->getPurePayoff(player, s); } void setPurePayoff(int player, const std::vector &s, double value) override From c9efb1a3a468947da98d9cc6c5d093358daeefc0 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 19:47:20 -0400 Subject: [PATCH 08/14] More type name cleanup --- src/games/agg/agg.cc | 177 +++++++++++++--------------------- src/games/agg/agg.h | 130 +++++++++++++++---------- src/games/agg/bagg.h | 4 +- src/solvers/gtracer/aggame.cc | 87 +++++++++-------- 4 files changed, 194 insertions(+), 204 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index a0df7771f2..42776a2e4d 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -38,14 +38,13 @@ AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNod vector> &_actionSets, vector> &neighb, vector &projTypes, vector>> &projS, vector>> &proj, vector> &projF, - vector>> &Po, vector> &P, - vector &_payoffs, vector &_exactPayoffs) + vector>> &Po, vector &_payoffs, + vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), actionSets(_actionSets), neighbors(neighb), projectionTypes(projTypes), payoffs(_payoffs), - exactPayoffs(_exactPayoffs), projection(proj), projectedStrat(projS), - exactProjectedStrat(numANodes, vector>(numPlayers)), - exactPr(numPlayers), fullProjectedStrat(projS), projFunctions(projF), Porder(Po), Pr(P), - isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), + exactPayoffs(_exactPayoffs), projection(proj), m_state(numANodes, numPlayers), + m_exactState(numANodes, numPlayers), fullProjectedStrat(projS), projFunctions(projF), + Porder(Po), isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), cache(numPlayers + 1), player2Class(numPlayers), kSymStrategyOffset(1, 0) { @@ -294,7 +293,7 @@ std::shared_ptr AGG::makeAGG(istream &in) throw std::runtime_error("Unknown payoff type " + std::to_string(t)); } } - return std::make_shared(n, size, S, P, ASets, neighb, projTypes, projS, proj, projF, Po, Pr, + return std::make_shared(n, size, S, P, ASets, neighb, projTypes, projS, proj, projF, Po, pays, exactPays); } @@ -393,99 +392,58 @@ void AGG::initPorder(vector &Po, int i, int N, vector(), differs by V. +template void AGG::computeP(int player, int act, int player2, int act2) { + auto &s = state(); // apply player's strat - Pr[0].reset(); - Pr[0].insert(make_pair(projection[actionSets[player][act]][player][act], 1.0)); + s.Pr[0].reset(); + s.Pr[0].insert(make_pair(projection[actionSets[player][act]][player][act], V(1))); const int numNei = neighbors[actionSets[player][act]].size(); // apply others' strat for (int k = 1; k < numPlayers; k++) { - Pr[k].reset(); + s.Pr[k].reset(); if (Porder[player][act][k] == player2) { if (act2 == -1) { - // Pr[k].swap(Pr[k-1]); - Pr[k] = Pr[k - 1]; + s.Pr[k] = s.Pr[k - 1]; } else { // apply player2's pure strat - ConfigDistribution temp; - temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], 1.0)); - Pr[k].multiply(Pr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); + ConfigDistribution temp; + temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], V(1))); + s.Pr[k].multiply(s.Pr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); } } else { - Pr[k].multiply(Pr[k - 1], projectedStrat[actionSets[player][act]][Porder[player][act][k]], - numNei, projFunctions[actionSets[player][act]]); + s.Pr[k].multiply(s.Pr[k - 1], + s.projectedStrat[actionSets[player][act]][Porder[player][act][k]], numNei, + projFunctions[actionSets[player][act]]); } } } -void AGG::doProjection(int Node, double *s) +template void AGG::doProjection(int Node, V *s) { for (int i = 0; i < numPlayers; i++) { doProjection(Node, i, &(s[firstAction(i)])); } } -void AGG::doProjection(int Node, int i, double *s) +template void AGG::doProjection(int Node, int i, V *s) { - projectedStrat[Node][i].reset(); - for (int j = 0; j < actions[i]; j++) { - if (s[j] > (double)0.0) { - projectedStrat[Node][i] += make_pair(projection[Node][i][j], s[j]); - } - } -} - -// Exact (Rational) counterparts of computeP/doProjection above. The action graph structure -// (projection, Porder, projFunctions, actionSets, neighbors) is purely combinatorial -- built -// once from integer data, independent of payoff/probability values -- so it's shared as-is; -// only the numeric working state (Pr/projectedStrat) needs a separate Rational-valued copy. -void AGG::computeExactP(int player, int act, int player2, int act2) -{ - exactPr[0].reset(); - exactPr[0].insert(make_pair(projection[actionSets[player][act]][player][act], Rational(1))); - - const int numNei = neighbors[actionSets[player][act]].size(); - for (int k = 1; k < numPlayers; k++) { - exactPr[k].reset(); - if (Porder[player][act][k] == player2) { - if (act2 == -1) { - exactPr[k] = exactPr[k - 1]; - } - else { - ConfigDistribution temp; - temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], Rational(1))); - exactPr[k].multiply(exactPr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); - } - } - else { - exactPr[k].multiply(exactPr[k - 1], - exactProjectedStrat[actionSets[player][act]][Porder[player][act][k]], - numNei, projFunctions[actionSets[player][act]]); - } - } -} - -void AGG::doExactProjection(int Node, Rational *s) -{ - for (int i = 0; i < numPlayers; i++) { - doExactProjection(Node, i, &(s[firstAction(i)])); - } + state().project(Node, i, projection[Node][i], actions[i], s); } -void AGG::doExactProjection(int Node, int i, Rational *s) -{ - exactProjectedStrat[Node][i].reset(); - for (int j = 0; j < actions[i]; j++) { - if (s[j] > Rational(0)) { - exactProjectedStrat[Node][i] += make_pair(projection[Node][i][j], s[j]); - } - } -} +template void AGG::computeP(int player, int act, int player2, int act2); +template void AGG::computeP(int player, int act, int player2, int act2); +template void AGG::doProjection(int Node, double *s); +template void AGG::doProjection(int Node, Rational *s); +template void AGG::doProjection(int Node, int i, double *s); +template void AGG::doProjection(int Node, int i, Rational *s); template V AGG::getPurePayoff(int player, const std::vector &s) const { @@ -498,27 +456,25 @@ template V AGG::getPurePayoff(int player, const std::vector &s) c pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); } } + const auto throwNotFound = [&]() -> void { + std::stringstream str; + str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; + str << "["; + copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); + str << "] in payoffs of action node #" << Node; + throw std::runtime_error(str.str()); + }; if constexpr (std::is_same_v) { auto p = exactPayoffs[Node].find(pureprofile); if (p == exactPayoffs[Node].end()) { - std::stringstream str; - str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; - str << "["; - copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); - str << "] in payoffs of action node #" << Node; - throw std::runtime_error(str.str()); + throwNotFound(); } return static_cast(p->second); } else { auto p = payoffs[Node].find(pureprofile); if (p == payoffs[Node].end()) { - std::stringstream str; - str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; - str << "["; - copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); - str << "] in payoffs of action node #" << Node; - throw std::runtime_error(str.str()); + throwNotFound(); } return p->second; } @@ -539,36 +495,35 @@ Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) return result; } -template V AGG::getV(int player, int act, const StrategyProfile &s) +template V AGG::payoffInnerProd(int node, const ConfigDistribution &dist) { if constexpr (std::is_same_v) { - doExactProjection(actionSets.at(player).at(act), s); - computeExactP(player, act); - return exactInnerProd(actionSets[player][act], exactPr[numPlayers - 1]); + return exactInnerProd(node, dist); } else { - // project s to the projectedStrat - doProjection(actionSets.at(player).at(act), s); - computeP(player, act); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); + return dist.inner_prod(payoffs[node]); } } +template V AGG::getV(int player, int act, const StrategyProfile &s) +{ + doProjection(actionSets.at(player).at(act), s); + computeP(player, act); + return payoffInnerProd(actionSets[player][act], state().Pr[numPlayers - 1]); +} + template V AGG::getJ(int player1, int act1, int player2, int act2, const StrategyProfile &s) { - if constexpr (std::is_same_v) { - doExactProjection(actionSets[player1][act1], s); - computeExactP(player1, act1, player2, act2); - return exactInnerProd(actionSets[player1][act1], exactPr[numPlayers - 1]); - } - else { - doProjection(actionSets[player1][act1], s); - computeP(player1, act1, player2, act2); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); - } + doProjection(actionSets[player1][act1], s); + computeP(player1, act1, player2, act2); + return payoffInnerProd(actionSets[player1][act1], state().Pr[numPlayers - 1]); } +template double AGG::payoffInnerProd(int node, const ConfigDistribution &dist); +template Rational AGG::payoffInnerProd(int node, + const ConfigDistribution &dist); + template V AGG::getMixedPayoff(int player, const StrategyProfile &s) { V result(0); @@ -636,10 +591,11 @@ double AGG::getSymMixedPayoff(int node, StrategyProfile &s) doProjection(node, 0, s); assert(numPlayers > 1); // ConfigDistribution *dest; - // projectedStrat[node][0].power(numPlayers-1, dest, Pr, numNei,projFunctions[node]); - ConfigDistribution &dest = Pr[numPlayers - 1]; - projectedStrat[node][0].power(numPlayers - 1, dest, Pr[numPlayers - 2], numNei, - projFunctions[node]); + // m_state.projectedStrat[node][0].power(numPlayers-1, dest, m_state.Pr, + // numNei,projFunctions[node]); + ConfigDistribution &dest = m_state.Pr[numPlayers - 1]; + m_state.projectedStrat[node][0].power(numPlayers - 1, dest, m_state.Pr[numPlayers - 2], numNei, + projFunctions[node]); return dest.inner_prod(projection[node][0][node], numNei, projFunctions[node], payoffs[node]); } @@ -719,14 +675,15 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlCla if (!isPure[node]) { const int player = playerClasses[plClass].at(0); - projectedStrat[node][player].reset(); + m_state.projectedStrat[node][player].reset(); if (numPl > 0) { for (int j = 0; j < actions[player]; j++) { if (s[j] > (double)0.0) { - projectedStrat[node][player] += make_pair(projection[node][player][j], s[j]); + m_state.projectedStrat[node][player] += make_pair(projection[node][player][j], s[j]); } } - projectedStrat[node][player].power(numPl, dest, Pr[0], numNei, projFunctions[node]); + m_state.projectedStrat[node][player].power(numPl, dest, m_state.Pr[0], numNei, + projFunctions[node]); } if (plClass == ownPlClass) { ConfigDistribution temp; diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index a7f8c745cf..59f6a96264 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -28,6 +28,7 @@ #include #include #include +#include #include "proj_func.h" #include "trie_map.h" #include "games/number.h" @@ -69,6 +70,41 @@ using ExactPayoffTable = std::map, Number>; // provides -- letting both instantiations reuse trie_map directly. template using ConfigDistribution = trie_map; +// AGG's mutable working state for the convolution algorithm, at numeric type V. AGG holds one +// instance per V (see AGG::state()) rather than a single instance of one or the other, since both +// may be in use simultaneously -- e.g. from different MixedStrategyProfile instantiations over +// the same AGG -- and each is rebuilt fresh per call regardless of its previous contents. +template struct ConvolutionState { + // foreach s \in S, foreach i \in N, the projected mixed strat: a prob distribution over the + // set of 'contributions' (see AGG::projection). + std::vector>> projectedStrat; + + // when computing the induced distribution via AGG::computeP(): foreach k <= n-1, the prob. + // distribution P_k induced by the partial strat profile of agents o_1..o_k. Also reused, when + // computing the partial distributions for the payoff jacobian, as the partial distribution + // induced by all agents except j. + std::vector> Pr; + + ConvolutionState(int numActionNodes, int numPlayers) + : projectedStrat(numActionNodes, std::vector>(numPlayers)), + Pr(numPlayers) + { + } + + // reset projectedStrat[Node][player] to the weighted sum, over actions j with s[j] > 0, of + // contributions[j] weighted by s[j]. + void project(int Node, int player, const std::vector> &contributions, + int numActions, const V *s) + { + projectedStrat[Node][player].reset(); + for (int j = 0; j < numActions; j++) { + if (s[j] > V(0)) { + projectedStrat[Node][player] += std::make_pair(contributions[j], s[j]); + } + } + } +}; + // types of input formats for payoff func using PayoffType = enum { COMPLETE, MAPPING, ADDITIVE }; @@ -88,15 +124,16 @@ class AGG { // read an AGG from input stream static std::shared_ptr makeAGG(std::istream &in); - // constructor + // constructor. Note projS is used only for fullProjectedStrat below -- the convolution + // engine's own working state (ConvolutionState) is sized from numANodes/numPlayers directly, + // not seeded from a caller-supplied array the way it used to be. AGG(int numPlayers, std::vector &actions, int numANodes, int numPNodes, std::vector> &actionSets, std::vector> &neighbors, std::vector &projTypes, std::vector>> &projS, std::vector>> &proj, std::vector> &projF, std::vector>> &Po, - std::vector> &P, std::vector &payoffs, - std::vector &exactPayoffs); + std::vector &payoffs, std::vector &exactPayoffs); ~AGG() = default; @@ -116,10 +153,9 @@ class AGG { int firstKSymAction(int i) const { return kSymStrategyOffset[i]; } int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } - // exp. payoff under mixed strat profile, via the convolution algorithm. V=double runs the - // floating-point engine; V=Rational runs the exact-arithmetic counterpart throughout - // (doProjection/computeP/inner_prod vs doExactProjection/computeExactP/exactInnerProd) -- - // dispatched at compile time in the .cc, not overloaded, since it's the same algorithm. + // exp. payoff under mixed strat profile, via the convolution algorithm (doProjection/computeP, + // templated on V; inner_prod is trie_map's own method for V=double, exactInnerProd for + // V=Rational -- see exactInnerProd's comment for why that one isn't merged too). template V getMixedPayoff(int player, const StrategyProfile &s); void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); template V getV(int player, int action, const StrategyProfile &s); @@ -235,18 +271,15 @@ class AGG { // the 'contribution' of s_i to D^(s) std::vector>> projection; - // foreach s \in S, foreach i \in N, the projected mixed strat - // which is a prob distribution over the set of 'contributions' - std::vector>> projectedStrat; + // working state for the convolution algorithm (see ConvolutionState above), one instance per + // numeric type V, since both may be in use simultaneously. + ConvolutionState m_state; + ConvolutionState m_exactState; - // exact (Rational) counterpart of projectedStrat/Pr below, used only by the Rational - // instantiations of getMixedPayoff/getV/getJ. Independent working state from the double - // versions, since both may be in use (e.g. from different MixedStrategyProfile instantiations - // over the same AGG) and each is rebuilt fresh per call regardless. - std::vector>> exactProjectedStrat; - std::vector> exactPr; - - // foreach s in S, i in N, the full set of projected actions. + // foreach s in S, i in N, the full set of projected actions. Unlike m_state/m_exactState + // above, this is filled once (from the constructor's projS argument) and never rebuilt -- + // read directly by gametracer::aggame (a friend) for its own jacobian bookkeeping, not used by + // AGG's own convolution algorithm. std::vector>> fullProjectedStrat; // foreach s in S, foreach neighbor of s, its projection function @@ -256,15 +289,6 @@ class AGG { // in which we apply the DP algorithm std::vector>> Porder; - // when computing the induced distribution via ComputeP(): - // foreach k<= n-1, - // prob. distrib P_k induced by the partial strat profile of agents o_1..o_k - - // when computing the partial distributions for the payoff jacobian: - // foreach j \in N, - // the partial distribution induced by all agents except j. - std::vector> Pr; - // foreach s in S, whether s's neighbors are all action nodes std::vector isPure; @@ -325,35 +349,41 @@ class AGG { static void initPorder(std::vector &Po, int i, int N, std::vector> &projS); - // private methods: - void computeP(int player, int act, int player2 = -1, int act2 = -1); - void doProjection(int Node, const StrategyProfile &s) - { - doProjection(Node, &(const_cast &>(s)[0])); - } - void doProjection(int Node, int player, const StrategyProfile &s) + // private methods. computeP/doProjection are the same algorithm for both V; they're made + // non-branching by going through state() for the V-specific working arrays (m_state vs + // m_exactState), rather than an if constexpr per call. + template ConvolutionState &state() { - doProjection(Node, player, &(const_cast &>(s)[firstAction(player)])); + if constexpr (std::is_same_v) { + return m_exactState; + } + else { + return m_state; + } } - void doProjection(int Node, double *s); - void doProjection(int Node, int player, double *s); - - // exact (Rational) counterparts, mirroring the above precisely. exactPayoffs is a - // std::map<..., Number>, not a trie_map, so trie_map's own inner_prod can't be - // used directly against it -- this does the equivalent sum manually. - Rational exactInnerProd(int node, const ConfigDistribution &dist) const; - void computeExactP(int player, int act, int player2 = -1, int act2 = -1); - void doExactProjection(int Node, const StrategyProfile &s) + template void computeP(int player, int act, int player2 = -1, int act2 = -1); + template void doProjection(int Node, const StrategyProfile &s) { - doExactProjection(Node, &(const_cast &>(s)[0])); + doProjection(Node, &(const_cast &>(s)[0])); } - void doExactProjection(int Node, int player, const StrategyProfile &s) + template void doProjection(int Node, int player, const StrategyProfile &s) { - doExactProjection(Node, player, - &(const_cast &>(s)[firstAction(player)])); + doProjection(Node, player, &(const_cast &>(s)[firstAction(player)])); } - void doExactProjection(int Node, Rational *s); - void doExactProjection(int Node, int player, Rational *s); + template void doProjection(int Node, V *s); + template void doProjection(int Node, int player, V *s); + + // exactPayoffs is a std::map<..., Number>, not a trie_map, so trie_map's own + // inner_prod can't be used directly against it (unlike the double path, which calls + // dist.inner_prod(payoffs[node]) directly) -- this does the equivalent sum manually. + Rational exactInnerProd(int node, const ConfigDistribution &dist) const; + + // dispatch point used by getV/getJ: V=double calls trie_map's own inner_prod against + // PayoffTable, V=Rational calls exactInnerProd against ExactPayoffTable. Not merged further + // than this one function -- same reason PayoffTable/ExactPayoffTable themselves aren't a + // template pair (see their comments): the two sides genuinely differ in container and value + // type, not just in V. + template V payoffInnerProd(int node, const ConfigDistribution &dist); void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, ConfigDistribution &dest, int plClass2 = -1, int act2 = -1); diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 3dd426ba6e..5f8064301d 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -38,8 +38,8 @@ using ProbDist = std::vector; class BAGG { public: - friend class Gambit::BAGGPureStrategyProfileRep; - template friend class Gambit::BAGGMixedStrategyProfileRep; + friend class BAGGPureStrategyProfileRep; + template friend class BAGGMixedStrategyProfileRep; friend std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/solvers/gtracer/aggame.cc b/src/solvers/gtracer/aggame.cc index e1996ad64c..0ef1f43a14 100644 --- a/src/solvers/gtracer/aggame.cc +++ b/src/solvers/gtracer/aggame.cc @@ -35,21 +35,21 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t std::vector strat(numNei); agg::AGG::Config a(numNei, 0); // compute the full distrib - aggPtr->computeP(player1, act1); + aggPtr->computeP(player1, act1); // store the full distrib in Pr[player1] - aggPtr->Pr[player1].swap(aggPtr->Pr[numPlayers - 1]); + aggPtr->m_state.Pr[player1].swap(aggPtr->m_state.Pr[numPlayers - 1]); for (i = 0; i < (int)tasks.size(); i++) { // assert(tasks[i]!=player1); - agg::ConfigDistribution &P = aggPtr->Pr[tasks[i]]; + agg::ConfigDistribution &P = aggPtr->m_state.Pr[tasks[i]]; // P.clear(); // to get ready for division, we need clear() - P = aggPtr->Pr[player1]; + P = aggPtr->m_state.Pr[player1]; bool NullOnly = true; for (j = 0; j < numNei; ++j) { a[j]++; - auto pp = aggPtr->projectedStrat[Node][tasks[i]].find(a); - if (pp == aggPtr->projectedStrat[Node][tasks[i]].end()) { + auto pp = aggPtr->m_state.projectedStrat[Node][tasks[i]].find(a); + if (pp == aggPtr->m_state.projectedStrat[Node][tasks[i]].end()) { strat[j] = 0; } else { @@ -88,7 +88,7 @@ void aggame::computePartialP_bisect(int player1, int act1, std::vector::ite << endl; #endif if (endp - start == 1) { - aggPtr->Pr[*start].reset(); + aggPtr->m_state.Pr[*start].reset(); return; } const int Node = aggPtr->actionSets[player1][act1]; @@ -103,37 +103,39 @@ void aggame::computePartialP_bisect(int player1, int act1, std::vector::ite computePartialP_bisect(player1, act1, mid, endp, temp); temp.reset(); - temp = aggPtr->projectedStrat[Node][*start]; + temp = aggPtr->m_state.projectedStrat[Node][*start]; if (mid - start > 1) { - temp.multiply(aggPtr->Pr[*start], numNei, aggPtr->projFunctions[Node]); + temp.multiply(aggPtr->m_state.Pr[*start], numNei, aggPtr->projFunctions[Node]); } if (mid - start == 1) { - // assert(aggPtr->Pr[*start].empty()); - aggPtr->Pr[*start] = aggPtr->projectedStrat[Node][*mid]; + // assert(aggPtr->m_state.Pr[*start].empty()); + aggPtr->m_state.Pr[*start] = aggPtr->m_state.projectedStrat[Node][*mid]; if (endp - mid > 1) { - aggPtr->Pr[*start].multiply(aggPtr->Pr[*mid], numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[*start].multiply(aggPtr->m_state.Pr[*mid], numNei, + aggPtr->projFunctions[Node]); } } else { for (ptr = start; ptr != mid; ++ptr) { player2 = *ptr; - aggPtr->Pr[player2].multiply(aggPtr->projectedStrat[Node][*mid], numNei, - aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(aggPtr->m_state.projectedStrat[Node][*mid], numNei, + aggPtr->projFunctions[Node]); if (endp - mid > 1) { - aggPtr->Pr[player2].multiply(aggPtr->Pr[*mid], numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(aggPtr->m_state.Pr[*mid], numNei, + aggPtr->projFunctions[Node]); } } } if (endp - mid == 1) { - // assert(aggPtr->Pr[*mid].empty()); - aggPtr->Pr[*mid] = temp; + // assert(aggPtr->m_state.Pr[*mid].empty()); + aggPtr->m_state.Pr[*mid] = temp; } else { for (ptr = mid; ptr != endp; ++ptr) { player2 = *ptr; - aggPtr->Pr[player2].multiply(temp, numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(temp, numNei, aggPtr->projFunctions[Node]); } } } @@ -244,40 +246,41 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const computePartialP_PureNode(rown, act1, tasks); } else { // do bisection - computePartialP_bisect(rown, act1, tasks.begin(), tasks.end(), aggPtr->Pr[rown]); + computePartialP_bisect(rown, act1, tasks.begin(), tasks.end(), aggPtr->m_state.Pr[rown]); #ifdef AGGDEBUG cout << "after calling computePartialP_bisect:" << endl; for (int tt = 0; tt < tasks.size(); tt++) { cout << "for player " << tasks[tt] << endl; - cout << aggPtr->Pr[tasks[tt]] << endl; + cout << aggPtr->m_state.Pr[tasks[tt]] << endl; } #endif // now apply rown's action (act1), and the strategies of // players in nontasks - aggPtr->Pr[rown].reset(); - aggPtr->Pr[rown].insert(make_pair(aggPtr->projection[currNode][rown][act1], 1.0)); + aggPtr->m_state.Pr[rown].reset(); + aggPtr->m_state.Pr[rown].insert(make_pair(aggPtr->projection[currNode][rown][act1], 1.0)); for (p = nontasks.begin(); p != nontasks.end(); ++p) { - aggPtr->Pr[rown].multiply(aggPtr->projectedStrat[currNode][*p], numNei, - aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[rown].multiply(aggPtr->m_state.projectedStrat[currNode][*p], numNei, + aggPtr->projFunctions[currNode]); } #ifdef AGGDEBUG cout << "the polynomial product of strats of player " << rown << " and players in the vector nontasks is:" << endl; - cout << aggPtr->Pr[rown] << endl; + cout << aggPtr->m_state.Pr[rown] << endl; #endif if (tasks.size() == 1) { - aggPtr->Pr[tasks[0]] = aggPtr->Pr[rown]; + aggPtr->m_state.Pr[tasks[0]] = aggPtr->m_state.Pr[rown]; } else { for (p = tasks.begin(); p != tasks.end(); ++p) { - if (aggPtr->Pr[*p].empty()) { + if (aggPtr->m_state.Pr[*p].empty()) { std::cerr << "AGG::payoffMatrix() ERROR for rown=" << rown << " act1=" << act1 << " *p=" << *p << ": the distribution should not be empty!" << std::endl; #ifdef AGGDEBUG std::cerr << "strategy profile is: " << s << std::endl; #endif } - aggPtr->Pr[*p].multiply(aggPtr->Pr[rown], numNei, aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[*p].multiply(aggPtr->m_state.Pr[rown], numNei, + aggPtr->projFunctions[currNode]); } // end for(p=tasks.begin... } @@ -286,17 +289,17 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const // we store this distrib in Pr[rown][act1][rown] if (!spares.empty()) { // assert(tasks.size()>0); - aggPtr->Pr[rown].reset(); - aggPtr->Pr[rown].multiply(aggPtr->Pr[tasks[0]], - aggPtr->projectedStrat[currNode][tasks[0]], numNei, - aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[rown].reset(); + aggPtr->m_state.Pr[rown].multiply(aggPtr->m_state.Pr[tasks[0]], + aggPtr->m_state.projectedStrat[currNode][tasks[0]], + numNei, aggPtr->projFunctions[currNode]); } } // end else #ifdef AGGDEBUG cout << "after computing parital distributions, the distributions are" << endl; for (int tt = 0; tt < numPlayers; tt++) { cout << "for player " << tt << endl; - cout << aggPtr->Pr[tt]; + cout << aggPtr->m_state.Pr[tt]; cout << endl; } #endif @@ -315,8 +318,8 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const for (p = tasks.begin(); p != tasks.end(); ++p) { for (act2 = 0; act2 < aggPtr->actions[*p]; act2++) { // act2: col action - if (aggPtr->projectedStrat[currNode][*p].size() == 1 && - aggPtr->projectedStrat[currNode][*p].begin()->first == + if (aggPtr->m_state.projectedStrat[currNode][*p].size() == 1 && + aggPtr->m_state.projectedStrat[currNode][*p].begin()->first == aggPtr->projection[currNode][*p][act2]) { computeUndisturbedPayoff(undisturbedPayoff, hasUndisturbed, rown, act1, *p); savePayoff(dest, rown, act1, *p, act2, undisturbedPayoff, aggPtr->cache); @@ -337,13 +340,13 @@ void aggame::computeUndisturbedPayoff(double &undisturbedPayoff, bool &has, int const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); if (player2 == player1) { - undisturbedPayoff = aggPtr->Pr[player2].inner_prod(aggPtr->payoffs[Node]); + undisturbedPayoff = aggPtr->m_state.Pr[player2].inner_prod(aggPtr->payoffs[Node]); } else { - // assert(aggPtr->projectedStrat[Node][player2].size()==1); - undisturbedPayoff = - aggPtr->Pr[player2].inner_prod(aggPtr->projectedStrat[Node][player2].begin()->first, - numNei, aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); + // assert(aggPtr->m_state.projectedStrat[Node][player2].size()==1); + undisturbedPayoff = aggPtr->m_state.Pr[player2].inner_prod( + aggPtr->m_state.projectedStrat[Node][player2].begin()->first, numNei, + aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); } has = true; } @@ -393,8 +396,8 @@ void aggame::computePayoff(cmatrix &dest, int player1, int act1, int player2, in } else { r.first->second = - aggPtr->Pr[player2].inner_prod(aggPtr->projection[Node][player2][act2], numNei, - aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); + aggPtr->m_state.Pr[player2].inner_prod(aggPtr->projection[Node][player2][act2], numNei, + aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); savePayoff(dest, player1, act1, player2, act2, r.first->second, cache, r.second); } } From 7060a8f6b39b147b10af2441f4150b8632353371 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 20:24:55 -0400 Subject: [PATCH 09/14] clang-tidy fixes --- src/games/agg/agg.cc | 2 +- src/games/agg/bagg.h | 4 ++-- src/solvers/gtracer/aggame.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 42776a2e4d..5ae8e83f15 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -919,7 +919,7 @@ void AGG::makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable throw std::runtime_error(str.str()); } const Number num(word); - const double u = static_cast(num); + const auto u = static_cast(num); // insert const pair::iterator, bool> r = pay.insert(make_pair(key, u)); diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 5f8064301d..3dd426ba6e 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -38,8 +38,8 @@ using ProbDist = std::vector; class BAGG { public: - friend class BAGGPureStrategyProfileRep; - template friend class BAGGMixedStrategyProfileRep; + friend class Gambit::BAGGPureStrategyProfileRep; + template friend class Gambit::BAGGMixedStrategyProfileRep; friend std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/solvers/gtracer/aggame.h b/src/solvers/gtracer/aggame.h index b8bf4dbfbd..d1b4b8346d 100644 --- a/src/solvers/gtracer/aggame.h +++ b/src/solvers/gtracer/aggame.h @@ -44,8 +44,8 @@ class aggame : public gnmgame { double getMixedPayoff(int player, const cvector &s) const override { - std::vector sp(s.values(), s.values() + s.getm()); - return (double)aggPtr->getMixedPayoff(player, sp); + const std::vector sp(s.values(), s.values() + s.getm()); + return aggPtr->getMixedPayoff(player, sp); } double getKSymMixedPayoff(int cls, cvector &s) From a7fe8470dda502fec9f9aa5f0052d4929ff8a5e8 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 5 Aug 2026 20:33:08 -0400 Subject: [PATCH 10/14] Add forgotten test games --- tests/test_games/2x2_fraction_payoffs.agg | 33 +++++++++++++++++++++++ tests/test_games/2x2_fraction_types.bagg | 21 +++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/test_games/2x2_fraction_payoffs.agg create mode 100644 tests/test_games/2x2_fraction_types.bagg diff --git a/tests/test_games/2x2_fraction_payoffs.agg b/tests/test_games/2x2_fraction_payoffs.agg new file mode 100644 index 0000000000..b2324eff9e --- /dev/null +++ b/tests/test_games/2x2_fraction_payoffs.agg @@ -0,0 +1,33 @@ +#AGG +#number of players: +2 +#number of action nodes: +2 +#number of func nodes: +0 + +#sizes of action sets: +2 2 + +#action sets: +0 1 +0 1 + + +#the action graph: +2 0 1 +2 1 0 + +#the types of func nodes: +#0: sum +#1: existence +#2: highest +#3: lowest + + +#the payoffs: +#now the payoff values: one row per action node. +#For each row: first, the type of the payoff format +#Then payoffs are given in lexicographical order of the input configurations +0 1/3 -4 +0 -10 0.123456789012345 diff --git a/tests/test_games/2x2_fraction_types.bagg b/tests/test_games/2x2_fraction_types.bagg new file mode 100644 index 0000000000..630b583ea1 --- /dev/null +++ b/tests/test_games/2x2_fraction_types.bagg @@ -0,0 +1,21 @@ +#BAGG +# Player 1 has two types with fractional probabilities (1/3, 2/3); player 2 has one type. +# Wraps the same 2x2 payoff structure as 2x2.agg, used to test that BAGG's type-distribution +# parsing (and the exact weighted-sum computation over it) handles fraction literals exactly. +2 +2 +0 +2 1 +1/3 2/3 +1 +2 +2 +2 +0 1 +0 1 +0 1 +2 0 1 +2 1 0 + +0 35 -4 +0 -10 95 From ad35911fad280de127c51c27d6d97574906816d6 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 14 Aug 2026 19:06:46 +0100 Subject: [PATCH 11/14] Fix AGG::getMaxPayoff/getMinPayoff skipping node 0's own entries Both loops started at node 1 after seeding `result` from only the first entry of node 0's payoff map, so any larger/smaller value elsewhere in that same map was never considered. Scan node 0 in full like every other node. --- src/games/agg/agg.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 5ae8e83f15..72f2914be7 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -955,7 +955,7 @@ template V AGG::getMaxPayoff() const assert(numActionNodes > 0); if constexpr (std::is_same_v) { Rational result = static_cast(exactPayoffs[0].begin()->second); - for (int i = 1; i < numActionNodes; i++) { + for (int i = 0; i < numActionNodes; i++) { for (const auto &it : exactPayoffs[i]) { result = max(result, static_cast(it.second)); } @@ -964,7 +964,7 @@ template V AGG::getMaxPayoff() const } else { double result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { + for (int i = 0; i < numActionNodes; i++) { for (const auto &it : payoffs[i]) { result = max(result, it.second); } @@ -978,7 +978,7 @@ template V AGG::getMinPayoff() const assert(numActionNodes > 0); if constexpr (std::is_same_v) { Rational result = static_cast(exactPayoffs[0].begin()->second); - for (int i = 1; i < numActionNodes; i++) { + for (int i = 0; i < numActionNodes; i++) { for (const auto &it : exactPayoffs[i]) { result = min(result, static_cast(it.second)); } @@ -987,7 +987,7 @@ template V AGG::getMinPayoff() const } else { double result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { + for (int i = 0; i < numActionNodes; i++) { for (const auto &it : payoffs[i]) { result = min(result, it.second); } From db363ae67f9773d9e297d302fe1e02058d4ff5e0 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 14 Aug 2026 19:13:35 +0100 Subject: [PATCH 12/14] Correct stale/incorrect comments and trim overly verbose ones in AGG/BAGG --- src/games/agg/agg.cc | 6 +-- src/games/agg/agg.h | 91 +++++++++++++------------------------------ src/games/agg/bagg.h | 7 +--- src/games/gameagg.cc | 4 -- src/games/gamebagg.cc | 4 -- tests/test_agg.py | 22 ++++------- tests/test_nash.py | 5 +-- 7 files changed, 41 insertions(+), 98 deletions(-) diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 72f2914be7..fd9f8028de 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -392,10 +392,8 @@ void AGG::initPorder(vector &Po, int i, int N, vector(), differs by V. +// compute the induced distribution; only the working state (state()) varies with V, the +// action graph structure itself is shared. template void AGG::computeP(int player, int act, int player2, int act2) { auto &s = state(); diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index 59f6a96264..8d0d42e65b 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -41,48 +41,31 @@ class aggame; namespace agg { -// data structure for a mixed strategy profile: one probability per action, using numeric type V -// (double for the floating-point engine, Rational for the exact-arithmetic counterpart). +// mixed strategy profile: one probability per action; V is double or Rational. template using StrategyProfile = std::vector; -// the local payoff function for one action node: maps a neighbor configuration to the payoff of -// playing that node under it. Populated at parse time; consumed by the floating-point -// convolution engine (getMixedPayoff/getV/getJ) via trie_map's own inner_prod, so it has to stay -// a trie_map -- unlike StrategyProfile/ConfigDistribution above, this is deliberately not -// a template (see ExactPayoffTable below for why a Rational instantiation would be unsound). +// payoff function for one action node (neighbor configuration -> payoff). Not a template like +// ConfigDistribution below: it's consumed via trie_map's own (floating-point) inner_prod. using PayoffTable = trie_map; -// exact (arbitrary-precision) counterpart of PayoffTable, populated alongside it at parse time -// from the same source text. Deliberately not a trie_map, for two independent reasons: -// (1) trie_map's own inner_prod has an epsilon-tolerance discard/throw policy -// (trie_map::THRESH = 1e-12) that is meaningful for double but unsound for exact arithmetic -- -// there is no notion of "too small to matter" once payoffs are exact; and (2) this stores Number, -// not Rational, to preserve the original entered text for exact serialization (printPayoffs), -// which a bare Rational cannot do. As a result this is only ever used for storage/lookup -// (find/insert/iterate) and a hand-written inner product (AGG::exactInnerProd), never trie_map's -// own arithmetic. +// exact counterpart of PayoffTable. A std::map, not a trie_map: it stores Number +// (not Rational) to preserve the original source text, and trie_map's inner_prod has an +// epsilon-discard threshold that is unsound for exact arithmetic. using ExactPayoffTable = std::map, Number>; -// data struct for a probability distribution over configurations, using numeric type V (double -// for the floating-point engine, Rational for the exact-arithmetic counterpart). Unlike -// PayoffTable/ExactPayoffTable above, this participates in the convolution algorithm -// (multiply/inner_prod), so V must support arithmetic -- which Rational, unlike Number, -// provides -- letting both instantiations reuse trie_map directly. +// probability distribution over configurations; V is double or Rational. Templated (unlike +// PayoffTable/ExactPayoffTable) because it participates in convolution arithmetic. template using ConfigDistribution = trie_map; -// AGG's mutable working state for the convolution algorithm, at numeric type V. AGG holds one -// instance per V (see AGG::state()) rather than a single instance of one or the other, since both -// may be in use simultaneously -- e.g. from different MixedStrategyProfile instantiations over -// the same AGG -- and each is rebuilt fresh per call regardless of its previous contents. +// AGG's mutable working state for the convolution algorithm. AGG keeps one instance per V (see +// AGG::state()) since double and Rational profiles may be in use simultaneously. template struct ConvolutionState { - // foreach s \in S, foreach i \in N, the projected mixed strat: a prob distribution over the - // set of 'contributions' (see AGG::projection). + // for each action node and player, the projected mixed strategy (a distribution over + // 'contributions' -- see AGG::projection). std::vector>> projectedStrat; - // when computing the induced distribution via AGG::computeP(): foreach k <= n-1, the prob. - // distribution P_k induced by the partial strat profile of agents o_1..o_k. Also reused, when - // computing the partial distributions for the payoff jacobian, as the partial distribution - // induced by all agents except j. + // used by AGG::computeP(): the distribution induced by a partial strategy profile, or (for + // the payoff jacobian) by all agents except one. std::vector> Pr; ConvolutionState(int numActionNodes, int numPlayers) @@ -91,8 +74,8 @@ template struct ConvolutionState { { } - // reset projectedStrat[Node][player] to the weighted sum, over actions j with s[j] > 0, of - // contributions[j] weighted by s[j]. + // set projectedStrat[Node][player] to the weighted sum of contributions[j] over actions with + // s[j] > 0. void project(int Node, int player, const std::vector> &contributions, int numActions, const V *s) { @@ -124,9 +107,8 @@ class AGG { // read an AGG from input stream static std::shared_ptr makeAGG(std::istream &in); - // constructor. Note projS is used only for fullProjectedStrat below -- the convolution - // engine's own working state (ConvolutionState) is sized from numANodes/numPlayers directly, - // not seeded from a caller-supplied array the way it used to be. + // constructor; projS seeds fullProjectedStrat only, not the convolution engine's own working + // state, which is sized independently. AGG(int numPlayers, std::vector &actions, int numANodes, int numPNodes, std::vector> &actionSets, std::vector> &neighbors, std::vector &projTypes, @@ -153,20 +135,15 @@ class AGG { int firstKSymAction(int i) const { return kSymStrategyOffset[i]; } int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } - // exp. payoff under mixed strat profile, via the convolution algorithm (doProjection/computeP, - // templated on V; inner_prod is trie_map's own method for V=double, exactInnerProd for - // V=Rational -- see exactInnerProd's comment for why that one isn't merged too). + // expected payoff under a mixed profile, via the convolution algorithm. template V getMixedPayoff(int player, const StrategyProfile &s); void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); template V getV(int player, int action, const StrategyProfile &s); template V getJ(int player, int action, int player2, int action2, const StrategyProfile &s); - // payoff of the pure profile s: a direct lookup, no floating-point computation involved. - // V=double looks up PayoffTable; V=Rational looks up ExactPayoffTable and casts the stored - // Number to Rational (see ExactPayoffTable's comment for why the table itself stays - // Number-valued). V is never deducible from s (a plain vector of action indices, not - // parametrized by V), so callers must specify it explicitly, e.g. getPurePayoff(...). + // payoff of the pure profile s: a direct table lookup. V is never deducible from s, so callers + // must specify it explicitly, e.g. getPurePayoff(...). template V getPurePayoff(int player, const std::vector &s) const; bool isSymmetric() const @@ -208,8 +185,7 @@ class AGG { const std::vector &getActionSet(int player) { return actionSets.at(player); } const PayoffTable &getPayoffMap(int node) { return payoffs.at(node); } - // largest/smallest payoff in any pure profile. Same V dispatch as getPurePayoff above; likewise - // never deducible (no arguments at all), so callers write getMaxPayoff(), etc. + // largest/smallest payoff in any pure profile; callers write getMaxPayoff(), etc. template V getMaxPayoff() const; template V getMinPayoff() const; @@ -271,15 +247,11 @@ class AGG { // the 'contribution' of s_i to D^(s) std::vector>> projection; - // working state for the convolution algorithm (see ConvolutionState above), one instance per - // numeric type V, since both may be in use simultaneously. ConvolutionState m_state; ConvolutionState m_exactState; - // foreach s in S, i in N, the full set of projected actions. Unlike m_state/m_exactState - // above, this is filled once (from the constructor's projS argument) and never rebuilt -- - // read directly by gametracer::aggame (a friend) for its own jacobian bookkeeping, not used by - // AGG's own convolution algorithm. + // full set of projected actions, filled once from the constructor's projS argument; read by + // gametracer::aggame for jacobian bookkeeping, not used by AGG's own convolution algorithm. std::vector>> fullProjectedStrat; // foreach s in S, foreach neighbor of s, its projection function @@ -349,9 +321,7 @@ class AGG { static void initPorder(std::vector &Po, int i, int N, std::vector> &projS); - // private methods. computeP/doProjection are the same algorithm for both V; they're made - // non-branching by going through state() for the V-specific working arrays (m_state vs - // m_exactState), rather than an if constexpr per call. + // dispatches to the V-specific working state, so computeP/doProjection don't need to branch. template ConvolutionState &state() { if constexpr (std::is_same_v) { @@ -373,16 +343,11 @@ class AGG { template void doProjection(int Node, V *s); template void doProjection(int Node, int player, V *s); - // exactPayoffs is a std::map<..., Number>, not a trie_map, so trie_map's own - // inner_prod can't be used directly against it (unlike the double path, which calls - // dist.inner_prod(payoffs[node]) directly) -- this does the equivalent sum manually. + // exactPayoffs is a std::map, not a trie_map, so this does the inner product by hand. Rational exactInnerProd(int node, const ConfigDistribution &dist) const; - // dispatch point used by getV/getJ: V=double calls trie_map's own inner_prod against - // PayoffTable, V=Rational calls exactInnerProd against ExactPayoffTable. Not merged further - // than this one function -- same reason PayoffTable/ExactPayoffTable themselves aren't a - // template pair (see their comments): the two sides genuinely differ in container and value - // type, not just in V. + // dispatch point used by getV/getJ: V=double uses trie_map's own inner_prod, V=Rational uses + // exactInnerProd. template V payoffInnerProd(int node, const ConfigDistribution &dist); void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 3dd426ba6e..75c58ebf12 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -66,14 +66,11 @@ class BAGG { template V getMaxPayoff() const { return aggPtr->getMaxPayoff(); } template V getMinPayoff() const { return aggPtr->getMinPayoff(); } - // exp. payoff under mixed strat profile, summed over player's types. No Rational counterpart -- - // unused outside this class; getPurePayoff and GameBAGGRep only ever go through the per-type - // overload below. + // exp. payoff under mixed strat profile, summed over player's types. No Rational counterpart; + // callers go through the per-type overload below. double getMixedPayoff(int player, StrategyProfile &s); // exp payoff for player, conditioned on her receiving type tp, via the convolution algorithm. - // V=double runs the floating-point engine; V=Rational runs the exact-arithmetic counterpart - // throughout -- see agg.h::getMixedPayoff for how the dispatch works. template V getMixedPayoff(int player, int tp, const StrategyProfile &s) const; void getPayoffVector(std::vector &dest, int player, int tp, diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 7a787a59a2..25fc2c88a6 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -215,10 +215,6 @@ MixedStrategyProfile GameAGGRep::NewMixedStrategyProfile(double) const MixedStrategyProfile GameAGGRep::NewMixedStrategyProfile(const Rational &) const { - // Constructing the profile itself is fine (e.g. to exactly represent a pure-strategy - // profile as a degenerate mixed one, as EnumPureStrategySolve does) -- it's specifically - // *payoff computation* on it that AGG cannot do exactly; see - // AGGMixedStrategyProfileRep::GetPayoff. return MixedStrategyProfile(std::make_unique>( StrategySupportProfile(std::const_pointer_cast(shared_from_this())))); } diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 21b25ab925..40d6584558 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -287,10 +287,6 @@ MixedStrategyProfile GameBAGGRep::NewMixedStrategyProfile(double) const MixedStrategyProfile GameBAGGRep::NewMixedStrategyProfile(const Rational &) const { - // Constructing the profile itself is fine (e.g. to exactly represent a pure-strategy - // profile as a degenerate mixed one, as EnumPureStrategySolve does) -- it's specifically - // *payoff computation* on it that BAGG cannot do exactly; see - // BAGGMixedStrategyProfileRep::GetPayoff. return MixedStrategyProfile(std::make_unique>( StrategySupportProfile(std::const_pointer_cast(shared_from_this())))); } diff --git a/tests/test_agg.py b/tests/test_agg.py index 3c9529d6c1..af2d1f49b1 100644 --- a/tests/test_agg.py +++ b/tests/test_agg.py @@ -43,10 +43,8 @@ def test_agg_bagg_to_nfg(game_path): def test_agg_fraction_and_long_decimal_payoffs_parsed_exactly(): - """Payoffs written in an .agg file as a fraction ("1/3") or a decimal needing more than - double's ~15-17 significant digits of precision to round-trip are parsed into exact - Rationals (via Number, agg::AGG::exactPayoffs), not silently rounded through a bare - `istream >> double` read. + """Payoffs written as a fraction ("1/3") or a decimal too precise for a double to round-trip + are parsed into exact Rationals, not silently rounded through a bare double read. 2x2_fraction_payoffs.agg is 2x2.agg with two of its four payoffs replaced: 35 -> 1/3 and 95 -> 0.123456789012345. @@ -58,8 +56,7 @@ def test_agg_fraction_and_long_decimal_payoffs_parsed_exactly(): assert player.max_payoff == gbt.Rational(1, 3) assert player.min_payoff == -10 - # pure-strategy payoff lookup (AGGPureStrategyProfileRep::GetPayoff) also reports the - # exact value, not a double-rounded approximation + # pure-strategy payoff lookup also reports the exact value, not a rounded approximation profile = game.mixed_strategy_profile(rational=True) p0, p1 = game.players s0 = list(p0.strategies) @@ -72,9 +69,9 @@ def test_agg_fraction_and_long_decimal_payoffs_parsed_exactly(): def test_bagg_fraction_type_distribution_parsed_exactly(): - """BAGG type-distribution probabilities written as a fraction ("1/3") are parsed exactly - (agg::BAGG::exactIndepTypeDist) and the exact weighted-sum payoff computation over them - (agg::BAGG::getExactMixedPayoff) agrees with the double engine on the same profile. + """BAGG type-distribution probabilities written as a fraction ("1/3") are parsed exactly, + and the exact payoff computation over them agrees with the double engine on the same + profile. 2x2_fraction_types.bagg: player 1 has two types (weights 1/3, 2/3), player 2 has one. """ @@ -93,13 +90,10 @@ def test_bagg_fraction_type_distribution_parsed_exactly(): @pytest.mark.parametrize("game_path", ["2x2.agg", "2x2.bagg"]) def test_agg_bagg_mixed_strategy_profile_rational_exact_payoff(game_path): - """AGG/BAGG mixed-strategy payoffs support exact (rational) computation: the convolution - algorithm (agg::AGG::getMixedPayoff et al.) is generic in its numeric type, so it also runs - with Rational arithmetic throughout (agg::AGG::getExactMixedPayoff), not just double. + """AGG/BAGG mixed-strategy payoffs support exact (rational) computation, not just double. 2x2.agg has a known mixed equilibrium at (10/11, 1/11) for both players, with payoff - exactly -5/11 to each -- computed here directly (not via a solver), confirming the exact - engine reproduces it with zero regret. + exactly -5/11 to each -- computed here directly (not via a solver), confirming zero regret. """ game = games.read_from_file(game_path) profile = game.mixed_strategy_profile(rational=True) diff --git a/tests/test_nash.py b/tests/test_nash.py index 1397ed38f3..5fdd85af04 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -928,10 +928,7 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_double_11", ), - # Action graph game. Exact throughout: pure-strategy payoffs via - # agg::AGG::getExactPurePayoff, and the genuinely mixed equilibrium found here via - # agg::AGG::getExactMixedPayoff (Rational arithmetic through the same convolution - # algorithm the double engine uses). + # Action graph game, solved exactly (Rational arithmetic throughout). pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "2x2.agg"), From 6f8fad656c6ea3c25451b8f959953bdeb09ee4f7 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 14 Aug 2026 19:17:54 +0100 Subject: [PATCH 13/14] Fix exponential blowup in BAGG pure-strategy payoff; templatize as getPurePayoff --- src/games/agg/bagg.cc | 60 +++++++------------------------------------ src/games/agg/bagg.h | 6 ++--- src/games/gamebagg.cc | 4 +-- tests/test_agg.py | 49 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index dfb2970c4e..80b07a824b 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -268,61 +268,19 @@ template double BAGG::getMixedPayoff(int player, int tp, template Rational BAGG::getMixedPayoff(int player, int tp, const StrategyProfile &s) const; -double BAGG::getPurePayoff(int player, int tp, std::vector &ps) +// payoff of the pure profile ps, via the degenerate mixed profile and the convolution engine +// (mirrors AGG::getPurePayoff). +template V BAGG::getPurePayoff(int player, int tp, const std::vector &ps) const { - StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); - for (int i = 0; i < strategyOffset[typeOffset[numPlayers]]; i++) { - st[i] = (double)0.0; - } + StrategyProfile s(strategyOffset[typeOffset[numPlayers]]); for (int i = 0; i < typeOffset[numPlayers]; i++) { - st[strategyOffset[i] + ps[i]] = (double)1.0; - } - return getMixedPayoff(player, tp, st); -} - -// Unlike getPurePayoff() above, this does NOT delegate to the general mixed-payoff convolution -// engine. A BAGG pure profile only leaves the OTHER players' realized types uncertain (each -// independent, per exactIndepTypeDist), not their actions -- so the expectation is a small, -// exact, finite sum over the Cartesian product of the other players' types (bounded by the -// product of their type counts, not their action counts), each term an exact AGG-level pure -// payoff via AGG::getPurePayoff. -Rational BAGG::getExactPurePayoff(int player, int tp, const std::vector &ps) const -{ - std::vector others; - for (int pl = 0; pl < numPlayers; ++pl) { - if (pl != player) { - others.push_back(pl); - } - } - - std::vector sAGG(numPlayers); - sAGG[player] = typeAction2ActionIndex[player][tp][ps[typeOffset[player] + tp]]; - - std::vector typeIndex(others.size(), 0); - Rational total(0); - while (true) { - Rational weight(1); - for (size_t k = 0; k < others.size(); ++k) { - const int pl = others[k]; - const int t = typeIndex[k]; - sAGG[pl] = typeAction2ActionIndex[pl][t][ps[typeOffset[pl] + t]]; - weight *= static_cast(exactIndepTypeDist[pl][t]); - } - total += weight * aggPtr->getPurePayoff(player, sAGG); - - size_t k = 0; - for (; k < others.size(); ++k) { - if (++typeIndex[k] < numTypes[others[k]]) { - break; - } - typeIndex[k] = 0; - } - if (k == others.size()) { - break; - } + s[strategyOffset[i] + ps[i]] = V(1); } - return total; + return getMixedPayoff(player, tp, s); } +template double BAGG::getPurePayoff(int player, int tp, const std::vector &ps) const; +template Rational BAGG::getPurePayoff(int player, int tp, + const std::vector &ps) const; void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) { diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 75c58ebf12..642947dd03 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -77,16 +77,16 @@ class BAGG { const StrategyProfile &s); template V getV(int player, int tp, int action, const StrategyProfile &s) const; - double getPurePayoff(int player, int tp, std::vector &s); + // payoff for player, conditioned on receiving type tp, under the pure profile s. + template V getPurePayoff(int player, int tp, const std::vector &s) const; double getPurePayoff(int player, std::vector &s) { double r = 0; for (int i = 0; i < numTypes[player]; ++i) { - r += indepTypeDist[player][i] * getPurePayoff(player, i, s); + r += indepTypeDist[player][i] * getPurePayoff(player, i, s); } return r; } - Rational getExactPurePayoff(int player, int tp, const std::vector &ps) const; double getSymMixedPayoff(StrategyProfile &s); diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 40d6584558..53041a73e1 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -57,7 +57,7 @@ Rational BAGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const } const int bp = dynamic_cast(*m_game).agent2baggPlayer[p_player->GetNumber()]; const int tp = p_player->GetNumber() - 1 - baggPtr->typeOffset[bp - 1]; - return baggPtr->getExactPurePayoff(bp - 1, tp, s); + return baggPtr->getPurePayoff(bp - 1, tp, s); } Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -71,7 +71,7 @@ Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_stra s[player - 1] = p_strategy->GetNumber() - 1; const int bp = dynamic_cast(*m_game).agent2baggPlayer[player]; const int tp = player - 1 - baggPtr->typeOffset[bp - 1]; - return baggPtr->getExactPurePayoff(bp - 1, tp, s); + return baggPtr->getPurePayoff(bp - 1, tp, s); } //======================================================================== diff --git a/tests/test_agg.py b/tests/test_agg.py index af2d1f49b1..c417a6f3af 100644 --- a/tests/test_agg.py +++ b/tests/test_agg.py @@ -1,6 +1,8 @@ """Tests of generic Game operations on action graph game (AGG/BAGG) representations. """ +import itertools + import pytest import pygambit as gbt @@ -124,3 +126,50 @@ def test_agg_bagg_rational_algorithms_find_exact_mixed_equilibrium(game_path): for strategy in player.strategies: assert mixed[0][strategy] in (gbt.Rational(10, 11), gbt.Rational(1, 11)) assert mixed[0].max_regret() == 0 + + +def _set_pure_profile(profile, players, contingency): + for player, strat_index in zip(players, contingency, strict=True): + for i, strategy in enumerate(player.strategies): + profile[strategy] = gbt.Rational(1) if i == strat_index else gbt.Rational(0) + + +@pytest.mark.parametrize("game_path", ["2x2.bagg", "2x2_fraction_types.bagg"]) +def test_bagg_pure_strategy_payoff_matches_degenerate_mixed_profile(game_path): + """BAGG's exact pure-strategy payoff agrees, for every pure-strategy contingency, with the + payoff of the corresponding degenerate mixed profile computed via the general convolution + engine -- covering agg::BAGG::getPurePayoff, which had no dedicated test before. + """ + game = games.read_from_file(game_path) + players = list(game.players) + for contingency in itertools.product(*(range(len(list(p.strategies))) for p in players)): + pure_payoffs = [game[contingency][p] for p in players] + + profile = game.mixed_strategy_profile(rational=True) + _set_pure_profile(profile, players, contingency) + mixed_payoffs = [profile.payoff(p) for p in players] + + assert pure_payoffs == mixed_payoffs + + +def test_bagg_pure_strategy_payoff_with_multiple_players_and_types(): + """Same cross-check as above, sampled (full enumeration is 7**6 contingencies) on a BAGG + with several players each holding several types, to exercise getPurePayoff's + handling of more than one other player's type distribution at once. + """ + game = games.read_from_file("Bayesian-Coffee-3-2-2-3.bagg") + players = list(game.players) + sizes = [len(list(p.strategies)) for p in players] + contingencies = [ + tuple(0 for _ in sizes), + tuple(size - 1 for size in sizes), + tuple(i % size for i, size in enumerate(sizes)), + ] + for contingency in contingencies: + pure_payoffs = [game[contingency][p] for p in players] + + profile = game.mixed_strategy_profile(rational=True) + _set_pure_profile(profile, players, contingency) + mixed_payoffs = [profile.payoff(p) for p in players] + + assert pure_payoffs == mixed_payoffs From df3abc37c98e8d04e77eddf6486e2c98b6482755 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Tue, 18 Aug 2026 16:26:04 +0100 Subject: [PATCH 14/14] Update ChangeLog --- ChangeLog | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 90effad052..57cbe6baab 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +## [17.0.0-alpha.2] - unreleased + +### Added +- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047) + +### Fixed +- `Game.add_player`, on a game in strategic form, no longer leaves the new player's sole + strategy with an empty label; it is now labeled `"1"`, as `Game.new_table` already labels + every other player's initial strategies. (#1047) +- Numerical data in action graph games is now read with exact precision, to match other game formats. (#879) + +### Removed +- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces + nonempty, unique labels. (#1047) + + ## [17.0.0-alpha.1] - 2026-08-13 ### Added @@ -11,7 +27,6 @@ its actions to be specified up front, just as `Game.make_event` does. (#999) - `Game.get_behavior` returns a map-like view of the actions a reduced strategy prescribes at its player's information sets; the unreachable information sets are absent. (#1002) -- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047) ### Changed - Adopted the C++20 standard; build system now enforces exactly C++20 (not a minimum) (cf. #1027) @@ -60,9 +75,6 @@ ### Fixed - Converting a behavior profile to a strategy profile, and computing pure-strategy payoffs no longer inserts spurious entries into a strategy's internal action map. -- `Game.add_player`, on a game in strategic form, no longer leaves the new player's sole - strategy with an empty label; it is now labeled `"1"`, as `Game.new_table` already labels - every other player's initial strategies. (#1047) ### Removed - `Game.set_player` has been removed; use `Game.make_infoset`, specifying the members of the @@ -87,8 +99,6 @@ stub overrides it forced on the strategic, AGG, and BAGG representations; it was subsumed by `MakeEvent`, and by this point was used only in support of the GUI's "Edit Move" dialog, which now builds the equivalent `MakeEvent` call directly. (#999) -- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces - nonempty, unique labels. (#1047) ## [16.7.0] - 2026-07-11