Skip to content

Commit

Permalink
Merge branch 'master' into 445-enh-add-starting-point-for-simpdiv_solve
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulsavani committed Apr 2, 2024
2 parents c952a8d + bdc3936 commit b951d9a
Show file tree
Hide file tree
Showing 22 changed files with 347 additions and 93 deletions.
5 changes: 4 additions & 1 deletion ChangeLog
Expand Up @@ -22,6 +22,8 @@
- Additional regret-related functions added to `MixedBehaviorProfile` and `MixedStrategyProfile`
in both C++ and Python
- Some caching added to payoff/strategy value calculations in `MixedStrategyProfile`
- `gambit-simpdiv` now supports expressing output as floating-point with a specified number of
digits (#296)

### Changed
- Gambit now requires a compiler that supports C++17.
Expand All @@ -46,7 +48,8 @@
Creation of random mixed profiles in C++ is done with new `Game::NewRandomStrategyProfile` and
`Game::NewRandomBehaviorProfile` methods; these accept STL `Generator` objects for reproducible state.
The Python implementation is no longer just a wrapper around the C++ one.

- Graphical interface now uses simplicial subdivision as the recommended method for finding some
equilibria in games with more than two players, instead of Lyapunov function minimisation

## [16.1.1] - 2024-01-10

Expand Down
86 changes: 86 additions & 0 deletions doc/pygambit.user.rst
Expand Up @@ -516,6 +516,92 @@ using :py:meth:`.MixedStrategyProfile.as_behavior` and :py:meth:`.MixedBehaviorP
eqm.as_behavior().as_strategy()
.. _pygambit-nash-maxregret:

Acceptance criteria for Nash equilibria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some methods for computing Nash equilibria operate using floating-point arithmetic and/or
generate candidate equilibrium profiles using methods which involve some form of successive
approximations. The outputs of these methods therefore are in general
:math:`\varepsilon`-equilibria, for some positive :math:`\varepsilon`.

To provide a uniform interface across methods, where relevant Gambit provides a parameter
`maxregret`, which specifies the acceptance criterion for labeling the output of the
algorithm as an equilibrium.
This parameter is interpreted *proportionally* to the range of payoffs in the game.
Any profile returned as an equilibrium is guaranteed to be an
:math:`\varepsilon`-equilibrium, for :math:`\varepsilon` no more than `maxregret`
times the difference of the game's maximum and minimum payoffs.

As an example, consider solving the standard one-card poker game using
:py:func:`.logit_solve`. The range of the payoffs in this game is 4 (from +2 to -2).

.. ipython:: python
g = gbt.Game.read_game("poker.efg")
g.max_payoff, g.min_payoff
:py:func:`.logit_solve` is a globally-convergent method, in that it computes a
sequence of profiles which is guaranteed to have a subsequence that converges to a
Nash equilibrium. The default value of `maxregret` for this method is set at
:math:`10^{-8}`:

.. ipython:: python
result = gbt.nash.logit_solve(g, maxregret=1e-8)
result.equilibria
result.equilibria[0].max_regret()
The value of :py:meth:`.MixedBehaviorProfile.max_regret` of the computed profile exceeds
:math:`10^{-8}` measured in payoffs of the game. However, when considered relative
to the scale of the game's payoffs, we see it is less than :math:`10^{-8}` of
the payoff range, as requested:

.. ipython:: python
result.equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)
In general, for globally-convergent methods especially, there is a tradeoff between
precision and running time. Some methods may be slow to converge on some games, and
it may be useful instead to get a more coarse approximation to an equilibrium.
We could instead ask only for an :math:`\varepsilon`-equilibrium with a
(scaled) :math:`\varepsilon` of no more than :math:`10^{-4}`:

.. ipython:: python
result = gbt.nash.logit_solve(g, maxregret=1e-4)
result.equilibria[0]
result.equilibria[0].max_regret()
result.equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)
The convention of expressing `maxregret` scaled by the game's payoffs standardises the
behavior of methods across games. For example, consider solving the poker game instead
using :py:meth:`.liap_solve`.

.. ipython:: python
result = gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)
result.equilibria[0]
result.equilibria[0].max_regret()
result.equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)
If, instead, we double all payoffs, the output of the method is unchanged.

.. ipython:: python
for outcome in g.outcomes:
outcome["Alice"] = outcome["Alice"] * 2
outcome["Bob"] = outcome["Bob"] * 2
result = gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)
result.equilibria[0]
result.equilibria[0].max_regret()
result.equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)
Estimating quantal response equilibria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion doc/tools.liap.rst
Expand Up @@ -47,7 +47,8 @@ not guaranteed to find all, or even any, Nash equilibria.

.. versionadded:: 16.2.0

Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium.
Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium
(default is 1e-4). See :ref:`pygambit-nash-maxregret` for interpretation and guidance.

.. cmdoption:: -h

Expand Down
19 changes: 11 additions & 8 deletions doc/tools.logit.rst
Expand Up @@ -33,6 +33,13 @@ if an information set is not reached due to being the successor of chance
moves with zero probability. In such games, the implementation treats
the beliefs at such information sets as being uniform across all member nodes.

.. versionchanged:: 16.2.0

The criterion for accepting whether a point is sufficiently close to a
Nash equilibrium to terminate the path-following is specified
in terms of the maximum regret. This regret is interpreted as a fraction
of the difference between the maximum and minimum payoffs in the game.

.. program:: gambit-logit

.. cmdoption:: -d
Expand Down Expand Up @@ -61,14 +68,10 @@ the beliefs at such information sets as being uniform across all member nodes.

.. cmdoption:: -m

Stop when reaching the specified value of the
parameter lambda. By default, the tracing stops when lambda reaches
1,000,000, which is usually suitable for computing a good
approximation to a Nash equilibrium. For applications, such as to
laboratory experiments, where the behavior of the correspondence for
small values of lambda is of interest and the asymptotic behavior is
not relevant, setting MAXLAMBDA to a much smaller value may be
indicated.
.. versionadded:: 16.2.0

Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium
(default is 1e-8). See :ref:`pygambit-nash-maxregret` for interpretation and guidance.

.. cmdoption:: -l

Expand Down
14 changes: 13 additions & 1 deletion doc/tools.simpdiv.rst
Expand Up @@ -65,7 +65,19 @@ options to specify additional starting points for the algorithm.

.. versionadded:: 16.2.0

Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium.
Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium
(default is 1e-8). See :ref:`pygambit-nash-maxregret` for interpretation and guidance.

.. cmdoption:: -d DECIMALS

.. versionadded:: 16.2.0

Simplicial subdivision operates on a triangulation grid in the set of mixed strategy profiles.
Therefore, it produces output in which all probabilities are expressed as rational numbers, and
by default the output reports these. By specifying this option, instead probabilities are
expressed as floating-point numbers with the specified number of decimal places. Specifying
this option sacrifices some precision in reporting the output of the method, in exchange for
probabilities which are more human-readable.

.. cmdoption:: -v

Expand Down
12 changes: 6 additions & 6 deletions src/gui/dlnash.cc
Expand Up @@ -49,14 +49,14 @@ gbtNashChoiceDialog::gbtNashChoiceDialog(wxWindow *p_parent, gbtGameDocument *p_

if (m_doc->GetGame()->NumPlayers() == 2) {
wxString countChoices[] = {wxT("Compute one Nash equilibrium"),
wxT("Compute as many Nash equilibria as possible"),
wxT("Compute some Nash equilibria"),
wxT("Compute all Nash equilibria")};
m_countChoice =
new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, countChoices);
}
else {
wxString countChoices[] = {wxT("Compute one Nash equilibrium"),
wxT("Compute as many Nash equilibria as possible")};
wxT("Compute some Nash equilibria")};
m_countChoice =
new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, countChoices);
}
Expand Down Expand Up @@ -213,9 +213,9 @@ gbtAnalysisOutput *gbtNashChoiceDialog::GetCommand() const
wxT("Some equilibria by solving a linear ") wxT("complementarity program ") + game);
}
else {
cmd = new gbtAnalysisProfileList<double>(m_doc, useEfg);
cmd->SetCommand(prefix + wxT("liap -d 10") + options);
cmd->SetDescription(wxT("Some equilibria by function minimization ") + game);
cmd = new gbtAnalysisProfileList<double>(m_doc, false);
cmd->SetCommand(prefix + wxT("simpdiv -d 10 -n 20 -r 100") + options);
cmd->SetDescription(wxT("Some equilibria by simplicial subdivision ") + game);
}
}
else {
Expand Down Expand Up @@ -285,7 +285,7 @@ gbtAnalysisOutput *gbtNashChoiceDialog::GetCommand() const
}
else if (method == s_simpdiv) {
cmd = new gbtAnalysisProfileList<double>(m_doc, false);
cmd->SetCommand(prefix + wxT("simpdiv") + options);
cmd->SetCommand(prefix + wxT("simpdiv -d 10 -n 20 -r 100") + options);
cmd->SetDescription(count + wxT(" by simplicial subdivision ") wxT("in strategic game"));
}
else {
Expand Down
4 changes: 2 additions & 2 deletions src/pygambit/gambit.pxd
Expand Up @@ -456,10 +456,10 @@ cdef extern from "solvers/gnm/gnm.h":
) except +RuntimeError

cdef extern from "solvers/logit/nfglogit.h":
c_List[c_MixedStrategyProfileDouble] LogitStrategySolve(c_Game) except +RuntimeError
c_List[c_MixedStrategyProfileDouble] LogitStrategySolve(c_Game, double) except +RuntimeError

cdef extern from "solvers/logit/efglogit.h":
c_List[c_MixedBehaviorProfileDouble] LogitBehaviorSolve(c_Game) except +RuntimeError
c_List[c_MixedBehaviorProfileDouble] LogitBehaviorSolve(c_Game, double) except +RuntimeError

cdef extern from "solvers/logit/nfglogit.h":
cdef cppclass c_LogitQREMixedStrategyProfile "LogitQREMixedStrategyProfile":
Expand Down
5 changes: 2 additions & 3 deletions src/pygambit/nash.h
Expand Up @@ -53,12 +53,11 @@ std::shared_ptr<LogitQREMixedStrategyProfile> logit_atlambda(const Game &p_game,
alg.SolveAtLambda(start, null_stream, p_lambda, 1.0));
}

List<LogitQREMixedStrategyProfile> logit_principal_branch(const Game &p_game,
double p_maxLambda = 1000000.0)
List<LogitQREMixedStrategyProfile> logit_principal_branch(const Game &p_game, double p_maxregret)
{
LogitQREMixedStrategyProfile start(p_game);
StrategicQREPathTracer alg;
NullBuffer null_buffer;
std::ostream null_stream(&null_buffer);
return alg.TraceStrategicPath(start, null_stream, p_maxLambda, 1.0);
return alg.TraceStrategicPath(start, null_stream, p_maxregret, 1.0);
}
13 changes: 7 additions & 6 deletions src/pygambit/nash.pxi
Expand Up @@ -33,6 +33,7 @@ def _convert_mspd(
) -> typing.List[MixedStrategyProfileDouble]:
ret = []
for i in range(inlist.Length()):
print(i)
p = MixedStrategyProfileDouble()
p.profile = copyitem_list_mspd(inlist, i+1)
ret.append(p)
Expand Down Expand Up @@ -182,12 +183,12 @@ def _gnm_strategy_solve(
raise


def _logit_strategy_solve(game: Game) -> typing.List[MixedStrategyProfileDouble]:
return _convert_mspd(LogitStrategySolve(game.game))
def _logit_strategy_solve(game: Game, maxregret: float) -> typing.List[MixedStrategyProfileDouble]:
return _convert_mspd(LogitStrategySolve(game.game, maxregret))


def _logit_behavior_solve(game: Game) -> typing.List[MixedBehaviorProfileDouble]:
return _convert_mbpd(LogitBehaviorSolve(game.game))
def _logit_behavior_solve(game: Game, maxregret: float) -> typing.List[MixedBehaviorProfileDouble]:
return _convert_mbpd(LogitBehaviorSolve(game.game, maxregret))


@cython.cclass
Expand Down Expand Up @@ -254,8 +255,8 @@ def logit_atlambda(game: Game, lam: float) -> LogitQREMixedStrategyProfile:
return ret


def logit_principal_branch(game: Game, maxlam: float = 100000.0):
solns = _logit_principal_branch(game.game, maxlam)
def logit_principal_branch(game: Game):
solns = _logit_principal_branch(game.game, 1.0e-8)
ret = []
for i in range(solns.Length()):
p = LogitQREMixedStrategyProfile()
Expand Down
30 changes: 23 additions & 7 deletions src/pygambit/nash.py
Expand Up @@ -249,7 +249,7 @@ def lp_solve(
def liap_solve(
start: typing.Union[libgbt.MixedStrategyProfileDouble,
libgbt.MixedBehaviorProfileDouble],
maxregret: float = 0.001,
maxregret: float = 1.0e-4,
maxiter: int = 1000
) -> NashComputationResult:
"""Compute approximate Nash equilibria of a game using
Expand All @@ -266,7 +266,7 @@ def liap_solve(
start : MixedStrategyProfileDouble or MixedBehaviorProfileDouble
The starting point for function minimization.
maxregret : float, default 0.001
maxregret : float, default 1e-4
The acceptance criterion for approximate Nash equilibrium; the maximum
regret of any player must be no more than `maxregret` times the
difference of the maximum and minimum payoffs of the game
Expand All @@ -283,6 +283,8 @@ def liap_solve(
res : NashComputationResult
The result represented as a ``NashComputationResult`` object.
"""
if maxregret <= 0.0:
raise ValueError("liap_solve(): maxregret argument must be positive")
if isinstance(start, libgbt.MixedStrategyProfileDouble):
equilibria = libgbt._liap_strategy_solve(start,
maxregret=maxregret, maxiter=maxiter)
Expand Down Expand Up @@ -318,7 +320,7 @@ def simpdiv_solve(
start: MixedStrategyProfileRational
The starting profile of the game to compute equilibria in.
maxregret : Rational, default 1/1000
maxregret : Rational, default 1e-8
The acceptance criterion for approximate Nash equilibrium; the maximum
regret of any player must be no more than `maxregret` times the
difference of the maximum and minimum payoffs of the game
Expand Down Expand Up @@ -346,7 +348,9 @@ def simpdiv_solve(
if leash is not None and (not isinstance(leash, int) or leash <= 0):
raise ValueError("simpdiv_solve(): leash must be a non-negative integer")
if maxregret is None:
maxregret = libgbt.Rational(1, 1000)
maxregret = libgbt.Rational(1, 10000000)
elif maxregret < libgbt.Rational(0):
raise ValueError("simpdiv_solve(): maxregret must be positive")
equilibria = libgbt._simpdiv_strategy_solve(start, maxregret, refine, leash or 0)
return NashComputationResult(
game=start.game,
Expand Down Expand Up @@ -516,7 +520,9 @@ def gnm_solve(


def logit_solve(
game: libgbt.Game, use_strategic: bool = False
game: libgbt.Game,
use_strategic: bool = False,
maxregret: float = 1.0e-8,
) -> NashComputationResult:
"""Compute Nash equilibria of a game using :ref:`the logit quantal response
equilibrium correspondence <gambit-logit>`.
Expand All @@ -528,19 +534,29 @@ def logit_solve(
----------
game : Game
The game to compute equilibria in.
use_strategic : bool, default False
Whether to use the strategic form. If True, always uses the strategic
representation even if the game's native representation is extensive.
maxregret : float, default 1e-8
The acceptance criterion for approximate Nash equilibrium; the maximum
regret of any player must be no more than `maxregret` times the
difference of the maximum and minimum payoffs of the game
.. versionadded: 16.2.0
Returns
-------
res : NashComputationResult
The result represented as a ``NashComputationResult`` object.
"""
if maxregret <= 0.0:
raise ValueError("logit_solve(): maxregret argument must be positive")
if not game.is_tree or use_strategic:
equilibria = libgbt._logit_strategy_solve(game)
equilibria = libgbt._logit_strategy_solve(game, maxregret)
else:
equilibria = libgbt._logit_behavior_solve(game)
equilibria = libgbt._logit_behavior_solve(game, maxregret)
return NashComputationResult(
game=game,
method="logit",
Expand Down
2 changes: 1 addition & 1 deletion src/solvers/liap/efgliap.cc
Expand Up @@ -156,7 +156,7 @@ List<MixedBehaviorProfile<double>> LiapBehaviorSolve(const MixedBehaviorProfile<
ConjugatePRMinimizer minimizer(p.BehaviorProfileLength());
Vector<double> gradient(p.BehaviorProfileLength()), dx(p.BehaviorProfileLength());
double fval;
minimizer.Set(F, static_cast<const Vector<double> &>(p), fval, gradient, .01, .0001);
minimizer.Set(F, static_cast<const Vector<double> &>(p), fval, gradient, .001, .00001);

for (int iter = 1; iter <= p_maxitsN; iter++) {
Vector<double> point(p);
Expand Down
2 changes: 1 addition & 1 deletion src/solvers/liap/nfgliap.cc
Expand Up @@ -165,7 +165,7 @@ List<MixedStrategyProfile<double>> LiapStrategySolve(const MixedStrategyProfile<
ConjugatePRMinimizer minimizer(p.MixedProfileLength());
Vector<double> gradient(p.MixedProfileLength()), dx(p.MixedProfileLength());
double fval;
minimizer.Set(F, static_cast<const Vector<double> &>(p), fval, gradient, .01, .0001);
minimizer.Set(F, static_cast<const Vector<double> &>(p), fval, gradient, .001, .00001);

for (int iter = 1; iter <= p_maxitsN; iter++) {
Vector<double> point(p);
Expand Down

0 comments on commit b951d9a

Please sign in to comment.