Skip to content
Merged
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
36 changes: 36 additions & 0 deletions doc/algorithms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,42 @@ Algorithm Description
:ref:`hp` Compute a specific Nash equilibrium using a homotopy path-following method :py:func:`pygambit.nash.hp_solve` :ref:`gambit-hp <gambit-hp>`
================ =========================================================================== ======================================== ==========================================

.. _pygambit-nash-maxregret:

Acceptance criteria for approximate 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
approximation. The outputs of these methods therefore are in general
:math:`\varepsilon`-equilibria, for some positive :math:`\varepsilon`: a strategy profile at
which no player can gain more than :math:`\varepsilon` in expected payoff by unilaterally
deviating. Every Nash equilibrium is an :math:`\varepsilon`-equilibrium with
:math:`\varepsilon = 0`.

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. For example, with the default
`maxregret` of :math:`10^{-8}` in a game whose payoffs range over 4 units, any equilibrium
returned is guaranteed to have a regret, measured directly in the game's own payoffs, of no
more than :math:`4 \times 10^{-8}`.

Expressing `maxregret` scaled by the game's payoffs in this way standardises the behavior of
methods across games: for instance, doubling all the payoffs in a game does not change the
`maxregret` value needed to obtain equilibria of comparable quality.

Methods differ in the guarantees they offer once a `maxregret` criterion is specified.
Globally-convergent methods, such as :ref:`logit` and :ref:`gnm`, are guaranteed to eventually
satisfy any `maxregret` criterion, though a tighter criterion generally requires more
computation. Other methods, such as :ref:`liap`, are not globally convergent, and may fail to
find any equilibrium satisfying the criterion from a given starting point.

See the :doc:`stripped-down poker tutorial <tutorials/03_stripped_down_poker>` for a worked
example comparing `maxregret` across several methods.

.. _enumpure:

enumpure
Expand Down
5 changes: 5 additions & 0 deletions doc/tools.hp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ different equilibria being found.

Prints a help message listing the available options.

.. cmdoption:: -m

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:: -n

