Skip to content

Commit 549b2e6

Browse files
committed
Suggestion for writing a generic Nash tester.
1 parent 156083b commit 549b2e6

File tree

1 file changed

+66
-47
lines changed

1 file changed

+66
-47
lines changed

tests/test_nash.py

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import dataclasses
1111
import functools
1212
import typing
13-
from fractions import Fraction as Q
1413

1514
import pytest
1615

1716
import pygambit as gbt
17+
from pygambit import Rational as Q
1818

1919
from . import games
2020

@@ -52,74 +52,93 @@ def d(*probs) -> tuple:
5252
@dataclasses.dataclass
5353
class EquilibriumTestCase:
5454
"""Summarising the data relevant for a test fixture of a call to an equilibrium solver."""
55-
label: str
5655
factory: typing.Callable[[], gbt.Game]
56+
solver: typing.Callable[[gbt.Game], gbt.nash.NashComputationResult]
5757
expected: list
58+
regret_tol: float | gbt.Rational = Q(0)
59+
prob_tol: float | gbt.Rational = Q(0)
5860

5961

6062
NASH_ENUMMIXED_RATIONAL_CASES = [
61-
EquilibriumTestCase(
62-
label="test1",
63-
factory=games.create_stripped_down_poker_efg,
64-
expected=[
65-
[d(Q("1/3"), Q("2/3"), 0, 0), d(Q("2/3"), Q("1/3"))],
66-
]
67-
),
68-
EquilibriumTestCase(
69-
label="test2",
70-
factory=games.create_one_shot_trust_efg,
71-
expected=[
72-
[d(0, 1), d(Q("1/2"), Q("1/2"))],
73-
[d(0, 1), d(0, 1)],
74-
]
63+
pytest.param(
64+
EquilibriumTestCase(
65+
factory=games.create_stripped_down_poker_efg,
66+
solver=functools.partial(gbt.nash.enummixed_solve, rational=True),
67+
expected=[
68+
[d(Q("1/3"), Q("2/3"), 0, 0), d(Q("2/3"), Q("1/3"))],
69+
],
70+
),
71+
marks=pytest.mark.nash_enummixed_strategy,
72+
id="test1",
7573
),
76-
EquilibriumTestCase(
77-
label="test3",
78-
factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3),
79-
expected=[
80-
[d(1, 0, 0), d(1, 0, 0)],
81-
[d(Q("1/2"), Q("1/2"), 0), d(Q("1/2"), Q("1/2"), 0)],
82-
[d(Q("1/3"), Q("1/3"), Q("1/3")), d(Q("1/3"), Q("1/3"), Q("1/3"))],
83-
[d(Q("1/2"), 0, Q("1/2")), d(Q("1/2"), 0, Q("1/2"))],
84-
[d(0, 1, 0), d(0, 1, 0)],
85-
[d(0, Q("1/2"), Q("1/2")), d(0, Q("1/2"), Q("1/2"))],
86-
[d(0, 0, 1), d(0, 0, 1)],
87-
]
74+
pytest.param(
75+
EquilibriumTestCase(
76+
factory=games.create_one_shot_trust_efg,
77+
solver=functools.partial(gbt.nash.enummixed_solve, rational=True),
78+
expected=[
79+
[d(0, 1), d(Q("1/2"), Q("1/2"))],
80+
[d(0, 1), d(0, 1)],
81+
],
82+
),
83+
marks=pytest.mark.nash_enummixed_strategy,
84+
id="test2",
8885
),
89-
EquilibriumTestCase(
90-
label="test4",
91-
factory=games.create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq,
92-
expected=[
93-
[d(Q("1/30"), Q("1/6"), Q("3/10"), Q("3/10"), Q("1/6"), Q("1/30")),
94-
d(Q("1/6"), Q("1/30"), Q("3/10"), Q("3/10"), Q("1/30"), Q("1/6"))],
95-
]
86+
pytest.param(
87+
EquilibriumTestCase(
88+
factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3),
89+
solver=functools.partial(gbt.nash.enummixed_solve, rational=True),
90+
expected=[
91+
[d(1, 0, 0), d(1, 0, 0)],
92+
[d(Q("1/2"), Q("1/2"), 0), d(Q("1/2"), Q("1/2"), 0)],
93+
[d(Q("1/3"), Q("1/3"), Q("1/3")), d(Q("1/3"), Q("1/3"), Q("1/3"))],
94+
[d(Q("1/2"), 0, Q("1/2")), d(Q("1/2"), 0, Q("1/2"))],
95+
[d(0, 1, 0), d(0, 1, 0)],
96+
[d(0, Q("1/2"), Q("1/2")), d(0, Q("1/2"), Q("1/2"))],
97+
[d(0, 0, 1), d(0, 0, 1)],
98+
]
99+
),
100+
marks=pytest.mark.nash_enummixed_strategy,
101+
id="test3",
96102
),
103+
pytest.param(
104+
EquilibriumTestCase(
105+
factory=games.create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq,
106+
solver=functools.partial(gbt.nash.enummixed_solve, rational=True),
107+
expected=[
108+
[d(Q("1/30"), Q("1/6"), Q("3/10"), Q("3/10"), Q("1/6"), Q("1/30")),
109+
d(Q("1/6"), Q("1/30"), Q("3/10"), Q("3/10"), Q("1/30"), Q("1/6"))],
110+
]
111+
),
112+
marks=pytest.mark.nash_enummixed_strategy,
113+
id="test4",
114+
)
97115
]
98116

99117

100118
@pytest.mark.nash
101-
@pytest.mark.nash_enummixed_strategy
102119
@pytest.mark.parametrize(
103120
"test_case", NASH_ENUMMIXED_RATIONAL_CASES, ids=lambda c: c.label
104121
)
105-
def test_enummixed_rational(
106-
test_case: EquilibriumTestCase,
107-
subtests,
108-
) -> None:
109-
"""Test calls of enumeration of extreme mixed strategy equilibria, rational precision
110-
111-
Tests max regret being zero (internal consistency) and compares the computed sequence of
112-
extreme equilibria to a previously-computed sequence (regression test)
122+
def test_nash_strategy_solver(test_case: EquilibriumTestCase, subtests) -> None:
123+
"""Test calls of Nash solvers.
124+
125+
Subtests:
126+
- Max regret no more than `test_case.regret_tol`
127+
- Equilibria are output in the expected order. Equilibria are deemed to match if the maximum
128+
difference in probabilities is no more than `test_case.prob_tol`
113129
"""
114130
game = test_case.factory()
115-
result = gbt.nash.enummixed_solve(game, rational=True)
131+
result = test_case.solver(game)
116132
with subtests.test("number of equilibria found"):
117133
assert len(result.equilibria) == len(test_case.expected)
118134
for (i, (eq, exp)) in enumerate(zip(result.equilibria, test_case.expected, strict=True)):
119135
with subtests.test(eq=i, check="max_regret"):
120-
assert eq.max_regret() == 0
136+
assert eq.max_regret() <= test_case.regret_tol
121137
with subtests.test(eq=i, check="strategy_profile"):
122-
assert eq == game.mixed_strategy_profile(rational=True, data=exp)
138+
expected = game.mixed_strategy_profile(rational=True, data=exp)
139+
for player in game.players:
140+
for strategy in player.strategies:
141+
assert abs(eq[strategy] - expected[strategy]) <= test_case.prob_tol
123142

124143

125144
@pytest.mark.nash

0 commit comments

Comments
 (0)