Skip to content

Commit

Permalink
Expose regret-based stopping criterion for simpdiv_solve
Browse files Browse the repository at this point in the history
Internally, the stopping criterion for simpdiv_solve has always been based on what we call the
maximum regret by any player.  This now exposes this as a parameter to simpdiv_solve in pygambit
and as a command-line parameter to the gambit-simpdiv tool.

The acceptance criterion is scaled by the range of payoffs in the game, so multiplying all payoffs in a game
by a common factor will result in the same output given the same acceptance tolerance.
  • Loading branch information
tturocy committed Feb 8, 2024
1 parent 59370d1 commit 6f71a89
Show file tree
Hide file tree
Showing 9 changed files with 52 additions and 18 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Expand Up @@ -24,6 +24,7 @@
- `liap_solve`/`gambit-liap` has been reimplemented to scale payoffs uniformly across games,
to always take an explicit starting point (in `liap_solve`), and to specify a regret-based
acceptance criterion (#330)
- `simpdiv_solve`/`gambit-simpdiv` now accepts a regret-based acceptance criterion (#439)
- Converted test suite for mixed behavior profiles to pytest style; added parametrizations for
test_realiz_prob; added test_martingale_property_of_node_value (#375)
- Improved test suite for mixed strategy profiles (#374)
Expand Down
6 changes: 6 additions & 0 deletions doc/tools.simpdiv.rst
Expand Up @@ -61,6 +61,12 @@ options to specify additional starting points for the algorithm.
one mixed strategy profile per line, in the same format used for
output of equilibria (excluding the initial NE tag).

.. cmdoption:: -m

.. versionadded:: 16.2.0

Specify the maximum regret criterion for acceptance as an approximate Nash equilibrium.

.. cmdoption:: -v

Sets verbose mode. In verbose mode, initial points, as well as
Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/gambit.pxd
Expand Up @@ -440,7 +440,7 @@ cdef extern from "solvers/liap/liap.h":

cdef extern from "solvers/simpdiv/simpdiv.h":
c_List[c_MixedStrategyProfileRational] SimpdivStrategySolve(
c_Game, int p_gridResize, int p_leashLength
c_Game, c_Rational p_maxregret, int p_gridResize, int p_leashLength
) except +RuntimeError

cdef extern from "solvers/ipa/ipa.h":
Expand Down
6 changes: 4 additions & 2 deletions src/pygambit/nash.pxi
Expand Up @@ -148,9 +148,11 @@ def _liap_behavior_solve(start: MixedBehaviorProfileDouble,


def _simpdiv_strategy_solve(
game: Game, gridstep: int, leash: int
game: Game, maxregret: Rational, gridstep: int, leash: int
) -> typing.List[MixedStrategyProfileRational]:
return _convert_mspr(SimpdivStrategySolve(game.game, gridstep, leash))
return _convert_mspr(SimpdivStrategySolve(game.game,
to_rational(str(maxregret).encode("ascii")),
gridstep, leash))


def _ipa_strategy_solve(
Expand Down
16 changes: 14 additions & 2 deletions src/pygambit/nash.py
Expand Up @@ -306,6 +306,7 @@ def liap_solve(

def simpdiv_solve(
game: libgbt.Game,
maxregret: libgbt.Rational = None,
refine: int = 2,
leash: typing.Optional[int] = None
) -> NashComputationResult:
Expand All @@ -316,9 +317,18 @@ def simpdiv_solve(
----------
game : Game
The game to compute equilibria in.
maxregret : Rational, default 1/1000
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
refine : int, default 2
This controls the rate at which the triangulation of the space of mixed strategy
profiles is made more fine at each iteration.
leash : int, optional
Simplicial subdivision is guaranteed to converge to an (approximate) Nash equilibrium.
The method may take arbitrarily long paths through the space of mixed strategies in
Expand All @@ -335,14 +345,16 @@ def simpdiv_solve(
raise ValueError("simpdiv_solve(): refine must be an integer no less than 2")
if leash is not None and (not isinstance(leash, int) or leash <= 0):
raise ValueError("simpdiv_solve(): leash must be a non-negative integer")
equilibria = libgbt._simpdiv_strategy_solve(game, refine, leash or 0)
if maxregret is None:
maxregret = libgbt.Rational(1, 1000)
equilibria = libgbt._simpdiv_strategy_solve(game, maxregret, refine, leash or 0)
return NashComputationResult(
game=game,
method="simpdiv",
rational=True,
use_strategic=True,
equilibria=equilibria,
parameters={"leash": leash}
parameters={"maxregret": maxregret, "refine": refine, "leash": leash}
)


Expand Down
10 changes: 6 additions & 4 deletions src/solvers/simpdiv/simpdiv.cc
Expand Up @@ -54,6 +54,8 @@ class NashSimpdivStrategySolver::State {
}
};

/// @brief Implementation of the piecewise path-following algorithm
/// @returns The maximum regret of any player at the terminal profile
Rational NashSimpdivStrategySolver::Simplex(MixedStrategyProfile<Rational> &y,
const Rational &d) const
{
Expand Down Expand Up @@ -463,21 +465,21 @@ NashSimpdivStrategySolver::Solve(const MixedStrategyProfile<Rational> &p_start)
"Computing equilibria of games with imperfect recall is not supported.");
}
Rational d(Integer(1), find_lcd((const Vector<Rational> &)p_start));
Rational scale = p_start.GetGame()->GetMaxPayoff() - p_start.GetGame()->GetMinPayoff();

MixedStrategyProfile<Rational> y(p_start);
if (m_verbose) {
this->m_onEquilibrium->Render(y, "start");
}

while (true) {
const double TOL = 1.0e-10;
d /= Rational(m_gridResize);
Rational maxz = Simplex(y, d);
Rational regret = Simplex(y, d);

if (m_verbose) {
this->m_onEquilibrium->Render(y, lexical_cast<std::string>(d));
this->m_onEquilibrium->Render(y, std::to_string(d));
}
if (maxz < Rational(TOL)) {
if (regret <= m_maxregret * scale) {
break;
}
}
Expand Down
12 changes: 8 additions & 4 deletions src/solvers/simpdiv/simpdiv.h
Expand Up @@ -36,10 +36,12 @@ namespace Nash {
class NashSimpdivStrategySolver : public StrategySolver<Rational> {
public:
explicit NashSimpdivStrategySolver(
int p_gridResize = 2, int p_leashLength = 0, bool p_verbose = false,
int p_gridResize = 2, int p_leashLength = 0,
const Rational &p_maxregret = Rational(1, 1000000), bool p_verbose = false,
std::shared_ptr<StrategyProfileRenderer<Rational>> p_onEquilibrium = nullptr)
: StrategySolver<Rational>(p_onEquilibrium), m_gridResize(p_gridResize),
m_leashLength((p_leashLength > 0) ? p_leashLength : 32000), m_verbose(p_verbose)
m_leashLength((p_leashLength > 0) ? p_leashLength : 32000), m_maxregret(p_maxregret),
m_verbose(p_verbose)
{
}
~NashSimpdivStrategySolver() override = default;
Expand All @@ -49,6 +51,7 @@ class NashSimpdivStrategySolver : public StrategySolver<Rational> {

private:
int m_gridResize, m_leashLength;
Rational m_maxregret;
bool m_verbose;

class State;
Expand All @@ -65,9 +68,10 @@ class NashSimpdivStrategySolver : public StrategySolver<Rational> {
};

inline List<MixedStrategyProfile<Rational>>
SimpdivStrategySolve(const Game &p_game, int p_gridResize = 2, int p_leashLength = 0)
SimpdivStrategySolve(const Game &p_game, const Rational &p_maxregret = Rational(1, 1000000),
int p_gridResize = 2, int p_leashLength = 0)
{
return NashSimpdivStrategySolver(p_gridResize, p_leashLength).Solve(p_game);
return NashSimpdivStrategySolver(p_gridResize, p_leashLength, p_maxregret).Solve(p_game);
}

} // namespace Nash
Expand Down
2 changes: 1 addition & 1 deletion src/tools/liap/liap.cc
Expand Up @@ -49,7 +49,7 @@ void PrintHelp(char *progname)
std::cerr << " -h, --help print this help message\n";
std::cerr << " -n COUNT number of starting points to generate\n";
std::cerr << " -i MAXITER maximum number of iterations per point (default is 1000)\n";
std::cerr << " -r MAXREGRET maximum regret acceptable as a proportion of range of\n";
std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
std::cerr << " payoffs in the game\n";
std::cerr << " -s FILE file containing starting points\n";
std::cerr << " -q quiet mode (suppresses banner)\n";
Expand Down
15 changes: 11 additions & 4 deletions src/tools/simpdiv/nfgsimpdiv.cc
Expand Up @@ -86,6 +86,8 @@ void PrintHelp(char *progname)
std::cerr << " -r DENOM generate random starting points with denominator DENOM\n";
std::cerr << " -n COUNT number of starting points to generate (requires -r)\n";
std::cerr << " -s FILE file containing starting points\n";
std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
std::cerr << " payoffs in the game\n";
std::cerr << " -q quiet mode (suppresses banner)\n";
std::cerr << " -V, --verbose verbose mode (shows intermediate output)\n";
std::cerr << " -v, --version print version information\n";
Expand All @@ -100,14 +102,16 @@ 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);

int long_opt_index = 0;
struct option long_options[] = {{"help", 0, nullptr, 'h'},
{"version", 0, nullptr, 'v'},
{"verbose", 0, nullptr, 'V'},
{nullptr, 0, nullptr, 0}};
int c;
while ((c = getopt_long(argc, argv, "g:hVvn:r:s:qS", long_options, &long_opt_index)) != -1) {
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 All @@ -125,6 +129,9 @@ int main(int argc, char *argv[])
case 'n':
stopAfter = atoi(optarg);
break;
case 'm':
maxregret = lexical_cast<Rational>(std::string(optarg));
break;
case 's':
startFile = optarg;
break;
Expand Down Expand Up @@ -183,11 +190,11 @@ int main(int argc, char *argv[])
starts[1][game->GetPlayer(pl)->GetStrategies()[1]] = Rational(1);
}
}
for (int i = 1; i <= starts.size(); i++) {
for (auto start : starts) {
std::shared_ptr<StrategyProfileRenderer<Rational>> renderer(
new MixedStrategyCSVRenderer<Rational>(std::cout));
NashSimpdivStrategySolver algorithm(gridResize, 0, verbose, renderer);
algorithm.Solve(starts[i]);
NashSimpdivStrategySolver algorithm(gridResize, 0, maxregret, verbose, renderer);
algorithm.Solve(start);
}
return 0;
}
Expand Down

0 comments on commit 6f71a89

Please sign in to comment.