From 008b7e8c6b7a3a3eeb899f595ebfac2b47d38b3d Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Wed, 23 Oct 2024 12:57:17 +0100 Subject: [PATCH] Modernise and simplify StrategySupportProfile implementation. This cleans up various aspects of technical debt in StrategySupportProfile and related classes: * Using STL containers instead of Gambit ones * Defining appropriate abstractions * Preferring STL-style algorithms to manually-crafted ones * Avoiding using explicit 1-based indexes in favour of contaniner-iterator idiom. --- src/core/array.h | 2 +- src/core/rational.cc | 2 + src/core/rational.h | 1 + src/gambit.h | 6 + src/games/game.cc | 14 +- src/games/gameagg.cc | 6 +- src/games/gamebagg.cc | 6 +- src/games/stratmixed.h | 6 +- src/games/stratpure.cc | 91 +++------- src/games/stratpure.h | 21 +-- src/games/stratspt.cc | 249 ++++++++++---------------- src/games/stratspt.h | 54 +++--- src/games/writer.cc | 12 +- src/gui/nfgtable.cc | 43 +++-- src/pygambit/gambit.pxd | 23 ++- src/pygambit/game.pxi | 26 ++- src/pygambit/nashphc.py | 11 +- src/pygambit/stratspt.pxi | 102 +++++++---- src/pygambit/supports.py | 11 +- src/solvers/enumpoly/nfgpoly.cc | 2 +- src/solvers/nashsupport/nfgsupport.cc | 5 - tests/test_stratprofiles.py | 79 ++++---- 22 files changed, 380 insertions(+), 392 deletions(-) diff --git a/src/core/array.h b/src/core/array.h index c42f9a089..be4923880 100644 --- a/src/core/array.h +++ b/src/core/array.h @@ -73,7 +73,7 @@ template class Array { using pointer = value_type *; using reference = value_type &; - iterator(Array *p_array, int p_index) : m_array(p_array), m_index(p_index) {} + iterator(Array *p_array = 0, int p_index = 0) : m_array(p_array), m_index(p_index) {} reference operator*() { return (*m_array)[m_index]; } pointer operator->() { return &(*m_array)[m_index]; } iterator &operator++() diff --git a/src/core/rational.cc b/src/core/rational.cc index fe6df6e1d..ddad5a189 100644 --- a/src/core/rational.cc +++ b/src/core/rational.cc @@ -401,6 +401,8 @@ Rational::Rational(long n) : num(n), den(&OneRep) {} Rational::Rational(int n) : num(n), den(&OneRep) {} +Rational::Rational(size_t n) : num(int(n)), den(&OneRep) {} + Rational::Rational(long n, long d) : num(n), den(d) { if (d == 0) { diff --git a/src/core/rational.h b/src/core/rational.h index 37b902130..f8f6295b3 100644 --- a/src/core/rational.h +++ b/src/core/rational.h @@ -44,6 +44,7 @@ class Rational { Rational(); explicit Rational(double); explicit Rational(int); + explicit Rational(size_t); explicit Rational(long n); Rational(int n, int d); Rational(long n, long d); diff --git a/src/gambit.h b/src/gambit.h index 214aec972..cd10edcb6 100644 --- a/src/gambit.h +++ b/src/gambit.h @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace Gambit { @@ -53,6 +54,11 @@ inline double abs(double x) { return std::fabs(x); } inline double sqr(double x) { return x * x; } +template bool contains(const C &p_container, const T &p_value) +{ + return std::find(p_container.cbegin(), p_container.cend(), p_value) != p_container.cend(); +} + template bool contains(const std::map &map, const Key &key) // TODO: remove when we move to C++20 which already includes a "contains" method { diff --git a/src/games/game.cc b/src/games/game.cc index 8dadcb94b..408805169 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -312,15 +312,27 @@ void GameRep::WriteNfgFile(std::ostream &p_file) const template MixedStrategyProfileRep::MixedStrategyProfileRep(const StrategySupportProfile &p_support) : m_probs(p_support.MixedProfileLength()), m_support(p_support), + m_profileIndex(p_support.GetGame()->MixedProfileLength()), m_gameversion(p_support.GetGame()->GetVersion()) { + int index = 1, stnum = 1; + for (auto player : p_support.GetGame()->GetPlayers()) { + for (auto strategy : player->GetStrategies()) { + if (p_support.Contains(strategy)) { + m_profileIndex[stnum++] = index++; + } + else { + m_profileIndex[stnum++] = -1; + } + } + } SetCentroid(); } template void MixedStrategyProfileRep::SetCentroid() { for (auto player : m_support.GetGame()->GetPlayers()) { - T center = ((T)1) / ((T)m_support.NumStrategies(player->GetNumber())); + T center = T(1) / T(m_support.GetStrategies(player).size()); for (auto strategy : m_support.GetStrategies(player)) { (*this)[strategy] = center; } diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index fa99371a2..73d019ee1 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -103,7 +103,7 @@ template T AGGMixedStrategyProfileRep::GetPayoff(int pl) const for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); - int ind = this->m_support.m_profileIndex[strategy->GetId()]; + int ind = this->m_profileIndex[strategy->GetId()]; s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs[ind]; } } @@ -125,7 +125,7 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); - const int &ind = this->m_support.m_profileIndex[strategy->GetId()]; + const int &ind = this->m_profileIndex[strategy->GetId()]; s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs[ind]; } } @@ -161,7 +161,7 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1, else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); - const int &ind = this->m_support.m_profileIndex[strategy->GetId()]; + const int &ind = this->m_profileIndex[strategy->GetId()]; s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs[ind]; } } diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 31a32b307..7a1deefb2 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -115,7 +115,7 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const GameStrategy strategy = this->m_support.GetGame() ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); - const int &ind = this->m_support.m_profileIndex[strategy->GetId()]; + int ind = this->m_profileIndex[strategy->GetId()]; s.at(offs) = (ind == -1) ? (T)0 : this->m_probs[ind]; } } @@ -146,7 +146,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) GameStrategy strategy = this->m_support.GetGame() ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); - const int &ind = this->m_support.m_profileIndex[strategy->GetId()]; + int ind = this->m_profileIndex[strategy->GetId()]; s.at(g.baggPtr->firstAction(i, tp) + j) = (ind == -1) ? Rational(0) : this->m_probs[ind]; } } @@ -192,7 +192,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 GameStrategy strategy = this->m_support.GetGame() ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); - const int &ind = this->m_support.m_profileIndex[strategy->GetId()]; + int ind = this->m_profileIndex[strategy->GetId()]; s.at(g.baggPtr->firstAction(i, tp) + j) = static_cast((ind == -1) ? T(0) : this->m_probs[ind]); } diff --git a/src/games/stratmixed.h b/src/games/stratmixed.h index ea665beb2..1b6f010a5 100644 --- a/src/games/stratmixed.h +++ b/src/games/stratmixed.h @@ -33,6 +33,8 @@ template class MixedStrategyProfileRep { public: Vector m_probs; StrategySupportProfile m_support; + /// The index into the strategy profile for a strategy (-1 if not in support) + Array m_profileIndex; unsigned int m_gameversion; explicit MixedStrategyProfileRep(const StrategySupportProfile &); @@ -44,12 +46,12 @@ template class MixedStrategyProfileRep { /// Returns the probability the strategy is played const T &operator[](const GameStrategy &p_strategy) const { - return m_probs[m_support.m_profileIndex[p_strategy->GetId()]]; + return m_probs[m_profileIndex[p_strategy->GetId()]]; } /// Returns the probability the strategy is played T &operator[](const GameStrategy &p_strategy) { - return m_probs[m_support.m_profileIndex[p_strategy->GetId()]]; + return m_probs[m_profileIndex[p_strategy->GetId()]]; } virtual T GetPayoff(int pl) const = 0; diff --git a/src/games/stratpure.cc b/src/games/stratpure.cc index eec80258f..29758e1a9 100644 --- a/src/games/stratpure.cc +++ b/src/games/stratpure.cc @@ -111,87 +111,38 @@ MixedStrategyProfile PureStrategyProfileRep::ToMixedStrategyProfile() // Lifecycle //--------------------------------------------------------------------------- -StrategyProfileIterator::StrategyProfileIterator(const StrategySupportProfile &p_support) - : m_atEnd(false), m_support(p_support), m_currentStrat(m_support.GetGame()->NumPlayers()), - m_profile(m_support.GetGame()->NewPureStrategyProfile()), m_frozen1(0), m_frozen2(0) -{ - First(); -} - -StrategyProfileIterator::StrategyProfileIterator(const StrategySupportProfile &p_support, int pl, - int st) - : m_atEnd(false), m_support(p_support), m_currentStrat(m_support.GetGame()->NumPlayers()), - m_profile(m_support.GetGame()->NewPureStrategyProfile()), m_frozen1(pl), m_frozen2(0) -{ - m_currentStrat[pl] = st; - m_profile->SetStrategy(m_support.GetStrategy(pl, st)); - First(); -} - StrategyProfileIterator::StrategyProfileIterator(const StrategySupportProfile &p_support, - const GameStrategy &p_strategy) - : m_atEnd(false), m_support(p_support), m_currentStrat(p_support.GetGame()->NumPlayers()), - m_profile(p_support.GetGame()->NewPureStrategyProfile()), - m_frozen1(p_strategy->GetPlayer()->GetNumber()), m_frozen2(0) -{ - m_currentStrat[m_frozen1] = p_strategy->GetNumber(); - m_profile->SetStrategy(p_strategy); - First(); -} - -StrategyProfileIterator::StrategyProfileIterator(const StrategySupportProfile &p_support, int pl1, - int st1, int pl2, int st2) - : m_atEnd(false), m_support(p_support), m_currentStrat(m_support.GetGame()->NumPlayers()), - m_profile(m_support.GetGame()->NewPureStrategyProfile()), m_frozen1(pl1), m_frozen2(pl2) -{ - m_currentStrat[pl1] = st1; - m_profile->SetStrategy(m_support.GetStrategy(pl1, st1)); - m_currentStrat[pl2] = st2; - m_profile->SetStrategy(m_support.GetStrategy(pl2, st2)); - First(); -} - -//--------------------------------------------------------------------------- -// Iteration -//--------------------------------------------------------------------------- - -void StrategyProfileIterator::First() + const std::vector &p_frozen) + : m_support(p_support), m_profile(p_support.GetGame()->NewPureStrategyProfile()), + m_frozen(p_frozen) { - for (int pl = 1; pl <= m_support.GetGame()->NumPlayers(); pl++) { - if (pl == m_frozen1 || pl == m_frozen2) { - continue; + for (auto strategy : m_frozen) { + m_profile->SetStrategy(strategy); + } + for (auto player : m_support.GetGame()->GetPlayers()) { + auto frozen = std::find_if(m_frozen.begin(), m_frozen.end(), [player](const GameStrategy &s) { + return s->GetPlayer() == player; + }); + if (frozen == m_frozen.end()) { + m_unfrozen.push_back(player); + m_currentStrat[player] = m_support.GetStrategies(player).begin(); + m_profile->SetStrategy(*m_currentStrat[player]); } - m_profile->SetStrategy(m_support.GetStrategy(pl, 1)); - m_currentStrat[pl] = 1; } } void StrategyProfileIterator::operator++() { - int pl = 1; - - while (true) { - if (pl == m_frozen1 || pl == m_frozen2) { - pl++; - if (pl > m_support.GetGame()->NumPlayers()) { - m_atEnd = true; - return; - } - continue; - } - - if (m_currentStrat[pl] < m_support.NumStrategies(pl)) { - m_profile->SetStrategy(m_support.GetStrategy(pl, ++(m_currentStrat[pl]))); - return; - } - m_profile->SetStrategy(m_support.GetStrategy(pl, 1)); - m_currentStrat[pl] = 1; - pl++; - if (pl > m_support.GetGame()->NumPlayers()) { - m_atEnd = true; + for (auto player : m_unfrozen) { + ++m_currentStrat[player]; + if (m_currentStrat[player] != m_support.GetStrategies(player).end()) { + m_profile->SetStrategy(*m_currentStrat[player]); return; } + m_currentStrat[player] = m_support.GetStrategies(player).begin(); + m_profile->SetStrategy(*m_currentStrat[player]); } + m_atEnd = true; } } // namespace Gambit diff --git a/src/games/stratpure.h b/src/games/stratpure.h index 398b20743..ef2c0bf25 100644 --- a/src/games/stratpure.h +++ b/src/games/stratpure.h @@ -137,26 +137,19 @@ class StrategyProfileIterator { friend class GameTableRep; private: - bool m_atEnd; + bool m_atEnd{false}; StrategySupportProfile m_support; - Array m_currentStrat; + std::map m_currentStrat; PureStrategyProfile m_profile; - int m_frozen1, m_frozen2; - - /// Reset the iterator to the first contingency (this is called by ctors) - void First(); + std::vector m_unfrozen; + std::vector m_frozen; public: /// @name Lifecycle //@{ - /// Construct a new iterator on the support, with no strategies held fixed - explicit StrategyProfileIterator(const StrategySupportProfile &); - /// Construct a new iterator on the support, fixing player pl's strategy - StrategyProfileIterator(const StrategySupportProfile &s, int pl, int st); - /// Construct a new iterator on the support, fixing the given strategy - StrategyProfileIterator(const StrategySupportProfile &, const GameStrategy &); - /// Construct a new iterator on the support, fixing two players' strategies - StrategyProfileIterator(const StrategySupportProfile &s, int pl1, int st1, int pl2, int st2); + /// Construct a new iterator on the support + explicit StrategyProfileIterator(const StrategySupportProfile &, + const std::vector &p_frozen = {}); //@} /// @name Iteration and data access diff --git a/src/games/stratspt.cc b/src/games/stratspt.cc index 7835a04bc..f0f956b9d 100644 --- a/src/games/stratspt.cc +++ b/src/games/stratspt.cc @@ -20,51 +20,42 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include +#include + #include "gambit.h" #include "gametable.h" namespace Gambit { //=========================================================================== -// class StrategySupportProfile +// class StrategySupportProfile //=========================================================================== -//--------------------------------------------------------------------------- -// Lifecycle -//--------------------------------------------------------------------------- - -StrategySupportProfile::StrategySupportProfile(const Game &p_nfg) - : m_nfg(p_nfg), m_profileIndex(p_nfg->MixedProfileLength()) +StrategySupportProfile::StrategySupportProfile(const Game &p_nfg) : m_nfg(p_nfg) { - for (int pl = 1, index = 1; pl <= p_nfg->NumPlayers(); pl++) { - m_support.push_back(Array()); - for (int st = 1; st <= p_nfg->GetPlayer(pl)->NumStrategies(); st++, index++) { - m_support[pl].push_back(p_nfg->GetPlayer(pl)->GetStrategy(st)); - m_profileIndex[index] = index; + for (auto player : m_nfg->GetPlayers()) { + for (auto strategy : player->GetStrategies()) { + m_support[player].push_back(strategy); } } } -//--------------------------------------------------------------------------- -// General information -//--------------------------------------------------------------------------- - Array StrategySupportProfile::NumStrategies() const { - Array a(m_support.Length()); - - for (int pl = 1; pl <= a.Length(); pl++) { - a[pl] = m_support[pl].Length(); - } - return a; + Array dim(m_support.size()); + std::transform( + m_support.cbegin(), m_support.cend(), dim.begin(), + [](std::pair> a) { return a.second.size(); }); + return dim; } int StrategySupportProfile::MixedProfileLength() const { - int total = 0; - for (int pl = 1; pl <= m_nfg->NumPlayers(); total += m_support[pl++].Length()) - ; - return total; + return std::accumulate(m_support.cbegin(), m_support.cend(), 0, + [](size_t tot, std::pair> a) { + return tot + a.second.size(); + }); } template <> MixedStrategyProfile StrategySupportProfile::NewMixedStrategyProfile() const @@ -79,20 +70,15 @@ template <> MixedStrategyProfile StrategySupportProfile::NewMixedStrat bool StrategySupportProfile::IsSubsetOf(const StrategySupportProfile &p_support) const { - if (m_nfg != p_support.m_nfg) { - return false; - } - for (int pl = 1; pl <= m_support.Length(); pl++) { - if (m_support[pl].Length() > p_support.m_support[pl].Length()) { + for (auto player : m_nfg->GetPlayers()) { + if (!std::includes(p_support.m_support.at(player).begin(), + p_support.m_support.at(player).end(), m_support.at(player).begin(), + m_support.at(player).end(), + [](const GameStrategy &s, const GameStrategy &t) { + return s->GetNumber() < t->GetNumber(); + })) { return false; } - else { - for (int st = 1; st <= m_support[pl].Length(); st++) { - if (!p_support.m_support[pl].Contains(m_support[pl][st])) { - return false; - } - } - } } return true; } @@ -124,72 +110,29 @@ void StrategySupportProfile::WriteNfgFile(std::ostream &p_file) const }; } -//--------------------------------------------------------------------------- -// Modifying the support -//--------------------------------------------------------------------------- - void StrategySupportProfile::AddStrategy(const GameStrategy &p_strategy) { - // Get the null-pointer checking out of the way once and for all - GameStrategyRep *strategy = p_strategy; - Array &support = m_support[strategy->GetPlayer()->GetNumber()]; - - for (int i = 1; i <= support.Length(); i++) { - GameStrategyRep *s = support[i]; - if (s->GetNumber() == strategy->GetNumber()) { - // Strategy already in support; no change - return; - } - if (s->GetNumber() > strategy->GetNumber()) { - // Shift all higher-id strategies by one in the profile - m_profileIndex[strategy->GetId()] = m_profileIndex[s->GetId()]; - for (int id = s->GetId(); id <= m_profileIndex.Length(); id++) { - if (m_profileIndex[id] >= 0) { - m_profileIndex[id]++; - } - } - // Insert here - support.Insert(strategy, i); - return; - } + auto &support = m_support[p_strategy->GetPlayer()]; + auto pos = std::find_if(support.begin(), support.end(), [p_strategy](const GameStrategy &s) { + return s->GetNumber() >= p_strategy->GetNumber(); + }); + if (pos == support.end() || *pos != p_strategy) { + // Strategy is not in the support for the player; add at this location to keep sorted by number + support.insert(pos, p_strategy); } - - // If we get here, p_strategy has a higher number than anything in the - // support for this player; append. - GameStrategyRep *last = support[support.Last()]; - m_profileIndex[strategy->GetId()] = m_profileIndex[last->GetId()] + 1; - for (int id = strategy->GetId() + 1; id <= m_profileIndex.Length(); id++) { - if (m_profileIndex[id] >= 0) { - m_profileIndex[id]++; - } - } - support.push_back(strategy); } bool StrategySupportProfile::RemoveStrategy(const GameStrategy &p_strategy) { - GameStrategyRep *strategy = p_strategy; - Array &support = m_support[strategy->GetPlayer()->GetNumber()]; - - if (support.Length() == 1) { + auto &support = m_support[p_strategy->GetPlayer()]; + if (support.size() == 1) { return false; } - - for (int i = 1; i <= support.Length(); i++) { - GameStrategyRep *s = support[i]; - if (s == strategy) { - support.Remove(i); - m_profileIndex[strategy->GetId()] = -1; - // Shift strategies left in the profile - for (int id = strategy->GetId() + 1; id <= m_profileIndex.Length(); id++) { - if (m_profileIndex[id] >= 0) { - m_profileIndex[id]--; - } - } - return true; - } + auto pos = std::find(support.begin(), support.end(), p_strategy); + if (pos != support.end()) { + support.erase(pos); + return true; } - return false; } @@ -225,18 +168,16 @@ bool StrategySupportProfile::IsDominated(const GameStrategy &s, bool p_strict, bool p_external) const { if (p_external) { - GamePlayer player = s->GetPlayer(); - for (int st = 1; st <= player->NumStrategies(); st++) { - if (player->GetStrategy(st) != s && Dominates(player->GetStrategy(st), s, p_strict)) { + for (auto strategy : s->GetPlayer()->GetStrategies()) { + if (strategy != s && Dominates(strategy, s, p_strict)) { return true; } } return false; } else { - for (int i = 1; i <= NumStrategies(s->GetPlayer()->GetNumber()); i++) { - if (GetStrategy(s->GetPlayer()->GetNumber(), i) != s && - Dominates(GetStrategy(s->GetPlayer()->GetNumber(), i), s, p_strict)) { + for (auto strategy : GetStrategies(s->GetPlayer())) { + if (strategy != s && Dominates(strategy, s, p_strict)) { return true; } } @@ -244,86 +185,88 @@ bool StrategySupportProfile::IsDominated(const GameStrategy &s, bool p_strict, } } -bool StrategySupportProfile::Undominated(StrategySupportProfile &newS, const GamePlayer &p_player, - bool p_strict, bool p_external) const +namespace { + +/// @brief Sort a range by a partial ordering and indicate minimal elements +/// +/// Sorts the range bracketed by `first` and `last` according to a partial ordering. +/// The partial ordering is specified by `greater`, which should be a binary +/// operation which returns `true` if the first argument is greater than the +/// second argument in the partial ordering. +/// +/// On termination, the range between `first` and `last` is sorted in decreasing +/// order by the partial ordering, in the sense that, if x > y according to the +/// partial ordering, then x will come before y in the sorted output. +/// +/// By this convention the set of **minimal elements** will all appear at the end +/// of the sorted output. The function returns an iterator pointing to the first +/// minimal element in the sorted range. +template +Iterator find_minimal_elements(Iterator first, Iterator last, Comparator greater) { - Array set((p_external) ? p_player->NumStrategies() - : NumStrategies(p_player->GetNumber())); - - if (p_external) { - for (int st = 1; st <= set.Length(); st++) { - set[st] = p_player->GetStrategy(st); - } - } - else { - for (int st = 1; st <= set.Length(); st++) { - set[st] = GetStrategy(p_player->GetNumber(), st); - } - } - - int min = 0, dis = set.Length() - 1; - + auto min = first, dis = std::prev(last); while (min <= dis) { - int pp; - for (pp = 0; pp < min && !Dominates(set[pp + 1], set[dis + 1], p_strict); pp++) - ; + auto pp = std::adjacent_find(first, last, greater); if (pp < min) { dis--; } else { - GameStrategy foo = set[dis + 1]; - set[dis + 1] = set[min + 1]; - set[min + 1] = foo; - - for (int inc = min + 1; inc <= dis;) { - if (Dominates(set[min + 1], set[dis + 1], p_strict)) { + std::iter_swap(dis, min); + auto inc = std::next(min); + while (inc <= dis) { + if (greater(*min, *dis)) { dis--; } - else if (Dominates(set[dis + 1], set[min + 1], p_strict)) { - foo = set[dis + 1]; - set[dis + 1] = set[min + 1]; - set[min + 1] = foo; + else if (greater(*dis, *min)) { + std::iter_swap(dis, min); dis--; } else { - foo = set[dis + 1]; - set[dis + 1] = set[inc + 1]; - set[inc + 1] = foo; + std::iter_swap(dis, inc); inc++; } } min++; } } + return min; +} - if (min + 1 <= set.Length()) { - for (int i = min + 1; i <= set.Length(); i++) { - newS.RemoveStrategy(set[i]); - } - return true; +} // end anonymous namespace + +bool UndominatedForPlayer(const StrategySupportProfile &p_support, + StrategySupportProfile &p_newSupport, const GamePlayer &p_player, + bool p_strict, bool p_external) +{ + std::vector set((p_external) ? p_player->NumStrategies() + : p_support.GetStrategies(p_player).size()); + if (p_external) { + auto strategies = p_player->GetStrategies(); + std::copy(strategies.begin(), strategies.end(), set.begin()); } else { - return false; + auto strategies = p_support.GetStrategies(p_player); + std::copy(strategies.begin(), strategies.end(), set.begin()); + } + auto min = + find_minimal_elements(set.begin(), set.end(), + [&p_support, p_strict](const GameStrategy &s, const GameStrategy &t) { + return p_support.Dominates(s, t, p_strict); + }); + + for (auto s = min; s != set.end(); s++) { + p_newSupport.RemoveStrategy(*s); } + return min != set.end(); } StrategySupportProfile StrategySupportProfile::Undominated(bool p_strict, bool p_external) const { - StrategySupportProfile newS(*this); - + StrategySupportProfile newSupport(*this); for (auto player : m_nfg->GetPlayers()) { - Undominated(newS, player, p_strict, p_external); + UndominatedForPlayer(*this, newSupport, player, p_strict, p_external); } - - return newS; -} - -StrategySupportProfile StrategySupportProfile::Undominated(bool p_strict, - const GamePlayer &p_player) const -{ - StrategySupportProfile newS(*this); - Undominated(newS, p_player, p_strict); - return newS; + return newSupport; } //--------------------------------------------------------------------------- diff --git a/src/games/stratspt.h b/src/games/stratspt.h index 75df1bfe1..bf9e6c1c4 100644 --- a/src/games/stratspt.h +++ b/src/games/stratspt.h @@ -27,9 +27,7 @@ namespace Gambit { -class StrategySupportProfile; - -/// \brief A support on a strategic game +/// @brief A support on a strategic game /// /// This class represents a subset of the strategies in strategic game. /// It is enforced that each player has at least one strategy; thus, @@ -41,22 +39,33 @@ class StrategySupportProfile; /// Within the support, strategies are maintained in the same order /// in which they appear in the underlying game. class StrategySupportProfile { - template friend class MixedStrategyProfile; - template friend class MixedStrategyProfileRep; - template friend class AGGMixedStrategyProfileRep; - template friend class BAGGMixedStrategyProfileRep; - protected: Game m_nfg; - Array> m_support; + std::map> m_support; + +public: + class Support { + private: + const StrategySupportProfile *m_profile; + GamePlayer m_player; - /// The index into a strategy profile for a strategy (-1 if not in support) - Array m_profileIndex; + public: + using const_iterator = std::vector::const_iterator; - bool Undominated(StrategySupportProfile &newS, const GamePlayer &, bool p_strict, - bool p_external = false) const; + Support() : m_profile(0), m_player(0) {} + Support(const StrategySupportProfile *p_profile, GamePlayer p_player) + : m_profile(p_profile), m_player(p_player) + { + } + + size_t size() const { return m_profile->m_support.at(m_player).size(); } + GameStrategy front() const { return m_profile->m_support.at(m_player).front(); } + GameStrategy back() const { return m_profile->m_support.at(m_player).back(); } + + const_iterator begin() const { return m_profile->m_support.at(m_player).begin(); } + const_iterator end() const { return m_profile->m_support.at(m_player).end(); } + }; -public: /// @name Lifecycle //@{ /// Constructor. By default, a support contains all strategies. @@ -82,9 +91,6 @@ class StrategySupportProfile { /// Returns the game on which the support is defined. Game GetGame() const { return m_nfg; } - /// Returns the number of strategies in the support for player pl. - int NumStrategies(int pl) const { return m_support[pl].size(); } - /// Returns the number of strategies in the support for all players. Array NumStrategies() const; @@ -93,21 +99,15 @@ class StrategySupportProfile { template MixedStrategyProfile NewMixedStrategyProfile() const; - /// Returns the strategy in the st'th position for player pl. - GameStrategy GetStrategy(int pl, int st) const { return m_support[pl][st]; } - /// Returns the number of players in the game int NumPlayers() const { return m_nfg->NumPlayers(); } /// Returns the set of players in the game Array GetPlayers() const { return m_nfg->GetPlayers(); } /// Returns the set of strategies in the support for a player - const Array &GetStrategies(const GamePlayer &p_player) const - { - return m_support[p_player->GetNumber()]; - } + Support GetStrategies(const GamePlayer &p_player) const { return Support(this, p_player); } /// Returns true exactly when the strategy is in the support. - bool Contains(const GameStrategy &s) const { return m_profileIndex[s->GetId()] >= 0; } + bool Contains(const GameStrategy &s) const { return contains(m_support.at(s->GetPlayer()), s); } /// Returns true iff this support is a (weak) subset of the specified support bool IsSubsetOf(const StrategySupportProfile &) const; @@ -124,7 +124,7 @@ class StrategySupportProfile { /// Add a strategy to the support. void AddStrategy(const GameStrategy &); - /// \brief Removes a strategy from the support + /// @brief Removes a strategy from the support /// /// Removes a strategy from the support. If the strategy is /// not present, or if the strategy is the only strategy for that @@ -140,8 +140,6 @@ class StrategySupportProfile { /// Returns a copy of the support with dominated strategies removed StrategySupportProfile Undominated(bool p_strict, bool p_external = false) const; - /// Returns a copy of the support with dominated strategies of the specified player removed - StrategySupportProfile Undominated(bool p_strict, const GamePlayer &p_player) const; //@} /// @name Identification of overwhelmed strategies diff --git a/src/games/writer.cc b/src/games/writer.cc index a60bb4ed3..e5f317f6b 100644 --- a/src/games/writer.cc +++ b/src/games/writer.cc @@ -30,8 +30,10 @@ std::string HTMLGameWriter::Write(const Game &p_game, int p_rowPlayer, int p_col std::string theHtml; theHtml += "

" + p_game->GetTitle() + "

\n"; - for (StrategyProfileIterator iter(p_game, p_rowPlayer, 1, p_colPlayer, 1); !iter.AtEnd(); - iter++) { + for (StrategyProfileIterator iter(p_game, + {p_game->GetPlayer(p_rowPlayer)->GetStrategies().front(), + p_game->GetPlayer(p_colPlayer)->GetStrategies().front()}); + !iter.AtEnd(); iter++) { if (p_game->NumPlayers() > 2) { theHtml += "
Subtable with strategies:
"; for (int pl = 1; pl <= p_game->NumPlayers(); pl++) { @@ -97,8 +99,10 @@ std::string LaTeXGameWriter::Write(const Game &p_game, int p_rowPlayer, int p_co { std::string theHtml; - for (StrategyProfileIterator iter(p_game, p_rowPlayer, 1, p_colPlayer, 1); !iter.AtEnd(); - iter++) { + for (StrategyProfileIterator iter(p_game, + {p_game->GetPlayer(p_rowPlayer)->GetStrategies().front(), + p_game->GetPlayer(p_colPlayer)->GetStrategies().front()}); + !iter.AtEnd(); iter++) { theHtml += "\\begin{game}{"; theHtml += lexical_cast(p_game->GetPlayer(p_rowPlayer)->NumStrategies()); theHtml += "}{"; diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index 78652dd23..d4ba9e534 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -194,6 +194,12 @@ gbtRowPlayerWidget::gbtRowPlayerWidget(gbtTableWidget *p_parent, gbtGameDocument wxSheetEventFunction, wxSheetEventFunction(&gbtRowPlayerWidget::OnCellRightClick)))); } +GameStrategy GetStrategy(const StrategySupportProfile &p_profile, int pl, int st) +{ + auto strategies = p_profile.GetStrategies(p_profile.GetGame()->GetPlayer(pl)); + return *std::next(strategies.begin(), st - 1); +} + void gbtRowPlayerWidget::OnCellRightClick(wxSheetEvent &p_event) { if (m_table->NumRowPlayers() == 0 || m_doc->GetGame()->IsTree()) { @@ -207,7 +213,7 @@ void gbtRowPlayerWidget::OnCellRightClick(wxSheetEvent &p_event) int player = m_table->GetRowPlayer(coords.GetCol() + 1); int strat = m_table->RowToStrategy(coords.GetCol() + 1, coords.GetRow()); - m_doc->DoDeleteStrategy(support.GetStrategy(player, strat)); + m_doc->DoDeleteStrategy(GetStrategy(support, player, strat)); } wxString gbtRowPlayerWidget::GetCellValue(const wxSheetCoords &p_coords) @@ -225,7 +231,7 @@ wxString gbtRowPlayerWidget::GetCellValue(const wxSheetCoords &p_coords) int player = m_table->GetRowPlayer(p_coords.GetCol() + 1); int strat = m_table->RowToStrategy(p_coords.GetCol() + 1, p_coords.GetRow()); - return {support.GetStrategy(player, strat)->GetLabel().c_str(), *wxConvCurrent}; + return {GetStrategy(support, player, strat)->GetLabel().c_str(), *wxConvCurrent}; } void gbtRowPlayerWidget::SetCellValue(const wxSheetCoords &p_coords, const wxString &p_value) @@ -235,7 +241,7 @@ void gbtRowPlayerWidget::SetCellValue(const wxSheetCoords &p_coords, const wxStr int player = m_table->GetRowPlayer(p_coords.GetCol() + 1); int strat = m_table->RowToStrategy(p_coords.GetCol() + 1, p_coords.GetRow()); - m_doc->DoSetStrategyLabel(support.GetStrategy(player, strat), p_value); + m_doc->DoSetStrategyLabel(GetStrategy(support, player, strat), p_value); } wxSheetCellAttr gbtRowPlayerWidget::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const @@ -268,7 +274,7 @@ void gbtRowPlayerWidget::DrawCell(wxDC &p_dc, const wxSheetCoords &p_coords) const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); int player = m_table->GetRowPlayer(p_coords.GetCol() + 1); int strat = m_table->RowToStrategy(p_coords.GetCol() + 1, p_coords.GetRow()); - Gambit::GameStrategy strategy = support.GetStrategy(player, strat); + Gambit::GameStrategy strategy = GetStrategy(support, player, strat); if (support.IsDominated(strategy, false)) { wxRect rect = CellToRect(p_coords); @@ -425,7 +431,7 @@ void gbtColPlayerWidget::OnCellRightClick(wxSheetEvent &p_event) int player = m_table->GetColPlayer(coords.GetRow() + 1); int strat = m_table->RowToStrategy(coords.GetRow() + 1, coords.GetCol()); - m_doc->DoDeleteStrategy(support.GetStrategy(player, strat)); + m_doc->DoDeleteStrategy(GetStrategy(support, player, strat)); } void gbtColPlayerWidget::OnUpdate() @@ -481,7 +487,7 @@ wxString gbtColPlayerWidget::GetCellValue(const wxSheetCoords &p_coords) int player = m_table->GetColPlayer(p_coords.GetRow() + 1); int strat = m_table->ColToStrategy(p_coords.GetRow() + 1, p_coords.GetCol()); - return {support.GetStrategy(player, strat)->GetLabel().c_str(), *wxConvCurrent}; + return {GetStrategy(support, player, strat)->GetLabel().c_str(), *wxConvCurrent}; } void gbtColPlayerWidget::SetCellValue(const wxSheetCoords &p_coords, const wxString &p_value) @@ -491,7 +497,7 @@ void gbtColPlayerWidget::SetCellValue(const wxSheetCoords &p_coords, const wxStr int player = m_table->GetColPlayer(p_coords.GetRow() + 1); int strat = m_table->ColToStrategy(p_coords.GetRow() + 1, p_coords.GetCol()); - m_doc->DoSetStrategyLabel(support.GetStrategy(player, strat), p_value); + m_doc->DoSetStrategyLabel(GetStrategy(support, player, strat), p_value); } wxSheetCellAttr gbtColPlayerWidget::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const @@ -524,7 +530,7 @@ void gbtColPlayerWidget::DrawCell(wxDC &p_dc, const wxSheetCoords &p_coords) const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); int player = m_table->GetColPlayer(p_coords.GetRow() + 1); int strat = m_table->ColToStrategy(p_coords.GetRow() + 1, p_coords.GetCol()); - Gambit::GameStrategy strategy = support.GetStrategy(player, strat); + Gambit::GameStrategy strategy = GetStrategy(support, player, strat); if (support.IsDominated(strategy, false)) { wxRect rect = CellToRect(p_coords); @@ -1071,11 +1077,16 @@ void gbtTableWidget::SetRowPlayer(int index, int pl) OnUpdate(); } +int NumStrategies(const StrategySupportProfile &p_profile, int p_player) +{ + return p_profile.GetStrategies(p_profile.GetGame()->GetPlayer(p_player)).size(); +} + int gbtTableWidget::NumRowContingencies() const { int ncont = 1; const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); - for (int i = 1; i <= NumRowPlayers(); ncont *= support.NumStrategies(GetRowPlayer(i++))) + for (int i = 1; i <= NumRowPlayers(); ncont *= NumStrategies(support, GetRowPlayer(i++))) ; return ncont; } @@ -1084,7 +1095,7 @@ int gbtTableWidget::NumRowsSpanned(int index) const { int ncont = 1; const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); - for (int i = index + 1; i <= NumRowPlayers(); ncont *= support.NumStrategies(GetRowPlayer(i++))) + for (int i = index + 1; i <= NumRowPlayers(); ncont *= NumStrategies(support, GetRowPlayer(i++))) ; return ncont; } @@ -1092,7 +1103,7 @@ int gbtTableWidget::NumRowsSpanned(int index) const int gbtTableWidget::RowToStrategy(int player, int row) const { int strat = row / NumRowsSpanned(player); - return (strat % m_doc->GetNfgSupport().NumStrategies(GetRowPlayer(player)) + 1); + return (strat % NumStrategies(m_doc->GetNfgSupport(), GetRowPlayer(player)) + 1); } void gbtTableWidget::SetColPlayer(int index, int pl) @@ -1118,7 +1129,7 @@ int gbtTableWidget::NumColContingencies() const { int ncont = 1; const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); - for (int i = 1; i <= NumColPlayers(); ncont *= support.NumStrategies(GetColPlayer(i++))) + for (int i = 1; i <= NumColPlayers(); ncont *= NumStrategies(support, GetColPlayer(i++))) ; return ncont; } @@ -1127,7 +1138,7 @@ int gbtTableWidget::NumColsSpanned(int index) const { int ncont = 1; const Gambit::StrategySupportProfile &support = m_doc->GetNfgSupport(); - for (int i = index + 1; i <= NumColPlayers(); ncont *= support.NumStrategies(GetColPlayer(i++))) + for (int i = index + 1; i <= NumColPlayers(); ncont *= NumStrategies(support, GetColPlayer(i++))) ; return ncont; } @@ -1135,7 +1146,7 @@ int gbtTableWidget::NumColsSpanned(int index) const int gbtTableWidget::ColToStrategy(int player, int col) const { int strat = col / m_doc->NumPlayers() / NumColsSpanned(player); - return (strat % m_doc->GetNfgSupport().NumStrategies(GetColPlayer(player)) + 1); + return (strat % NumStrategies(m_doc->GetNfgSupport(), GetColPlayer(player)) + 1); } Gambit::PureStrategyProfile gbtTableWidget::CellToProfile(const wxSheetCoords &p_coords) const @@ -1145,12 +1156,12 @@ Gambit::PureStrategyProfile gbtTableWidget::CellToProfile(const wxSheetCoords &p Gambit::PureStrategyProfile profile = m_doc->GetGame()->NewPureStrategyProfile(); for (int i = 1; i <= NumRowPlayers(); i++) { int player = GetRowPlayer(i); - profile->SetStrategy(support.GetStrategy(player, RowToStrategy(i, p_coords.GetRow()))); + profile->SetStrategy(GetStrategy(support, player, RowToStrategy(i, p_coords.GetRow()))); } for (int i = 1; i <= NumColPlayers(); i++) { int player = GetColPlayer(i); - profile->SetStrategy(support.GetStrategy(player, ColToStrategy(i, p_coords.GetCol()))); + profile->SetStrategy(GetStrategy(support, player, ColToStrategy(i, p_coords.GetCol()))); } return profile; diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 8aaa6d2fc..265eb682c 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -26,10 +26,18 @@ cdef extern from "games/number.h": cdef extern from "core/array.h": cdef cppclass Array[T]: + cppclass iterator: + T operator*() + iterator operator++() + bint operator==(iterator) + bint operator!=(iterator) T getitem "operator[]"(int) except + int Length() except + Array() except + Array(int) except + + iterator begin() except + + iterator end() except + + cdef extern from "core/list.h": cdef cppclass c_List "List"[T]: @@ -188,6 +196,7 @@ cdef extern from "games/game.h": int NumPlayers() except + c_GamePlayer GetPlayer(int) except +IndexError + Array[c_GamePlayer] GetPlayers() except + c_GamePlayer GetChance() except + c_GamePlayer NewPlayer() except + @@ -331,6 +340,17 @@ cdef extern from "games/behavmixed.h": cdef extern from "games/stratspt.h": cdef cppclass c_StrategySupportProfile "StrategySupportProfile": + cppclass Support: + cppclass const_iterator: + c_GameStrategy operator *() + const_iterator operator++() + bint operator ==(const_iterator) + bint operator !=(const_iterator) + Support() + int size() + const_iterator begin() except + + const_iterator end() except + + c_StrategySupportProfile(c_Game) except + c_StrategySupportProfile(c_StrategySupportProfile) except + bool operator ==(c_StrategySupportProfile) except + @@ -339,10 +359,9 @@ cdef extern from "games/stratspt.h": Array[int] NumStrategies() except + int MixedProfileLength() except + int GetIndex(c_GameStrategy) except + - int NumStrategiesPlayer "NumStrategies"(int) except +IndexError bool IsSubsetOf(c_StrategySupportProfile) except + bool RemoveStrategy(c_GameStrategy) except + - c_GameStrategy GetStrategy(int, int) except +IndexError + Support GetStrategies(c_GamePlayer) except + bool Contains(c_GameStrategy) except + bool IsDominated(c_GameStrategy, bool, bool) except + c_StrategySupportProfile Undominated(bool, bool) # except + doesn't compile diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 8b116f62f..ae26e5600 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -952,8 +952,30 @@ class Game: profile[action] = Rational(hi - lo - 1, denom) return profile - def support_profile(self): - return StrategySupportProfile(self) + def strategy_support_profile( + self, strategies: typing.Callable | None = None + ) -> StrategySupportProfile: + """Create a new `StrategySupportProfile` on the game. + + Parameters + ---------- + strategies : function, optional + By default the support profile contains all strategies for all players. + If specified, only strategies for which the supplied function returns `True` + are included. + + Returns + ------- + StrategySupportProfile + """ + profile = StrategySupportProfile(self) + if strategies is not None: + for strategy in self.strategies: + if not strategies(strategy): + if not (deref(profile.support) + .RemoveStrategy(cython.cast(Strategy, strategy).strategy)): + raise ValueError("attempted to remove the last strategy for player") + return profile def nodes( self, diff --git a/src/pygambit/nashphc.py b/src/pygambit/nashphc.py index 98e36f332..2ef9ed959 100644 --- a/src/pygambit/nashphc.py +++ b/src/pygambit/nashphc.py @@ -136,17 +136,13 @@ def _contingencies( yield list(profile) -def _get_strategies(support: gbt.StrategySupportProfile, player: gbt.Player) -> list[gbt.Strategy]: - return [s for s in player.strategies if s in support] - - def _equilibrium_equations(support: gbt.StrategySupportProfile, player: gbt.Player) -> list: """Generate the equations that the strategy of `player` must satisfy in any totally-mixed equilibrium on `support`. """ payoffs = {strategy: [] for strategy in player.strategies if strategy in support} - strategies = _get_strategies(support, player) + strategies = list(support[player]) for profile in _contingencies(support, player): contingency = "*".join(f"{_playerletters[strat.player.number]}{strat.number}" for strat in profile if strat is not None) @@ -207,8 +203,7 @@ def _profile_from_support(support: gbt.StrategySupportProfile) -> gbt.MixedStrat profile = support.game.mixed_strategy_profile() for player in support.game.players: for strategy in player.strategies: - profile[strategy] = 0.0 - profile[_get_strategies(support, player)[0]] = 1.0 + profile[strategy] = 1.0 if strategy in support else 0.0 return profile @@ -254,7 +249,7 @@ def phcpack_solve(game: gbt.Game, phcpack_path: pathlib.Path | str, def main(): game = gbt.Game.parse_game(sys.stdin.read()) - phcpack_solve(game, maxregret=1.0e-6) + phcpack_solve(game, "./phc", maxregret=1.0e-6) if __name__ == "__main__": diff --git a/src/pygambit/stratspt.pxi b/src/pygambit/stratspt.pxi index 28ce63a63..dc2986c87 100644 --- a/src/pygambit/stratspt.pxi +++ b/src/pygambit/stratspt.pxi @@ -3,7 +3,7 @@ # Copyright (c) 1994-2024, The Gambit Project (http://www.gambit-project.org) # # FILE: src/pygambit/stratspt.pxi -# Cython wrapper for strategy supports +# Cython wrapper for strategy support profiles # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +24,28 @@ from cython.operator cimport dereference as deref from libcpp.memory cimport unique_ptr +@cython.cclass +class StrategySupport: + """A set of strategies for a specified player in a `StrategySupportProfile`. + """ + _profile = cython.declare(StrategySupportProfile) + _player = cython.declare(Player) + + def __init__(self, profile: StrategySupportProfile, player: Player) -> None: + self._profile = profile + self._player = player + + @property + def player(self) -> Player: + return self._player + + def __iter__(self) -> typing.Generator[Strategy, None, None]: + for strat in deref(self._profile.support).GetStrategies(self._player.player): + s = Strategy() + s.strategy = strat + yield s + + @cython.cclass class StrategySupportProfile: """A set-like object representing a subset of the strategies in game. @@ -32,20 +54,8 @@ class StrategySupportProfile: """ support = cython.declare(unique_ptr[c_StrategySupportProfile]) - def __init__(self, - game: Game, - strategies: typing.Optional[typing.Iterable[Strategy]] = None): - if (strategies is not None and - len(set([strat.player.number for strat in strategies])) != len(game.players)): - raise ValueError( - "A StrategySupportProfile must have at least one strategy for each player" - ) - # There's at least one strategy for each player, so this forms a valid support profile + def __init__(self, game: Game) -> None: self.support.reset(new c_StrategySupportProfile(game.game)) - if strategies is not None: - for strategy in game.strategies: - if strategy not in strategies: - deref(self.support).RemoveStrategy(cython.cast(Strategy, strategy).strategy) @property def game(self) -> Game: @@ -73,15 +83,6 @@ class StrategySupportProfile: def __ge__(self, other: StrategySupportProfile) -> bool: return self.issuperset(other) - def __getitem__(self, index: int) -> Strategy: - for pl in range(len(self.game.players)): - if index < deref(self.support).NumStrategiesPlayer(pl+1): - s = Strategy() - s.strategy = deref(self.support).GetStrategy(pl+1, index+1) - return s - index = index - deref(self.support).NumStrategiesPlayer(pl+1) - raise IndexError("StrategySupportProfile index out of range") - def __contains__(self, strategy: Strategy) -> bool: if strategy not in self.game.strategies: raise MismatchError( @@ -90,19 +91,56 @@ class StrategySupportProfile: return deref(self.support).Contains(strategy.strategy) def __iter__(self) -> typing.Generator[Strategy, None, None]: - for pl in range(len(self.game.players)): - for st in range(deref(self.support).NumStrategiesPlayer(pl+1)): + for player in deref(self.support).GetGame().deref().GetPlayers(): + for strat in deref(self.support).GetStrategies(player): s = Strategy() - s.strategy = deref(self.support).GetStrategy(pl+1, st+1) + s.strategy = strat yield s + def __getitem__(self, player: PlayerReference) -> StrategySupport: + """Return a `StrategySupport` representing the strategies in the support + belonging to `player`. + + Parameters + ---------- + player : Player + The player to extract the support for + + Raises + ------ + MismatchError + If `player` is a `Player` from a different game. + """ + return StrategySupport( + self, + cython.cast(Player, self.game._resolve_player(player, "__getitem__")) + ) + def __and__(self, other: StrategySupportProfile) -> StrategySupportProfile: + """Operator version of set intersection on support profiles. + + See also + -------- + intersection + """ return self.intersection(other) def __or__(self, other: StrategySupportProfile) -> StrategySupportProfile: + """Operator version of set union on support profiles. + + See also + -------- + union + """ return self.union(other) def __sub__(self, other: StrategySupportProfile) -> StrategySupportProfile: + """Operator version of set difference on support profiles. + + See also + -------- + difference + """ return self.difference(other) def remove(self, strategy: Strategy) -> StrategySupportProfile: @@ -122,13 +160,15 @@ class StrategySupportProfile: raise MismatchError( "remove(): strategy is not part of the game on which the profile is defined." ) - if deref(self.support).NumStrategiesPlayer(strategy.player.number + 1) == 1: + if deref(self.support).GetStrategies( + cython.cast(Player, strategy.player).player + ).size() == 1: raise UndefinedOperationError( "remove(): cannot remove last strategy of a player" ) strategies = list(self) strategies.remove(strategy) - return StrategySupportProfile(self.game, strategies) + return self.game.strategy_support_profile(lambda x: x in strategies) def difference(self, other: StrategySupportProfile) -> StrategySupportProfile: """Create a support profile which contains all strategies in this profile that @@ -151,7 +191,7 @@ class StrategySupportProfile: """ if self.game != other.game: raise MismatchError("difference(): support profiles are defined on different games") - return StrategySupportProfile(self.game, set(self) - set(other)) + return self.game.strategy_support_profile(lambda x: x in set(self) - set(other)) def intersection(self, other: StrategySupportProfile) -> StrategySupportProfile: """Create a support profile which contains all strategies that are in both this and @@ -174,7 +214,7 @@ class StrategySupportProfile: """ if self.game != other.game: raise MismatchError("intersection(): support profiles are defined on different games") - return StrategySupportProfile(self.game, set(self) & set(other)) + return self.game.strategy_support_profile(lambda x: x in set(self) & set(other)) def union(self, other: StrategySupportProfile) -> StrategySupportProfile: """Create a support profile which contains all strategies that are in either this or @@ -197,7 +237,7 @@ class StrategySupportProfile: """ if self.game != other.game: raise MismatchError("union(): support profiles are defined on different games") - return StrategySupportProfile(self.game, set(self) | set(other)) + return self.game.strategy_support_profile(lambda x: x in set(self) | set(other)) def issubset(self, other: StrategySupportProfile) -> bool: """Test for whether this support is contained in another. diff --git a/src/pygambit/supports.py b/src/pygambit/supports.py index f093d4cac..a609e70db 100644 --- a/src/pygambit/supports.py +++ b/src/pygambit/supports.py @@ -20,12 +20,14 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # +from __future__ import annotations + import pygambit as gbt import pygambit.gambit as libgbt def undominated_strategies_solve( - profile: gbt.StrategySupportProfile, + profile: gbt.Game | gbt.StrategySupportProfile, strict: bool = False, external: bool = False ) -> gbt.StrategySupportProfile: @@ -36,8 +38,9 @@ def undominated_strategies_solve( Parameters ---------- - profile: StrategySupportProfile - The initial profile of strategies + profile : Game or StrategySupportProfile + The initial profile of strategies. If a `Game` is passed, elimination begins with + the full set of strategies on the game. strict : bool, default False If specified `True`, eliminate only strategies which are strictly dominated. @@ -53,4 +56,6 @@ def undominated_strategies_solve( StrategySupportProfile A new support profile containing only the strategies which are not dominated. """ + if isinstance(profile, gbt.Game): + profile = profile.strategy_support_profile() return libgbt._undominated_strategies_solve(profile, strict, external) diff --git a/src/solvers/enumpoly/nfgpoly.cc b/src/solvers/enumpoly/nfgpoly.cc index e725eac4b..ae14331f7 100644 --- a/src/solvers/enumpoly/nfgpoly.cc +++ b/src/solvers/enumpoly/nfgpoly.cc @@ -67,7 +67,7 @@ gPoly IndifferenceEquation(const gSpace &space, const term_order &lex, { gPoly equation(&space, &lex); - for (StrategyProfileIterator A(support, s1), B(support, s2); !A.AtEnd(); A++, B++) { + for (StrategyProfileIterator A(support, {s1}), B(support, {s2}); !A.AtEnd(); A++, B++) { gPoly term(&space, 1, &lex); for (auto player : support.GetGame()->GetPlayers()) { if (player != s1->GetPlayer()) { diff --git a/src/solvers/nashsupport/nfgsupport.cc b/src/solvers/nashsupport/nfgsupport.cc index 5c7632d49..f5260a5be 100644 --- a/src/solvers/nashsupport/nfgsupport.cc +++ b/src/solvers/nashsupport/nfgsupport.cc @@ -84,11 +84,6 @@ class CartesianRange { iterator end() { return {m_sizes, true}; } }; -template bool contains(const C &p_container, const T &p_value) -{ - return std::find(p_container.cbegin(), p_container.cend(), p_value) != p_container.cend(); -} - StrategySupportProfile RestrictedGame(const Game &game, const GamePlayer &player, std::map &domainStrategies) { diff --git a/tests/test_stratprofiles.py b/tests/test_stratprofiles.py index 34785949e..1e20026bc 100644 --- a/tests/test_stratprofiles.py +++ b/tests/test_stratprofiles.py @@ -7,8 +7,8 @@ def test_remove_strategy(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strategy = support_profile[0] + support_profile = game.strategy_support_profile() + strategy = game.players["Player 1"].strategies["1"] new_profile = support_profile.remove(strategy) assert len(support_profile) == len(new_profile) + 1 assert strategy not in new_profile @@ -16,76 +16,65 @@ def test_remove_strategy(): def test_difference(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strat_list = [support_profile[0], support_profile[4]] - dif_profile = gbt.StrategySupportProfile(game, strat_list) + support_profile = game.strategy_support_profile() + strategies = [p.strategies["1"] for p in game.players] + dif_profile = game.strategy_support_profile(lambda x: x in strategies) new_profile = support_profile - dif_profile assert len(new_profile) == 3 - for strategy in strat_list: + for strategy in strategies: assert strategy not in new_profile def test_difference_error(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strat_list = [support_profile[0], support_profile[4]] - dif_profile = gbt.StrategySupportProfile(game, strat_list) + support_profile = game.strategy_support_profile() with pytest.raises(ValueError): - dif_profile - support_profile + game.strategy_support_profile(lambda x: x.label == "1") - support_profile def test_intersection(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strat_list = [support_profile[0], support_profile[2], - support_profile[4]] - fir_profile = gbt.StrategySupportProfile(game, strat_list) - sec_profile = support_profile.remove(support_profile[2]) - new_profile = fir_profile & sec_profile - assert len(new_profile) == 2 - assert new_profile <= sec_profile - assert new_profile <= fir_profile + first_profile = game.strategy_support_profile(lambda x: x.label not in ["1", "3"]) + second_profile = game.strategy_support_profile(lambda x: x.label != "3") + intersect_profile = first_profile & second_profile + assert len(intersect_profile) == 2 + assert intersect_profile <= first_profile + assert intersect_profile <= second_profile def test_intersection_error(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strat_list = [support_profile[0], support_profile[2], - support_profile[4]] - fir_profile = gbt.StrategySupportProfile(game, strat_list) - sec_profile = support_profile.remove(support_profile[4]) + first_profile = game.strategy_support_profile(lambda x: x.label not in ["1", "3"]) + second_profile = game.strategy_support_profile( + lambda x: x.player.label == "Player 1" or x.label == "1" + ) with pytest.raises(ValueError): - fir_profile & sec_profile + first_profile & second_profile def test_union(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - strat_list = [support_profile[0], support_profile[2], - support_profile[4]] - fir_profile = gbt.StrategySupportProfile(game, strat_list) - sec_profile = support_profile.remove(support_profile[4]) - new_profile = fir_profile | sec_profile - assert new_profile == support_profile + first_profile = game.strategy_support_profile(lambda x: x.label not in ["1", "3"]) + second_profile = game.strategy_support_profile( + lambda x: x.player.label == "Player 1" or x.label == "1" + ) + assert (first_profile | second_profile) == game.strategy_support_profile() def test_undominated(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - new_profile = support_profile - loop_profile = gbt.supports.undominated_strategies_solve(new_profile) - while loop_profile != new_profile: - new_profile = loop_profile - loop_profile = gbt.supports.undominated_strategies_solve(new_profile) - assert len(loop_profile) == 2 - assert loop_profile == gbt.StrategySupportProfile( - game, [support_profile[0], support_profile[3]] - ) + profile = gbt.supports.undominated_strategies_solve(game) + while True: + new_profile = gbt.supports.undominated_strategies_solve(profile) + if new_profile == profile: + break + profile = new_profile + assert profile == game.strategy_support_profile(lambda x: x.label == "1") def test_remove_error(): game = games.read_from_file("mixed_strategy.nfg") - support_profile = game.support_profile() - profile = support_profile.remove(support_profile[3]) + support_profile = game.strategy_support_profile() + profile = support_profile.remove(game.players["Player 2"].strategies["1"]) with pytest.raises(gbt.UndefinedOperationError): - profile.remove(profile[3]) + profile.remove(game.players["Player 2"].strategies["2"])