Randomly generate the specified number of prior distributions.
Expand Down
24 changes: 1 addition & 23 deletions doc/tutorials/03_stripped_down_poker.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -675,29 +675,7 @@
"cell_type": "markdown",
"id": "b2867dca",
"metadata": {},
"source": [
"Acceptance criteria for Nash equilibria\n",
"---------------------------------------\n",
"\n",
"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.\n",
"The outputs of these methods therefore are in general $\\varepsilon$-equilibria, for some positive $\\varepsilon$.\n",
"\n",
"$\\varepsilon$-equilibria (from [Wikipedia](https://en.wikipedia.org/wiki/Epsilon-equilibrium)):\n",
"\n",
"> In game theory, an epsilon-equilibrium, or near-Nash equilibrium, is a strategy profile that approximately satisfies the condition of Nash equilibrium. In a Nash equilibrium, no player has an incentive to change his behavior. In an approximate Nash equilibrium, this requirement is weakened to allow the possibility that a player may have a small incentive to do something different.\n",
"\n",
"> Given a game and a real non-negative parameter $\\varepsilon$, a strategy profile is said to be an $\\varepsilon$-equilibrium if it is not possible for any player to gain more than $\\varepsilon$ in expected payoff by unilaterally deviating from his strategy. Every Nash Equilibrium is an $\\varepsilon$-equilibrium where $\\varepsilon = 0$.\n",
"\n",
"\n",
"To provide a uniform interface across methods, where relevant Gambit provides a parameter\n",
"`maxregret`, which specifies the acceptance criterion for labeling the output of the\n",
"algorithm as an equilibrium.\n",
"This parameter is interpreted *proportionally* to the range of payoffs in the game.\n",
"Any profile returned as an equilibrium is guaranteed to be an $\\varepsilon$-equilibrium, for $\\varepsilon$ no more than `maxregret`\n",
"times the difference of the game's maximum and minimum payoffs.\n",
"\n",
"As an example, consider solving our one-card poker game using `logit_solve`. The range of the payoffs in this game is 4 (from +2 to -2):\n"
]
"source": "Acceptance criteria for Nash equilibria\n---------------------------------------\n\nSome 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, and so return only approximate equilibria.\nGambit expresses how close an approximation must be, before it is reported as an equilibrium, via a `maxregret` parameter, interpreted *proportionally* to the range of payoffs in the game.\nSee [Acceptance criteria for approximate Nash equilibria](../algorithms.html#pygambit-nash-maxregret) in the algorithms documentation for the full explanation of `maxregret` and the guarantees it provides.\n\nAs an example, consider solving our one-card poker game using `logit_solve`. The range of the payoffs in this game is 4 (from +2 to -2):\n"
},
{
"cell_type": "code",
Expand Down
6 changes: 4 additions & 2 deletions src/gui/dlnash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ wxString ExternalCommand(const NashComputationSpec &p_spec)
method.localNewtonMaxIterations);
}
else if constexpr (std::is_same_v<Method, HPNashSpec>) {
return prefix + wxString::Format("hp -d 10 -n %d", method.priors);
return prefix +
wxString::Format("hp -d 10 -n %d -m %.17g", method.priors, method.maxRegret);
}
else if constexpr (std::is_same_v<Method, IPANashSpec>) {
return prefix + wxString::Format("ipa -d 10 -n %d", method.perturbations);
Expand Down Expand Up @@ -274,7 +275,8 @@ wxString ParameterDescription(const NashMethodSpec &p_method)
method.localNewtonMaxIterations);
}
else if constexpr (std::is_same_v<Method, HPNashSpec>) {
return wxString::Format(" (%d random priors)", method.priors);
return wxString::Format(" (%d random priors; maximum regret %.4g)", method.priors,
method.maxRegret);
}
else if constexpr (std::is_same_v<Method, IPANashSpec>) {
return wxString::Format(" (%d perturbation)", method.perturbations);
Expand Down
2 changes: 1 addition & 1 deletion src/gui/nashspec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ std::optional<SolverFunction> HPNashSpec::MakeSolver(NashRepresentation) const
for (const auto &prior : NewRandomStrategyProfiles(p_game, spec.priors)) {
p_cancel.Check();
Nash::HPStrategySolve(
prior,
prior, spec.maxRegret,
[&p_callback](const MixedStrategyProfile<double> &p) { p_callback(ComputedProfile(p)); },
Nash::NullHPEventCallback, p_cancel);
}
Expand Down
1 change: 1 addition & 0 deletions src/gui/nashspec.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ struct HPNashSpec {
// path-tracing can surface more), so this defaults high, like LiapNashSpec's
// startingPoints, rather than to 1.
int priors{10};
double maxRegret{1.0e-8};

std::optional<SolverFunction> MakeSolver(NashRepresentation) const;
};
Expand Down
11 changes: 10 additions & 1 deletion src/pygambit/cli/hp.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
default=None,
help="file containing prior distributions (mutually exclusive with -n)",
)
@click.option(
"-m",
"maxregret",
default=1.0e-8,
show_default=True,
type=float,
help="maximum regret acceptable as a proportion of the range of payoffs in the game",
)
@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
@click.option(
"-V",
Expand All @@ -95,6 +103,7 @@ def main(
n_priors: int | None,
seed: int | None,
start_file: str | None,
maxregret: float,
quiet: bool,
verbose: bool,
) -> None:
Expand All @@ -109,7 +118,7 @@ def render_event(event: gbt.HPStepEvent) -> None:
prior = prior.as_float()
if verbose:
click.echo(render_profile_csv(prior, "prior", decimals))
result = gbt.nash.hp_solve(prior, event_callback=render_event)
result = gbt.nash.hp_solve(prior, maxregret=maxregret, event_callback=render_event)
for eq in result.equilibria:
click.echo(render_profile_csv(eq, "NE", decimals))

Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -701,5 +701,5 @@ cdef extern from "nash.h":
LogitEventCallbackType[c_LogitQREMixedStrategyProfile]
) except +
stdlist[c_MixedStrategyProfile[double]] HPStrategySolveWrapper(
c_MixedStrategyProfile[double], HPEventCallbackType
c_MixedStrategyProfile[double], double, HPEventCallbackType
) except +RuntimeError
5 changes: 3 additions & 2 deletions src/pygambit/nash.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ LogitStrategyEstimateWrapper(std::shared_ptr<MixedStrategyProfile<double>> p_fre
}

