Skip to content

Commit

Permalink
Remove integrated lrslib and call lrsnash as external tool.
Browse files Browse the repository at this point in the history
This removes the very dated implementation of `lrslib` previously
embedded, and instead allows for calls to `lrsnash` as an
external program.
  • Loading branch information
tturocy committed Oct 18, 2024
1 parent d7d6c0f commit 1d5c3c4
Show file tree
Hide file tree
Showing 20 changed files with 166 additions and 9,587 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
`enumpoly_solve` based on an implementation formerly in the `contrib` section of the
repository. (#165)

### Changed
- The built-in implementation of lrslib (dating from 2016) has been removed. Instead, access to
lrsnash is provided as an external tool via the `enummixed_solve` function, in parallel to
PHCpack for `enumpoly_solve`.

### Fixed
- When parsing .nfg files, check that the number of outcomes or payoffs is the expected number,
and raise an exception if not. (#119)
Expand Down
11 changes: 0 additions & 11 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,6 @@ linalg_SOURCES = \
src/solvers/linalg/vertenum.h \
src/solvers/linalg/vertenum.imp

lrslib_SOURCES = \
src/solvers/lrs/lrslib.h \
src/solvers/lrs/lrslib.c \
src/solvers/lrs/lrsmp.h \
src/solvers/lrs/lrsmp.c \
src/solvers/lrs/lrsnashlib.h \
src/solvers/lrs/lrsnashlib.c

gtracer_SOURCES = \
src/solvers/gtracer/cmatrix.h \
src/solvers/gtracer/cmatrix.cc \
Expand Down Expand Up @@ -403,10 +395,8 @@ gambit_convert_SOURCES = \
gambit_enummixed_SOURCES = \
${core_SOURCES} ${game_SOURCES} \
${linalg_SOURCES} \
${lrslib_SOURCES} \
src/solvers/enummixed/clique.cc \
src/solvers/enummixed/clique.h \
src/solvers/enummixed/lrsenum.cc \
src/solvers/enummixed/enummixed.cc \
src/solvers/enummixed/enummixed.h \
src/tools/enummixed/enummixed.cc
Expand All @@ -416,7 +406,6 @@ gambit_nashsupport_SOURCES = \
src/solvers/nashsupport/nfgsupport.cc \
src/solvers/nashsupport/nashsupport.h

# For enumpoly, sources starting in 'pel' are from Pelican.
gambit_enumpoly_SOURCES = \
${core_SOURCES} ${game_SOURCES} ${gambit_nashsupport_SOURCES} \
src/solvers/enumpoly/gameseq.cc \
Expand Down
33 changes: 33 additions & 0 deletions doc/pygambit.user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -797,3 +797,36 @@ data passed are a :py:class:`.MixedBehaviorProfile`, and will return a

.. [#f1] The log-likelihoods quoted in [McKPal95]_ are exactly a factor of 10 larger than
those obtained by replicating the calculation.
Using external programs to compute Nash equilbria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Because the problem of finding Nash equilibria can be expressed in various
mathematical formulations (see [McKMcL96]_), it is helpful to make use
of other software packages designed specifically for solving those problems.

There are currently two integrations offered for using external programs to solve
for equilibria:

- :py:func:`.enummixed_solve` supports enumeration of equilibria in
two-player games via `lrslib`. [#lrslib]_
- :py:func:`.enumpoly_solve` supports computation of totally-mixed equilibria
on supports in strategic games via `PHCpack`. [#phcpack]_

For both calls, using the external program requires passing the path to the
executable (via the `lrsnash_path` and `phcpack_path` arguments, respectively).

The user must download and compile or install these programs on their own; these are
not packaged with Gambit. The solver calls do take care of producing the required
input files, and reading the output to convert into Gambit objects for further
processing.


.. [#lrslib] http://cgm.cs.mcgill.ca/~avis/C/lrs.html
.. [#phcpack] https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html
.. [McKMcL96] McKelvey, Richard D. and McLennan, Andrew M. (1996) Computation of equilibria
in finite games. In Handbook of Computational Economics, Volume 1,
pages 87-142.
2 changes: 1 addition & 1 deletion doc/tools.enumpoly.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ singular supports.
default, no information about supports is printed.

Computing equilibria of the strategic game :download:`e01.nfg
<../contrib/games/n01.efg>`, the example in Figure 1 of Selten
<../contrib/games/e01.efg>`, the example in Figure 1 of Selten
(International Journal of Game Theory, 1975) sometimes called
"Selten's horse"::

Expand Down
1 change: 1 addition & 0 deletions doc/tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ documentation.

tools.enumpure
tools.enummixed
tools.enumpoly
tools.lcp
tools.lp
tools.liap
Expand Down
6 changes: 1 addition & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@
import Cython.Build
import setuptools

# By compiling this separately as a C library, we avoid problems
# with passing C++-specific flags when building the extension
lrslib = ("lrslib", {"sources": glob.glob("src/solvers/lrs/*.c")})

cppgambit_include_dirs = ["src"]
cppgambit_cflags = (
["-std=c++17"] if platform.system() == "Darwin"
Expand Down Expand Up @@ -144,7 +140,7 @@ def readme():
],
libraries=[cppgambit_bimatrix, cppgambit_liap, cppgambit_logit, cppgambit_simpdiv,
cppgambit_gtracer, cppgambit_enumpoly,
cppgambit_games, cppgambit_core, lrslib],
cppgambit_games, cppgambit_core],
package_dir={"": "src"},
packages=["pygambit"],
ext_modules=Cython.Build.cythonize(libgambit,
Expand Down
4 changes: 0 additions & 4 deletions src/pygambit/nash.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ def _enummixed_strategy_solve_rational(game: Game) -> typing.List[MixedStrategyP
return _convert_mspr(EnumMixedStrategySolveRational(game.game))


def _enummixed_strategy_solve_lrs(game: Game) -> typing.List[MixedStrategyProfileRational]:
return _convert_mspr(EnumMixedStrategySolveLrs(game.game))


def _lcp_behavior_solve_double(
game: Game, stop_after: int, max_depth: int
) -> typing.List[MixedBehaviorProfileDouble]:
Expand Down
41 changes: 30 additions & 11 deletions src/pygambit/nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import pygambit.gambit as libgbt

from . import nashphc
from . import nashlrs, nashphc

MixedStrategyEquilibriumSet = list[libgbt.MixedStrategyProfile]
MixedBehaviorEquilibriumSet = list[libgbt.MixedBehaviorProfile]
Expand Down Expand Up @@ -103,7 +103,7 @@ def enumpure_solve(game: libgbt.Game, use_strategic: bool = True) -> NashComputa
def enummixed_solve(
game: libgbt.Game,
rational: bool = True,
use_lrs: bool = False
lrsnash_path: pathlib.Path | str | None = None,
) -> NashComputationResult:
"""Compute all :ref:`mixed-strategy Nash equilibria <gambit-enummixed>`
of a two-player game using the strategic representation.
Expand All @@ -112,11 +112,16 @@ def enummixed_solve(
----------
game : Game
The game to compute equilibria in.
rational : bool, default True
Compute using rational numbers. If `False`, using floating-point
arithmetic. Using rationals is more precise, but slower.
use_lrs : bool, default False
If `True`, use the implementation based on ``lrslib``. This is experimental.
lrsnash_path : pathlib.Path | str | None = None,
If specified, use lrsnash to solve the systems of equations.
This argument specifies the path to the lrsnash executable.
.. versionadded:: 16.3.0
Returns
-------
Expand All @@ -127,17 +132,29 @@ def enummixed_solve(
------
RuntimeError
If game has more than two players.
Notes
-----
`lrsnash` is part of `lrslib`, available at http://cgm.cs.mcgill.ca/~avis/C/lrs.html
"""
if use_lrs:
equilibria = libgbt._enummixed_strategy_solve_lrs(game)
elif rational:
if lrsnash_path is not None:
equilibria = nashlrs.lrsnash_solve(game, lrsnash_path=lrsnash_path)
return NashComputationResult(
game=game,
method="enummixed",
rational=True,
use_strategic=True,
parameters={"lrsnash_path": lrsnash_path},
equilibria=equilibria,
)
if rational:
equilibria = libgbt._enummixed_strategy_solve_rational(game)
else:
equilibria = libgbt._enummixed_strategy_solve_double(game)
return NashComputationResult(
game=game,
method="enummixed",
rational=use_lrs or rational,
rational=rational,
use_strategic=True,
equilibria=equilibria
)
Expand Down Expand Up @@ -566,7 +583,7 @@ def enumpoly_solve(
use_strategic: bool = False,
stop_after: int | None = None,
maxregret: float = 1.0e-4,
phcpack_path: pathlib.Path | str | None = None,
phcpack_path: pathlib.Path | str | None = None
) -> NashComputationResult:
"""Compute Nash equilibria by enumerating all support profiles of strategies
or actions, and for each support finding all totally-mixed equilibria of
Expand All @@ -591,7 +608,7 @@ def enumpoly_solve(
difference of the maximum and minimum payoffs of the game
phcpack_path : str or pathlib.Path, optional
If specified, use PHCpack [1]_ to solve the systems of equations.
If specified, use PHCpack to solve the systems of equations.
This argument specifies the path to the PHCpack executable.
With this method, only enumeration on the strategic game is supported.
Expand All @@ -600,7 +617,9 @@ def enumpoly_solve(
res : NashComputationResult
The result represented as a ``NashComputationResult`` object.
.. [1] https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html
Notes
-----
PHCpack is available at https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html
"""
if stop_after is None:
stop_after = 0
Expand Down
60 changes: 60 additions & 0 deletions src/pygambit/nashlrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Interface to external standalone tool lrsnash.
"""

from __future__ import annotations

import itertools
import pathlib
import subprocess
import sys

import pygambit as gbt
import pygambit.util as util


def _generate_lrs_input(game: gbt.Game) -> str:
s = f"{len(game.players[0].strategies)} {len(game.players[1].strategies)}\n\n"
for st1 in game.players[0].strategies:
s += " ".join(str(gbt.Rational(game[st1, st2][game.players[0]]))
for st2 in game.players[1].strategies) + "\n"
s += "\n"
for st1 in game.players[0].strategies:
s += " ".join(str(gbt.Rational(game[st1, st2][game.players[1]]))
for st2 in game.players[1].strategies) + "\n"
return s


def _parse_lrs_output(game: gbt.Game, txt: str) -> list[gbt.MixedStrategyProfileRational]:
data = "\n".join([x for x in txt.splitlines() if not x.startswith("*")]).strip()
eqa = []
for component in data.split("\n\n"):
profiles = {"1": [], "2": []}
for line in component.strip().splitlines():
profiles[line[0]].append([gbt.Rational(x) for x in line[1:].strip().split()[:-1]])
for p1, p2 in itertools.product(profiles["1"], profiles["2"]):
eqa.append(game.mixed_strategy_profile([p1, p2], rational=True))
return eqa


def lrsnash_solve(game: gbt.Game,
lrsnash_path: pathlib.Path | str) -> list[gbt.MixedStrategyProfileRational]:
if len(game.players) != 2:
raise RuntimeError("Method only valid for two-player games.")
with util.make_temporary(_generate_lrs_input(game)) as infn:
result = subprocess.run([lrsnash_path, infn], encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if result.returncode != 0:
raise ValueError(f"PHC run failed with return code {result.returncode}")
return _parse_lrs_output(game, result.stdout)


def main():
game = gbt.Game.parse_game(sys.stdin.read())
eqa = lrsnash_solve(game, "./lrsnash")
for eqm in eqa:
print("NE," +
",".join(str(eqm[strat]) for player in game.players for strat in player.strategies))


if __name__ == "__main__":
main()
35 changes: 9 additions & 26 deletions src/pygambit/nashphc.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Enumerate Nash equilibria by solving systems of polynomial equations using PHCpack.
"""

from __future__ import annotations

import contextlib
import itertools
import pathlib
import string
import subprocess
import sys
import tempfile
import typing

import pygambit as gbt
import pygambit.util as util


def _process_phc_output(output: str) -> list[dict]:
Expand Down Expand Up @@ -71,27 +73,7 @@ def _process_phc_output(output: str) -> list[dict]:
return solutions


@contextlib.contextmanager
def _make_temporary(content: typing.Optional[str] = None) -> pathlib.Path:
"""Context manager to create a temporary file containing `content', and
provide the path to the temporary file.
If `content' is none, the temporary file is created and then deleted, while
returning the filename, for another process then to write to that file
(under the assumption that it is extremely unlikely that another program
will try to write to that same tempfile name).
"""
with tempfile.NamedTemporaryFile("w", delete=(content is None)) as f:
if content:
f.write(content)
filepath = pathlib.Path(f.name)
try:
yield filepath
finally:
filepath.unlink(missing_ok=True)


def _run_phc(phcpack_path: typing.Union[pathlib.Path, str], equations: list[str]) -> list[dict]:
def _run_phc(phcpack_path: pathlib.Path | str, equations: list[str]) -> list[dict]:
"""Call PHCpack via an external binary to solve a set of equations, and return
the details on solutions found.
Expand Down Expand Up @@ -123,8 +105,9 @@ def _run_phc(phcpack_path: typing.Union[pathlib.Path, str], equations: list[str]
- `res'
"""
with (
_make_temporary(f"{len(equations)}\n" + ";\n".join(equations) + ";\n\n\n") as infn,
_make_temporary() as outfn
util.make_temporary(f"{len(equations)}\n" +
";\n".join(equations) + ";\n\n\n") as infn,
util.make_temporary() as outfn
):
result = subprocess.run([phcpack_path, "-b", infn, outfn])
if result.returncode != 0:
Expand Down Expand Up @@ -230,7 +213,7 @@ def _profile_from_support(support: gbt.StrategySupportProfile) -> gbt.MixedStrat


def _solve_support(support: gbt.StrategySupportProfile,
phcpack_path: typing.Union[pathlib.Path, str],
phcpack_path: pathlib.Path | str,
maxregret: float,
negtol: float,
onsupport=lambda x, label: None,
Expand Down Expand Up @@ -259,7 +242,7 @@ def _solve_support(support: gbt.StrategySupportProfile,
return profiles


def phcpack_solve(game: gbt.Game, phcpack_path: typing.Union[pathlib.Path, str],
def phcpack_solve(game: gbt.Game, phcpack_path: pathlib.Path | str,
maxregret: float) -> list[gbt.MixedStrategyProfileDouble]:
negtol = 1.0e-6
return [
Expand Down
24 changes: 24 additions & 0 deletions src/pygambit/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import contextlib
import pathlib
import tempfile
import typing


@contextlib.contextmanager
def make_temporary(content: typing.Optional[str] = None) -> pathlib.Path:
"""Context manager to create a temporary file containing `content', and
provide the path to the temporary file.
If `content' is none, the temporary file is created and then deleted, while
returning the filename, for another process then to write to that file
(under the assumption that it is extremely unlikely that another program
will try to write to that same tempfile name).
"""
with tempfile.NamedTemporaryFile("w", delete=(content is None)) as f:
if content:
f.write(content)
filepath = pathlib.Path(f.name)
try:
yield filepath
finally:
filepath.unlink(missing_ok=True)
Loading

0 comments on commit 1d5c3c4

Please sign in to comment.