|
10 | 10 | import dataclasses |
11 | 11 | import functools |
12 | 12 | import typing |
13 | | -from fractions import Fraction as Q |
14 | 13 |
|
15 | 14 | import pytest |
16 | 15 |
|
17 | 16 | import pygambit as gbt |
| 17 | +from pygambit import Rational as Q |
18 | 18 |
|
19 | 19 | from . import games |
20 | 20 |
|
@@ -52,74 +52,93 @@ def d(*probs) -> tuple: |
52 | 52 | @dataclasses.dataclass |
53 | 53 | class EquilibriumTestCase: |
54 | 54 | """Summarising the data relevant for a test fixture of a call to an equilibrium solver.""" |
55 | | - label: str |
56 | 55 | factory: typing.Callable[[], gbt.Game] |
| 56 | + solver: typing.Callable[[gbt.Game], gbt.nash.NashComputationResult] |
57 | 57 | expected: list |
| 58 | + regret_tol: float | gbt.Rational = Q(0) |
| 59 | + prob_tol: float | gbt.Rational = Q(0) |
58 | 60 |
|
59 | 61 |
|
60 | 62 | 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", |
75 | 73 | ), |
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", |
88 | 85 | ), |
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", |
96 | 102 | ), |
| 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 | + ) |
97 | 115 | ] |
98 | 116 |
|
99 | 117 |
|
100 | 118 | @pytest.mark.nash |
101 | | -@pytest.mark.nash_enummixed_strategy |
102 | 119 | @pytest.mark.parametrize( |
103 | 120 | "test_case", NASH_ENUMMIXED_RATIONAL_CASES, ids=lambda c: c.label |
104 | 121 | ) |
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` |
113 | 129 | """ |
114 | 130 | game = test_case.factory() |
115 | | - result = gbt.nash.enummixed_solve(game, rational=True) |
| 131 | + result = test_case.solver(game) |
116 | 132 | with subtests.test("number of equilibria found"): |
117 | 133 | assert len(result.equilibria) == len(test_case.expected) |
118 | 134 | for (i, (eq, exp)) in enumerate(zip(result.equilibria, test_case.expected, strict=True)): |
119 | 135 | with subtests.test(eq=i, check="max_regret"): |
120 | | - assert eq.max_regret() == 0 |
| 136 | + assert eq.max_regret() <= test_case.regret_tol |
121 | 137 | 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 |
123 | 142 |
|
124 | 143 |
|
125 | 144 | @pytest.mark.nash |
|
0 commit comments