Skip to content

Commit

Permalink
Document maxregret parameter in user guide.
Browse files Browse the repository at this point in the history
This adds some user guide explanation of the maxregret parameter for Nash-computing algorithms,
and standardises the default values.
  • Loading branch information
tturocy committed Mar 28, 2024
1 parent b6ab6a7 commit 0acdf85
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 17 deletions.
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
3 changes: 2 additions & 1 deletion doc/tools.logit.rst
Expand Up @@ -70,7 +70,8 @@ the beliefs at such information sets as being uniform across all member nodes.

.. 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:: -l

Expand Down
3 changes: 2 additions & 1 deletion doc/tools.simpdiv.rst
Expand Up @@ -65,7 +65,8 @@ 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:: -v

Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/nash.pxi
Expand Up @@ -256,7 +256,7 @@ def logit_atlambda(game: Game, lam: float) -> LogitQREMixedStrategyProfile:


def logit_principal_branch(game: Game):
solns = _logit_principal_branch(game.game, 1.0e-6)
solns = _logit_principal_branch(game.game, 1.0e-8)
ret = []
for i in range(solns.Length()):
p = LogitQREMixedStrategyProfile()
Expand Down
16 changes: 10 additions & 6 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(
game : Game
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(game, maxregret, refine, leash or 0)
return NashComputationResult(
game=game,
Expand Down Expand Up @@ -518,7 +522,7 @@ def gnm_solve(
def logit_solve(
game: libgbt.Game,
use_strategic: bool = False,
maxregret: float = 0.0001,
maxregret: float = 1.0e-8,
) -> NashComputationResult:
"""Compute Nash equilibria of a game using :ref:`the logit quantal response
equilibrium correspondence <gambit-logit>`.
Expand All @@ -535,7 +539,7 @@ def logit_solve(
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 0.0001
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
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
2 changes: 1 addition & 1 deletion src/solvers/logit/path.cc
Expand Up @@ -245,7 +245,7 @@ void PathTracer::TracePath(const EquationSystem &p_system, Vector<double> &x, do
// Bifurcation detected; for now, just "jump over" and continue,
// taking into account the change in orientation of the curve.
// Someday, we need to do more here!
std::cout << "Flippin heck!\n";

p_omega = -p_omega;
}
t = newT;
Expand Down
2 changes: 1 addition & 1 deletion src/tools/liap/liap.cc
Expand Up @@ -132,7 +132,7 @@ int main(int argc, char *argv[])
int numTries = 10;
int maxitsN = 1000;
int numDecimals = 6;
double maxregret = 0.001;
double maxregret = 1.0e-4;
double tolN = 1.0e-10;
std::string startFile;

Expand Down
2 changes: 1 addition & 1 deletion src/tools/logit/logit.cc
Expand Up @@ -90,7 +90,7 @@ int main(int argc, char *argv[])

bool quiet = false, useStrategic = false;
double maxLambda = 1000000.0;
double maxregret = 0.0001;
double maxregret = 1.0e-8;
std::string mleFile;
double maxDecel = 1.1;
double hStart = 0.03;
Expand Down
3 changes: 1 addition & 2 deletions src/tools/simpdiv/nfgsimpdiv.cc
Expand Up @@ -102,7 +102,7 @@ int main(int argc, char *argv[])
bool useRandom = false;
int randDenom = 1, gridResize = 2, stopAfter = 1;
bool verbose = false, quiet = false;
Rational maxregret(1, 1000000);
Rational maxregret(1, 10000000);

int long_opt_index = 0;
struct option long_options[] = {{"help", 0, nullptr, 'h'},
Expand All @@ -111,7 +111,6 @@ int main(int argc, char *argv[])
{nullptr, 0, nullptr, 0}};
int c;
while ((c = getopt_long(argc, argv, "g:hVvn:r:s:m:qS", long_options, &long_opt_index)) != -1) {
std::cout << c << std::endl;
switch (c) {
case 'v':
PrintBanner(std::cerr);
Expand Down

0 comments on commit 0acdf85

Please sign in to comment.