Skip to content

Commit c76441f

Browse files
rahulsavanitturocy
andauthored
Lemke on trees stops after finding 1 eq; stop_after/max_depth not used (#722)
Lemke's algorjthm, used for `lcp_solve` using the sequence form, finds only one equilibrium. Previously this method was advertised as having the possibility of recursing to find more. At least as implemented, this is not mathematically well-founded, and actually the implementation would not find another equilibrium after the first (unless possibly by pure luck). This removes the `stop_after` and `max_depth` options from `lcp_solve` on sequence forms to clarify the operation of the method. --------- Co-authored-by: Ted Turocy <ted.turocy@gmail.com>
1 parent 867d2b8 commit c76441f

File tree

7 files changed

+64
-121
lines changed

7 files changed

+64
-121
lines changed

ChangeLog

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
### Fixed
66
- Sequence-form based equilibrium-finding methods returned incorrect output on games with
77
outcomes at non-terminal nodes. (#654)
8+
89
### Added
910
- Implement `IsAbsentMinded()` on information sets (C++) and `Infoset.is_absent_minded` (Python)
1011
to detect if an information is absent-minded.
11-
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
12+
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior strategies
1213
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
1314
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
1415
taken by the player before reaching the node or information set, respectively. (#582)
@@ -27,6 +28,8 @@
2728
distinguish "agent" regret and liap values from their strategy-based analogs. Methods which compute
2829
using the agent-form - specifically `enumpure_solve` and `liap_solve`, now clarify this by being named
2930
differently in `pygambit`. (#617)
31+
- For clarity, the `stop_after` and `max_depth` arguments to `lcp_solve` are no longer permitted when solving using
32+
the sequence form. These actually had no effect in previous versions. (#671)
3033
- In the graphical interface, removed option to configure information set link drawing; information sets
3134
are always drawn and indicators are always drawn if an information set spans multiple levels.
3235
- In `pygambit`, indexing the children of a node by a string inteprets the string as an action label,

doc/tools.lcp.rst

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66

77
:program:`gambit-lcp` reads a two-player game on standard input and
88
computes Nash equilibria by finding solutions to a linear
9-
complementarity problem. For extensive games, the program uses the
9+
complementarity problem.
10+
11+
For extensive games, the program uses the
1012
sequence form representation of the extensive game, as defined by
1113
Koller, Megiddo, and von Stengel [KolMegSte94]_, and applies the
12-
algorithm developed by Lemke. For strategic games, the program uses
13-
the method of Lemke and Howson [LemHow64]_. There exist strategic
14-
games for which some equilibria cannot be located by this method; see
15-
Shapley [Sha74]_.
14+
algorithm developed by Lemke. In that case, the method will find
15+
one Nash equilibrium.
16+
17+
For strategic games, the program uses the method of Lemke and Howson
18+
[LemHow64]_. In this case, the method will find all "accessible"
19+
equilibria, i.e., those that can be found as concatenations of Lemke-Howson
20+
paths that start at the artificial equilibrium.
21+
There exist strategic-form games for which some equilibria cannot be found
22+
by this method, i.e., some equilibria are inaccessible; see Shapley [Sha74]_.
1623

1724
In a two-player strategic game, the set of Nash equilibria can be expressed
1825
as the union of convex sets. This program will find extreme points
@@ -53,9 +60,11 @@ game.
5360

5461
.. cmdoption:: -e EQA
5562

56-
By default, the program will find all equilibria accessible from
57-
the origin of the polytopes. This switch instructs the program
58-
to terminate when EQA equilibria have been found.
63+
By default, when working with the reduced strategic game, the program
64+
will find all equilibria accessible from the origin of the polytopes.
65+
This switch instructs the program to terminate when EQA equilibria
66+
have been found. This has no effect when using the extensive representation
67+
of a game, in which case the method always only returns one equilibrium.
5968

6069
.. cmdoption:: -h
6170

src/pygambit/nash.pxi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ def _enummixed_strategy_solve_rational(game: Game) -> list[MixedStrategyProfileR
7474

7575

7676
def _lcp_behavior_solve_double(
77-
game: Game, stop_after: int, max_depth: int
77+
game: Game
7878
) -> list[MixedBehaviorProfileDouble]:
79-
return _convert_mbpd(LcpBehaviorSolve[double](game.game, stop_after, max_depth))
79+
return _convert_mbpd(LcpBehaviorSolve[double](game.game))
8080

8181

8282
def _lcp_behavior_solve_rational(
83-
game: Game, stop_after: int, max_depth: int
83+
game: Game
8484
) -> list[MixedBehaviorProfileRational]:
85-
return _convert_mbpr(LcpBehaviorSolve[c_Rational](game.game, stop_after, max_depth))
85+
return _convert_mbpr(LcpBehaviorSolve[c_Rational](game.game))
8686

8787

8888
def _lcp_strategy_solve_double(

src/pygambit/nash.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,13 @@ def lcp_solve(
210210
representation even if the game's native representation is extensive.
211211
212212
stop_after : int, optional
213-
Maximum number of equilibria to compute. If not specified, computes all
214-
accessible equilibria.
213+
Maximum number of equilibria to compute when using the strategic representation.
214+
If not specified, computes all accessible equilibria.
215215
216216
max_depth : int, optional
217-
Maximum depth of recursion. If specified, will limit the recursive search,
218-
but may result in some accessible equilibria not being found.
217+
Maximum depth of recursion when using the strategic representation.
218+
If specified, will limit the recursive search, but may result in some accessible
219+
equilibria not being found.
219220
220221
Returns
221222
-------
@@ -226,24 +227,32 @@ def lcp_solve(
226227
------
227228
RuntimeError
228229
If game has more than two players.
230+
231+
ValueError
232+
If stop_after or max_depth are supplied for use on the tree representation.
229233
"""
230-
if stop_after is None:
231-
stop_after = 0
232-
elif stop_after < 0:
234+
if game.is_tree and not use_strategic:
235+
if stop_after is not None:
236+
raise ValueError(
237+
"lcp_solve(): stop_after can only be used on the strategic representation"
238+
)
239+
if max_depth is not None:
240+
raise ValueError(
241+
"lcp_solve(): max_depth can only be used on the strategic representation"
242+
)
243+
if stop_after is not None and stop_after < 0:
233244
raise ValueError(
234245
f"lcp_solve(): stop_after argument must be a non-negative number; got {stop_after}"
235246
)
236-
if max_depth is None:
237-
max_depth = 0
238247
if not game.is_tree or use_strategic:
239248
if rational:
240249
equilibria = libgbt._lcp_strategy_solve_rational(game, stop_after or 0, max_depth or 0)
241250
else:
242251
equilibria = libgbt._lcp_strategy_solve_double(game, stop_after or 0, max_depth or 0)
243252
elif rational:
244-
equilibria = libgbt._lcp_behavior_solve_rational(game, stop_after or 0, max_depth or 0)
253+
equilibria = libgbt._lcp_behavior_solve_rational(game)
245254
else:
246-
equilibria = libgbt._lcp_behavior_solve_double(game, stop_after or 0, max_depth or 0)
255+
equilibria = libgbt._lcp_behavior_solve_double(game)
247256
return NashComputationResult(
248257
game=game,
249258
method="lcp",

src/solvers/lcp/efglcp.cc

Lines changed: 16 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ namespace Gambit::Nash {
2929

3030
template <class T> class NashLcpBehaviorSolver {
3131
public:
32-
NashLcpBehaviorSolver(int p_stopAfter, int p_maxDepth,
33-
BehaviorCallbackType<T> p_onEquilibrium = NullBehaviorCallback<T>)
34-
: m_onEquilibrium(p_onEquilibrium), m_stopAfter(p_stopAfter), m_maxDepth(p_maxDepth)
32+
NashLcpBehaviorSolver(BehaviorCallbackType<T> p_onEquilibrium = NullBehaviorCallback<T>)
33+
: m_onEquilibrium(p_onEquilibrium)
3534
{
3635
}
3736
~NashLcpBehaviorSolver() = default;
@@ -40,7 +39,6 @@ template <class T> class NashLcpBehaviorSolver {
4039

4140
private:
4241
BehaviorCallbackType<T> m_onEquilibrium;
43-
int m_stopAfter, m_maxDepth;
4442

4543
class Solution;
4644

@@ -145,98 +143,23 @@ std::list<MixedBehaviorProfile<T>> NashLcpBehaviorSolver<T>::Solve(const Game &p
145143
solution.eps = tab.Epsilon();
146144

147145
try {
148-
if (m_stopAfter != 1) {
149-
try {
150-
AllLemke(p_game, solution.ns1 + solution.ns2 + 1, tab, 0, A, solution);
151-
}
152-
catch (EquilibriumLimitReached &) {
153-
// Handle this silently; equilibria are recorded as found so no action needed
154-
}
155-
}
156-
else {
157-
tab.Pivot(solution.ns1 + solution.ns2 + 1, 0);
158-
tab.SF_LCPPath(solution.ns1 + solution.ns2 + 1);
159-
solution.AddBFS(tab);
160-
Vector<T> sol(tab.MinRow(), tab.MaxRow());
161-
tab.BasisVector(sol);
162-
MixedBehaviorProfile<T> profile(p_game);
163-
GetProfile(tab, profile, sol, p_game->GetRoot(), 1, 1, solution);
164-
profile.UndefinedToCentroid();
165-
solution.m_equilibria.push_back(profile);
166-
this->m_onEquilibrium(profile, "NE");
167-
}
146+
tab.Pivot(solution.ns1 + solution.ns2 + 1, 0);
147+
tab.SF_LCPPath(solution.ns1 + solution.ns2 + 1);
148+
solution.AddBFS(tab);
149+
Vector<T> sol(tab.MinRow(), tab.MaxRow());
150+
tab.BasisVector(sol);
151+
MixedBehaviorProfile<T> profile(p_game);
152+
GetProfile(tab, profile, sol, p_game->GetRoot(), 1, 1, solution);
153+
profile.UndefinedToCentroid();
154+
solution.m_equilibria.push_back(profile);
155+
this->m_onEquilibrium(profile, "NE");
168156
}
169157
catch (std::runtime_error &e) {
170158
std::cerr << "Error: " << e.what() << std::endl;
171159
}
172160
return solution.m_equilibria;
173161
}
174162

175-
//
176-
// All_Lemke finds all accessible Nash equilibria by recursively
177-
// calling itself. List maintains the list of basic variables
178-
// for the equilibria that have already been found.
179-
// From each new accessible equilibrium, it follows
180-
// all possible paths, adding any new equilibria to the List.
181-
//
182-
template <class T>
183-
void NashLcpBehaviorSolver<T>::AllLemke(const Game &p_game, int j, linalg::LemkeTableau<T> &B,
184-
int depth, Matrix<T> &A, Solution &p_solution) const
185-
{
186-
if (m_maxDepth != 0 && depth > m_maxDepth) {
187-
return;
188-
}
189-
190-
Vector<T> sol(B.MinRow(), B.MaxRow());
191-
MixedBehaviorProfile<T> profile(p_game);
192-
193-
bool newsol = false;
194-
for (int i = B.MinRow(); i <= B.MaxRow() && !newsol; i++) {
195-
if (i == j) {
196-
continue;
197-
}
198-
199-
linalg::LemkeTableau<T> BCopy(B);
200-
// Perturb tableau by a small number
201-
A(i, 0) = static_cast<T>(-1) / static_cast<T>(1000);
202-
BCopy.Refactor();
203-
204-
int missing;
205-
if (depth == 0) {
206-
BCopy.Pivot(j, 0);
207-
missing = -j;
208-
}
209-
else {
210-
missing = BCopy.SF_PivotIn(0);
211-
}
212-
213-
newsol = false;
214-
215-
if (BCopy.SF_LCPPath(-missing) == 1) {
216-
newsol = p_solution.AddBFS(BCopy);
217-
BCopy.BasisVector(sol);
218-
GetProfile(BCopy, profile, sol, p_game->GetRoot(), 1, 1, p_solution);
219-
profile.UndefinedToCentroid();
220-
if (newsol) {
221-
m_onEquilibrium(profile, "NE");
222-
p_solution.m_equilibria.push_back(profile);
223-
if (m_stopAfter > 0 && p_solution.EquilibriumCount() >= m_stopAfter) {
224-
throw EquilibriumLimitReached();
225-
}
226-
}
227-
}
228-
else {
229-
// Dead end
230-
}
231-
232-
A(i, 0) = static_cast<T>(-1);
233-
if (newsol) {
234-
BCopy.Refactor();
235-
AllLemke(p_game, i, BCopy, depth + 1, A, p_solution);
236-
}
237-
}
238-
}
239-
240163
template <class T>
241164
void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T prob, int s1, int s2,
242165
T payoff1, T payoff2, Solution &p_solution) const
@@ -342,16 +265,15 @@ void NashLcpBehaviorSolver<T>::GetProfile(const linalg::LemkeTableau<T> &tab,
342265
}
343266

344267
template <class T>
345-
std::list<MixedBehaviorProfile<T>> LcpBehaviorSolve(const Game &p_game, int p_stopAfter,
346-
int p_maxDepth,
268+
std::list<MixedBehaviorProfile<T>> LcpBehaviorSolve(const Game &p_game,
347269
BehaviorCallbackType<T> p_onEquilibrium)
348270
{
349-
return NashLcpBehaviorSolver<T>(p_stopAfter, p_maxDepth, p_onEquilibrium).Solve(p_game);
271+
return NashLcpBehaviorSolver<T>(p_onEquilibrium).Solve(p_game);
350272
}
351273

352-
template std::list<MixedBehaviorProfile<double>> LcpBehaviorSolve(const Game &, int, int,
274+
template std::list<MixedBehaviorProfile<double>> LcpBehaviorSolve(const Game &,
353275
BehaviorCallbackType<double>);
354276
template std::list<MixedBehaviorProfile<Rational>>
355-
LcpBehaviorSolve(const Game &, int, int, BehaviorCallbackType<Rational>);
277+
LcpBehaviorSolve(const Game &, BehaviorCallbackType<Rational>);
356278

357279
} // end namespace Gambit::Nash

src/solvers/lcp/lcp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ LcpStrategySolve(const Game &p_game, int p_stopAfter, int p_maxDepth,
3434

3535
template <class T>
3636
std::list<MixedBehaviorProfile<T>>
37-
LcpBehaviorSolve(const Game &p_game, int p_stopAfter, int p_maxDepth,
37+
LcpBehaviorSolve(const Game &p_game,
3838
BehaviorCallbackType<T> p_onEquilibrium = NullBehaviorCallback<T>);
3939

4040
} // end namespace Gambit::Nash

src/tools/lcp/lcp.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,14 @@ int main(int argc, char *argv[])
149149
if (useFloat) {
150150
auto renderer =
151151
MakeMixedBehaviorProfileRenderer<double>(std::cout, numDecimals, printDetail);
152-
LcpBehaviorSolve<double>(game, stopAfter, maxDepth,
152+
LcpBehaviorSolve<double>(game,
153153
[&](const MixedBehaviorProfile<double> &p,
154154
const std::string &label) { renderer->Render(p, label); });
155155
}
156156
else {
157157
auto renderer =
158158
MakeMixedBehaviorProfileRenderer<Rational>(std::cout, numDecimals, printDetail);
159-
LcpBehaviorSolve<Rational>(game, stopAfter, maxDepth,
159+
LcpBehaviorSolve<Rational>(game,
160160
[&](const MixedBehaviorProfile<Rational> &p,
161161
const std::string &label) { renderer->Render(p, label); });
162162
}

0 commit comments

Comments
 (0)