2 Unit tests for the :class:`SymmetricLinearGame` class.
5 from unittest
import TestCase
7 from dunshire
.games
import SymmetricLinearGame
8 from dunshire
.matrices
import eigenvalues_re
, inner_product
, norm
9 from dunshire
import options
10 from .randomgen
import (random_icecream_game
, random_ll_icecream_game
,
11 random_ll_orthant_game
, random_nn_scaling
,
12 random_orthant_game
, random_positive_orthant_game
,
16 # Tell pylint to shut up about the large number of methods.
17 class SymmetricLinearGameTest(TestCase
): # pylint: disable=R0904
19 Tests for the SymmetricLinearGame and Solution classes.
21 def assert_within_tol(self
, first
, second
, modifier
=1):
23 Test that ``first`` and ``second`` are equal within a multiple of
24 our default tolerances.
30 The first number to compare.
33 The second number to compare.
36 A scaling factor (default: 1) applied to the default
37 tolerance for this comparison. If you have a poorly-
38 conditioned matrix, for example, you may want to set this
42 self
.assertTrue(abs(first
- second
) < options
.ABS_TOL
*modifier
)
45 def test_solutions_dont_change(self
):
47 If we solve the same problem twice, we should get
48 the same answer both times.
50 G
= random_orthant_game()
53 p1_diff
= norm(soln1
.player1_optimal() - soln2
.player1_optimal())
54 p2_diff
= norm(soln1
.player2_optimal() - soln2
.player2_optimal())
55 gv_diff
= abs(soln1
.game_value() - soln2
.game_value())
57 p1_close
= p1_diff
< options
.ABS_TOL
58 p2_close
= p2_diff
< options
.ABS_TOL
59 gv_close
= gv_diff
< options
.ABS_TOL
61 self
.assertTrue(p1_close
and p2_close
and gv_close
)
64 def test_condition_lower_bound(self
):
66 Ensure that the condition number of a game is greater than or
69 It should be safe to compare these floats directly: we compute
70 the condition number as the ratio of one nonnegative real number
71 to a smaller nonnegative real number.
73 G
= random_orthant_game()
74 self
.assertTrue(G
.condition() >= 1.0)
75 G
= random_icecream_game()
76 self
.assertTrue(G
.condition() >= 1.0)
79 def assert_scaling_works(self
, G
):
81 Test that scaling ``L`` by a nonnegative number scales the value
82 of the game by the same number.
84 (alpha
, H
) = random_nn_scaling(G
)
85 value1
= G
.solution().game_value()
86 value2
= H
.solution().game_value()
87 modifier
= 4*max(abs(alpha
), 1)
88 self
.assert_within_tol(alpha
*value1
, value2
, modifier
)
91 def test_scaling_orthant(self
):
93 Test that scaling ``L`` by a nonnegative number scales the value
94 of the game by the same number over the nonnegative orthant.
96 G
= random_orthant_game()
97 self
.assert_scaling_works(G
)
100 def test_scaling_icecream(self
):
102 The same test as :meth:`test_nonnegative_scaling_orthant`,
103 except over the ice cream cone.
105 G
= random_icecream_game()
106 self
.assert_scaling_works(G
)
109 def assert_translation_works(self
, G
):
111 Check that translating ``L`` by alpha*(e1*e2.trans()) increases
112 the value of the associated game by alpha.
114 # We need to use ``L`` later, so make sure we transpose it
115 # before passing it in as a column-indexed matrix.
117 value1
= soln1
.game_value()
118 x_bar
= soln1
.player1_optimal()
119 y_bar
= soln1
.player2_optimal()
121 # This is the "correct" representation of ``M``, but COLUMN
123 (alpha
, H
) = random_translation(G
)
124 value2
= H
.solution().game_value()
126 modifier
= 4*max(abs(alpha
), 1)
127 self
.assert_within_tol(value1
+ alpha
, value2
, modifier
)
129 # Make sure the same optimal pair works.
130 self
.assert_within_tol(value2
, H
.payoff(x_bar
, y_bar
), modifier
)
133 def test_translation_orthant(self
):
135 Test that translation works over the nonnegative orthant.
137 G
= random_orthant_game()
138 self
.assert_translation_works(G
)
141 def test_translation_icecream(self
):
143 The same as :meth:`test_translation_orthant`, except over the
146 G
= random_icecream_game()
147 self
.assert_translation_works(G
)
150 def assert_opposite_game_works(self
, G
):
152 Check the value of the "opposite" game that gives rise to a
153 value that is the negation of the original game. Comes from
156 # This is the "correct" representation of ``M``, but
160 # so we have to transpose it when we feed it to the constructor.
161 # Note: the condition number of ``H`` should be comparable to ``G``.
162 H
= SymmetricLinearGame(M
.trans(), G
.K(), G
.e2(), G
.e1())
165 x_bar
= soln1
.player1_optimal()
166 y_bar
= soln1
.player2_optimal()
169 # The modifier of 4 is because each could be off by 2*ABS_TOL,
170 # which is how far apart the primal/dual objectives have been
172 self
.assert_within_tol(-soln1
.game_value(), soln2
.game_value(), 4)
174 # Make sure the switched optimal pair works. Since x_bar and
175 # y_bar come from G, we use the same modifier.
176 self
.assert_within_tol(soln2
.game_value(), H
.payoff(y_bar
, x_bar
), 4)
180 def test_opposite_game_orthant(self
):
182 Test the value of the "opposite" game over the nonnegative
185 G
= random_orthant_game()
186 self
.assert_opposite_game_works(G
)
189 def test_opposite_game_icecream(self
):
191 Like :meth:`test_opposite_game_orthant`, except over the
194 G
= random_icecream_game()
195 self
.assert_opposite_game_works(G
)
198 def assert_orthogonality(self
, G
):
200 Two orthogonality relations hold at an optimal solution, and we
204 x_bar
= soln
.player1_optimal()
205 y_bar
= soln
.player2_optimal()
206 value
= soln
.game_value()
208 ip1
= inner_product(y_bar
, G
.L()*x_bar
- value
*G
.e1())
209 ip2
= inner_product(value
*G
.e2() - G
.L().trans()*y_bar
, x_bar
)
211 # Huh.. well, y_bar and x_bar can each be epsilon away, but
212 # x_bar is scaled by L, so that's (norm(L) + 1), and then
213 # value could be off by epsilon, so that's another norm(e1) or
214 # norm(e2). On the other hand, this test seems to pass most of
215 # the time even with a modifier of one. How about.. four?
216 self
.assert_within_tol(ip1
, 0, 4)
217 self
.assert_within_tol(ip2
, 0, 4)
220 def test_orthogonality_orthant(self
):
222 Check the orthgonality relationships that hold for a solution
223 over the nonnegative orthant.
225 G
= random_orthant_game()
226 self
.assert_orthogonality(G
)
229 def test_orthogonality_icecream(self
):
231 Check the orthgonality relationships that hold for a solution
232 over the ice-cream cone.
234 G
= random_icecream_game()
235 self
.assert_orthogonality(G
)
238 def test_positive_operator_value(self
):
240 Test that a positive operator on the nonnegative orthant gives
241 rise to a a game with a nonnegative value.
243 This test theoretically applies to the ice-cream cone as well,
244 but we don't know how to make positive operators on that cone.
246 G
= random_positive_orthant_game()
247 self
.assertTrue(G
.solution().game_value() >= -options
.ABS_TOL
)
250 def assert_lyapunov_works(self
, G
):
252 Check that Lyapunov games act the way we expect.
256 # We only check for positive/negative stability if the game
257 # value is not basically zero. If the value is that close to
258 # zero, we just won't check any assertions.
260 # See :meth:`assert_within_tol` for an explanation of the
262 eigs
= eigenvalues_re(G
.L())
264 if soln
.game_value() > options
.ABS_TOL
:
265 # L should be positive stable
266 positive_stable
= all([eig
> -options
.ABS_TOL
for eig
in eigs
])
267 self
.assertTrue(positive_stable
)
268 elif soln
.game_value() < -options
.ABS_TOL
:
269 # L should be negative stable
270 negative_stable
= all([eig
< options
.ABS_TOL
for eig
in eigs
])
271 self
.assertTrue(negative_stable
)
273 # The dual game's value should always equal the primal's.
274 # The modifier of 4 is because even though the games are dual,
275 # CVXOPT doesn't know that, and each could be off by 2*ABS_TOL.
276 dualsoln
= G
.dual().solution()
277 self
.assert_within_tol(dualsoln
.game_value(), soln
.game_value(), 4)
280 def test_lyapunov_orthant(self
):
282 Test that a Lyapunov game on the nonnegative orthant works.
284 G
= random_ll_orthant_game()
285 self
.assert_lyapunov_works(G
)
288 def test_lyapunov_icecream(self
):
290 Test that a Lyapunov game on the ice-cream cone works.
292 G
= random_ll_icecream_game()
293 self
.assert_lyapunov_works(G
)