Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/pygambit/cli/enummixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,15 @@ def render(profile) -> None:
game,
rational=rational,
nash_callback=render,
cliques=cliques,
components=cliques,
)
if cliques:
for index, clique in enumerate(result.cliques, start=1):
polytopes = [
polytope for component in result.components for polytope in component.polytopes
]
for index, polytope in enumerate(polytopes, start=1):
label = f"convex-{index}"
for profile in clique:
for profile in polytope.vertices:
click.echo(render_profile_csv(profile, label, decimals or 0))


Expand Down
5 changes: 3 additions & 2 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,9 @@ cdef extern from "solvers/logit/logit.h":

cdef extern from "nash.h":
pair[
c_EnumMixedStrategyResult[T], stdlist[stdlist[c_MixedStrategyProfile[T]]]
] EnumMixedStrategySolveCliquesWrapper[T](
c_EnumMixedStrategyResult[T],
stdlist[stdlist[stdlist[c_MixedStrategyProfile[T]]]]
] EnumMixedStrategySolveComponentsWrapper[T](
c_Game, StrategyCallbackType[T]
) except +RuntimeError
stdlist[c_LogitQREMixedBehaviorProfile] LogitBehaviorPrincipalBranchWrapper(
Expand Down
17 changes: 11 additions & 6 deletions src/pygambit/nash.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,23 @@ using namespace std;
using namespace Gambit;

template <class T>
std::pair<Nash::EnumMixedStrategyResult<T>, std::list<std::list<MixedStrategyProfile<T>>>>
EnumMixedStrategySolveCliquesWrapper(
std::pair<Nash::EnumMixedStrategyResult<T>,
std::list<std::list<std::list<MixedStrategyProfile<T>>>>>
EnumMixedStrategySolveComponentsWrapper(
const Game &p_game,
Nash::StrategyCallbackType<T> p_onEquilibrium = Nash::NullStrategyCallback<T>)
{
auto solution = Nash::EnumMixedStrategySolveDetailed<T>(p_game, p_onEquilibrium);
std::list<std::list<MixedStrategyProfile<T>>> cliques;
for (auto &clique : solution->GetCliques()) {
cliques.emplace_back(clique.begin(), clique.end());
std::list<std::list<std::list<MixedStrategyProfile<T>>>> components;
for (auto &component : solution->GetComponents()) {
std::list<std::list<MixedStrategyProfile<T>>> polytopes;
for (auto &polytope : component) {
polytopes.emplace_back(polytope.begin(), polytope.end());
}
components.emplace_back(std::move(polytopes));
}
return {Nash::EnumMixedStrategyResult<T>{solution->GetExtremeEquilibria(), solution->success},
cliques};
components};
}

inline std::list<LogitQREMixedBehaviorProfile>
Expand Down
72 changes: 60 additions & 12 deletions src/pygambit/nash.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,43 @@ MixedStrategyEquilibriumSet = list[MixedStrategyProfile]
MixedBehaviorEquilibriumSet = list[MixedBehaviorProfile]


@dataclasses.dataclass(frozen=True)
class NashPolytope:
"""A maximal convex subset of a two-player game's set of Nash equilibria.

Attributes
----------
vertices : MixedStrategyEquilibriumSet
The extreme equilibria which are the vertices of this polytope.
"""
vertices: MixedStrategyEquilibriumSet


@dataclasses.dataclass(frozen=True)
class NashComponent:
"""A connected component of a two-player game's set of Nash equilibria.

A component consists of one or more `NashPolytope` which touch one
another at a shared vertex; the component itself need not be convex.

Attributes
----------
polytopes : list of NashPolytope
The maximal convex subsets making up this component.
"""
polytopes: list[NashPolytope]

@property
def vertices(self) -> MixedStrategyEquilibriumSet:
"""The union of vertices across this component's polytopes."""
result: MixedStrategyEquilibriumSet = []
for polytope in self.polytopes:
for vertex in polytope.vertices:
if vertex not in result:
result.append(vertex)
return result


@dataclasses.dataclass(frozen=True, kw_only=True)
class NashResultBase:
"""Common attributes shared by every result of a method which computes Nash
Expand Down Expand Up @@ -238,14 +275,14 @@ class EnumMixedResult(NashResultBase):
Always True; enumeration always completes.
lrsnash_path : pathlib.Path or str, optional
Set if `lrsnash` was used to solve the systems of equations.
cliques : list of MixedStrategyEquilibriumSet, optional
Set if `cliques=True` was requested: the sets of extreme equilibria which
are connected to one another.
components : list of NashComponent, optional
Set if `components=True` was requested: the connected components of the
set of extreme equilibria.
"""
equilibria: MixedStrategyEquilibriumSet
success: bool
lrsnash_path: pathlib.Path | str | None = None
cliques: list[MixedStrategyEquilibriumSet] | None = None
components: list[NashComponent] | None = None


@dataclasses.dataclass(frozen=True, kw_only=True)
Expand Down Expand Up @@ -885,36 +922,47 @@ def _enummixed_strategy_solve_rational(
)


def _enummixed_strategy_solve_cliques_double(
def _enummixed_strategy_solve_components_double(
game: Game, nash_callback: object = None
) -> EnumMixedResult:
result: pair[
c_EnumMixedStrategyResult[float], stdlist[stdlist[c_MixedStrategyProfile[float]]]
] = EnumMixedStrategySolveCliquesWrapper[double](
c_EnumMixedStrategyResult[float],
stdlist[stdlist[stdlist[c_MixedStrategyProfile[float]]]]
] = EnumMixedStrategySolveComponentsWrapper[double](
game.game, MakeStrategyCallback[double](nash_callback)
)
return EnumMixedResult(
game=game, rational=False, use_strategic=True,
equilibria=_convert_mspd(result.first.equilibria),
success=result.first.success,
cliques=[_convert_mspd(clique) for clique in result.second],
components=[
NashComponent(polytopes=[
NashPolytope(vertices=_convert_mspd(polytope)) for polytope in component
])
for component in result.second
],
)


def _enummixed_strategy_solve_cliques_rational(
def _enummixed_strategy_solve_components_rational(
game: Game, nash_callback: object = None
) -> EnumMixedResult:
result: pair[
c_EnumMixedStrategyResult[c_Rational],
stdlist[stdlist[c_MixedStrategyProfile[c_Rational]]]
] = EnumMixedStrategySolveCliquesWrapper[c_Rational](
stdlist[stdlist[stdlist[c_MixedStrategyProfile[c_Rational]]]]
] = EnumMixedStrategySolveComponentsWrapper[c_Rational](
game.game, MakeStrategyCallback[c_Rational](nash_callback)
)
return EnumMixedResult(
game=game, rational=True, use_strategic=True,
equilibria=_convert_mspr(result.first.equilibria),
success=result.first.success,
cliques=[_convert_mspr(clique) for clique in result.second],
components=[
NashComponent(polytopes=[
NashPolytope(vertices=_convert_mspr(polytope)) for polytope in component
])
for component in result.second
],
)


Expand Down
24 changes: 13 additions & 11 deletions src/pygambit/nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
LpResult,
MixedBehaviorEquilibriumSet,
MixedStrategyEquilibriumSet,
NashComponent,
NashPolytope,
NashResultBase,
SimpdivResult,
)
Expand Down Expand Up @@ -178,7 +180,7 @@ def enummixed_solve(
nash_callback: Callable[
[libgbt.MixedStrategyProfile], None
] | None = None,
cliques: bool = False,
components: bool = False,
) -> EnumMixedResult:
"""Compute all :ref:`mixed-strategy Nash equilibria <enummixed>`
of a two-player game using the strategic representation.
Expand All @@ -205,10 +207,10 @@ def enummixed_solve(

.. versionadded:: 17.0.0

cliques : bool, default False
If specified and True, also compute the sets of extreme equilibria which
are connected to one another, returned as `res.cliques`. Not available
when `lrsnash_path` is specified.
components : bool, default False
If specified and True, also compute the connected components of the set of
extreme equilibria, returned as `res.components`. Not available when
`lrsnash_path` is specified.

.. versionadded:: 17.0.0

Expand All @@ -224,7 +226,7 @@ def enummixed_solve(

ValueError
If both `lrsnash_path` and `nash_callback` are specified, or both
`lrsnash_path` and `cliques` are specified.
`lrsnash_path` and `components` are specified.

Notes
-----
Expand All @@ -235,9 +237,9 @@ def enummixed_solve(
raise ValueError(
"enummixed_solve(): nash_callback cannot be used with lrsnash_path"
)
if cliques:
if components:
raise ValueError(
"enummixed_solve(): cliques cannot be used with lrsnash_path"
"enummixed_solve(): components cannot be used with lrsnash_path"
)
equilibria = nashlrs.lrsnash_solve(game, lrsnash_path=lrsnash_path)
return EnumMixedResult(
Expand All @@ -248,10 +250,10 @@ def enummixed_solve(
equilibria=equilibria,
success=True,
)
if cliques:
if components:
if rational:
return libgbt._enummixed_strategy_solve_cliques_rational(game, nash_callback)
return libgbt._enummixed_strategy_solve_cliques_double(game, nash_callback)
return libgbt._enummixed_strategy_solve_components_rational(game, nash_callback)
return libgbt._enummixed_strategy_solve_components_double(game, nash_callback)
if rational:
return libgbt._enummixed_strategy_solve_rational(game, nash_callback)
return libgbt._enummixed_strategy_solve_double(game, nash_callback)
Expand Down
100 changes: 84 additions & 16 deletions src/solvers/enummixed/enummixed.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//

#include <algorithm>
#include <map>
#include <numeric>

#include "games.h"
#include "solvers/linalg/vertenum.h"
#include "solvers/enummixed/enummixed.h"
Expand All @@ -37,8 +41,36 @@ bool EqZero(const double &x)

bool EqZero(const Rational &x) { return x == Rational(0); }

template <class T>
Array<Array<MixedStrategyProfile<T>>> EnumMixedStrategySolution<T>::GetCliques() const
namespace {

int FindRoot(std::vector<int> &p_parent, int p_node)
{
while (p_parent[p_node] != p_node) {
p_parent[p_node] = p_parent[p_parent[p_node]];
p_node = p_parent[p_node];
}
return p_node;
}

void UnionNodes(std::vector<int> &p_parent, int p_node1, int p_node2)
{
const int root1 = FindRoot(p_parent, p_node1);
const int root2 = FindRoot(p_parent, p_node2);
if (root1 != root2) {
p_parent[root1] = root2;
}
}

bool SharesElement(const Array<int> &p_a, const Array<int> &p_b)
{
return std::any_of(p_a.begin(), p_a.end(), [&](const int p_element) {
return std::find(p_b.begin(), p_b.end(), p_element) != p_b.end();
});
}

} // end anonymous namespace

template <class T> void EnumMixedStrategySolution<T>::EnsureCliques() const
{
if (m_cliques1.empty()) {
// Cliques are generated on demand
Expand All @@ -56,25 +88,61 @@ Array<Array<MixedStrategyProfile<T>>> EnumMixedStrategySolution<T>::GetCliques()
m_cliques1 = clique.GetCliques1();
m_cliques2 = clique.GetCliques2();
}
}

Array<Array<MixedStrategyProfile<T>>> solution;
for (size_t cl = 1; cl <= m_cliques1.size(); cl++) {
solution.push_back(Array<MixedStrategyProfile<T>>());
for (size_t i = 1; i <= m_cliques1[cl].size(); i++) {
for (size_t j = 1; j <= m_cliques2[cl].size(); j++) {
MixedStrategyProfile<T> profile(m_game->NewMixedStrategyProfile(static_cast<T>(0)));
template <class T>
Array<MixedStrategyProfile<T>>
EnumMixedStrategySolution<T>::BuildCliqueProfiles(size_t p_clique) const
{
Array<MixedStrategyProfile<T>> profiles;
for (size_t i = 1; i <= m_cliques1[p_clique].size(); i++) {
for (size_t j = 1; j <= m_cliques2[p_clique].size(); j++) {
MixedStrategyProfile<T> profile(m_game->NewMixedStrategyProfile(static_cast<T>(0)));

for (size_t k = 1; k <= m_key1[m_cliques1[cl][i]].size(); k++) {
profile[k] = m_key1[m_cliques1[cl][i]][k];
}
for (size_t k = 1; k <= m_key2[m_cliques2[cl][j]].size(); k++) {
profile[k + m_key1[m_cliques1[cl][i]].size()] = m_key2[m_cliques2[cl][j]][k];
}
solution[cl].push_back(profile);
for (size_t k = 1; k <= m_key1[m_cliques1[p_clique][i]].size(); k++) {
profile[k] = m_key1[m_cliques1[p_clique][i]][k];
}
for (size_t k = 1; k <= m_key2[m_cliques2[p_clique][j]].size(); k++) {
profile[k + m_key1[m_cliques1[p_clique][i]].size()] = m_key2[m_cliques2[p_clique][j]][k];
}
profiles.push_back(profile);
}
}
return solution;
return profiles;
}

template <class T>
Array<Array<Array<MixedStrategyProfile<T>>>> EnumMixedStrategySolution<T>::GetComponents() const
{
EnsureCliques();

const auto numCliques = m_cliques1.size();
std::vector<int> parent(numCliques + 1);
std::iota(parent.begin(), parent.end(), 0);
for (size_t cl = 1; cl <= numCliques; cl++) {
for (size_t other = cl + 1; other <= numCliques; other++) {
if (SharesElement(m_cliques1[cl], m_cliques1[other]) ||
SharesElement(m_cliques2[cl], m_cliques2[other])) {
UnionNodes(parent, static_cast<int>(cl), static_cast<int>(other));
}
}
}

std::map<int, Array<Array<MixedStrategyProfile<T>>>> componentsByRoot;
std::vector<int> rootOrder;
for (size_t cl = 1; cl <= numCliques; cl++) {
const int root = FindRoot(parent, static_cast<int>(cl));
if (componentsByRoot.find(root) == componentsByRoot.end()) {
rootOrder.push_back(root);
}
componentsByRoot[root].push_back(BuildCliqueProfiles(cl));
}

Array<Array<Array<MixedStrategyProfile<T>>>> components;
for (const int root : rootOrder) {
components.push_back(componentsByRoot[root]);
}
return components;
}

template <class T>
Expand Down
8 changes: 7 additions & 1 deletion src/solvers/enummixed/enummixed.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ template <class T> class EnumMixedStrategySolution {
return m_extremeEquilibria;
}

Array<Array<MixedStrategyProfile<T>>> GetCliques() const;
Array<Array<Array<MixedStrategyProfile<T>>>> GetComponents() const;

Game m_game;
std::list<MixedStrategyProfile<T>> m_extremeEquilibria;
Expand All @@ -60,6 +60,12 @@ template <class T> class EnumMixedStrategySolution {
/// Representation of the connectedness of the extreme equilibria
/// These are generated only on demand
mutable Array<Array<int>> m_cliques1, m_cliques2;

private:
/// Populates m_cliques1/m_cliques2 on first use.
void EnsureCliques() const;
/// Builds the vertex profiles for one maximal clique.
Array<MixedStrategyProfile<T>> BuildCliqueProfiles(size_t p_clique) const;
};

template <class T>
Expand Down
Loading
Loading