std::list<MixedStrategyProfile<double>>
HPStrategySolveWrapper(const MixedStrategyProfile<double> &p_prior,
HPStrategySolveWrapper(const MixedStrategyProfile<double> &p_prior, double p_maxRegret,
Nash::HPEventCallbackType p_onEvent = Nash::NullHPEventCallback)
{
return Nash::HPStrategySolve(p_prior, Nash::NullStrategyCallback<double>, p_onEvent);
return Nash::HPStrategySolve(p_prior, p_maxRegret, Nash::NullStrategyCallback<double>,
p_onEvent);
}
3 changes: 2 additions & 1 deletion src/pygambit/nash.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -914,8 +914,9 @@ def _logit_behavior_branch(game: Game,

def _hp_strategy_solve(
prior: MixedStrategyProfileDouble,
maxregret: float,
event_callback: object = None,
) -> list[MixedStrategyProfileDouble]:
return _convert_mspd(HPStrategySolveWrapper(
deref(prior.profile), MakeHPEventCallback(event_callback)
deref(prior.profile), maxregret, MakeHPEventCallback(event_callback)
))
12 changes: 10 additions & 2 deletions src/pygambit/nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,7 @@ def logit_solve(

def hp_solve(
prior: libgbt.MixedStrategyProfileDouble,
maxregret: float = 1.0e-8,
event_callback: Callable[[libgbt.HPStepEvent], None] | None = None,
) -> NashComputationResult:
"""Compute Nash equilibria of a game using :cite:p:`HerPee01`
Expand All @@ -1125,6 +1126,11 @@ def hp_solve(
prior : MixedStrategyProfileDouble
The prior distribution over strategies.

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

event_callback : Callable[[HPStepEvent], None], optional
If specified, called with each point traced along the homotopy path,
and the homotopy parameter ``t`` at which it was reached, on the way
Expand All @@ -1137,12 +1143,14 @@ def hp_solve(
res : NashComputationResult
The result represented as a ``NashComputationResult`` object.
"""
equilibria = libgbt._hp_strategy_solve(prior, event_callback)
if maxregret <= 0.0:
raise ValueError("hp_solve(): maxregret argument must be positive")
equilibria = libgbt._hp_strategy_solve(prior, maxregret, event_callback)
return NashComputationResult(
game=prior.game,
method="hp",
rational=False,
use_strategic=True,
equilibria=equilibria,
parameters={},
parameters={"maxregret": maxregret},
)
65 changes: 60 additions & 5 deletions src/solvers/hp/hp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

namespace Gambit::Nash {
std::list<MixedStrategyProfile<double>>
HPStrategySolve(const MixedStrategyProfile<double> &p_prior,
HPStrategySolve(const MixedStrategyProfile<double> &p_prior, double p_maxRegret,
StrategyCallbackType<double> p_onEquilibrium, HPEventCallbackType p_onEvent,
const CancelToken &p_cancel)
{
Expand All @@ -39,11 +39,49 @@ HPStrategySolve(const MixedStrategyProfile<double> &p_prior,
const PathTracer tracer;
const PathTracer::TraceDirection direction = PathTracer::TraceDirection::Positive;
const size_t tracking_index = 1; // Track the first variable (t) for orientation
auto termination_condition = [](const Vector<double> &point) { return point[1] >= 1.5; };
auto criterion_function = [](const Vector<double> &point,
const Vector<double> &tangent) -> double { return point[1] - 1.0; };
const double t_target = 1.0;
double last_t = 0.0;
bool has_crossed = false;

tracer.TracePath(
auto termination_condition = [t_target, &last_t, &has_crossed, p_maxRegret,
&system](const Vector<double> &point) {
const double t = point[1];

// Path tracer reaches maximum acceptable regret
if (system.ExtractEquilibrium(point).GetMaxRegret() <= p_maxRegret &&
t >= t_target - p_maxRegret) {
return true;
}

if (t > t_target) {
if (!has_crossed) {
has_crossed = true;
}
else if (t >
last_t + p_maxRegret) { // Criterion function is not working; polish will do the job
return true;
}
}
else {
// Criterion function might take t back to being less than t_target
has_crossed = false;
}

last_t = t;
return false;
};

auto criterion_function = [t_target](const Vector<double> &point,
const Vector<double> &tangent) -> double {
return point[1] - t_target;
};

auto polishing_termination_condition = [p_maxRegret,
&system](const Vector<double> &point) -> bool {
return system.ExtractEquilibrium(point).GetMaxRegret() <= p_maxRegret;
};

const auto tracing_result = tracer.TracePath(
[&system](const Vector<double> &point, Vector<double> &lhs) { system.GetValue(point, lhs); },
[&system](const Vector<double> &point, Matrix<double> &jac) {
system.GetJacobian(point, jac);
Expand All @@ -55,7 +93,24 @@ HPStrategySolve(const MixedStrategyProfile<double> &p_prior,
},
criterion_function, NullCriterionBracketFunction, p_cancel);

const PolishResult polishing_result = PolishPoint(
[&system](const Vector<double> &point, Vector<double> &lhs) { system.GetValue(point, lhs); },
[&system](const Vector<double> &point, Matrix<double> &jac) {
system.GetJacobian(point, jac);
},
x, t_target, 1, polishing_termination_condition, 100,
[&system, &p_onEvent](const Vector<double> &point) {
const MixedStrategyProfile<double> profile = system.ExtractEquilibrium(point);
p_onEvent(HPStepEvent{.profile = profile, .t = point[1]});
});

if (!polishing_result.status) {
return {};
}
const MixedStrategyProfile<double> equilibrium = system.ExtractEquilibrium(x);
if (equilibrium.GetMaxRegret() > p_maxRegret) {
return {};
}
p_onEquilibrium(equilibrium);
equilibria.push_back(equilibrium);
return equilibria;
Expand Down
2 changes: 1 addition & 1 deletion src/solvers/hp/hp.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ inline void NullHPEventCallback(const HPEvent &) {}
/// @brief Compute a Nash equilibrium of a game using the homotopy method of
/// Herings and Peeters (2001)
std::list<MixedStrategyProfile<double>>
HPStrategySolve(const MixedStrategyProfile<double> &p_prior,
HPStrategySolve(const MixedStrategyProfile<double> &p_prior, double p_maxRegret = 1.0e-8,
StrategyCallbackType<double> p_onEquilibrium = NullStrategyCallback<double>,
HPEventCallbackType p_onEvent = NullHPEventCallback,
const CancelToken &p_cancel = CancelToken());
Expand Down
Loading
Loading