2 Unit tests for the :class:`SymmetricLinearGame` class.
5 from unittest
import TestCase
7 from dunshire
.cones
import NonnegativeOrthant
8 from dunshire
.games
import SymmetricLinearGame
9 from dunshire
.matrices
import eigenvalues_re
, inner_product
, norm
10 from dunshire
import options
11 from .randomgen
import (RANDOM_MAX
, random_icecream_game
,
12 random_ll_icecream_game
, random_ll_orthant_game
,
13 random_nn_scaling
, random_orthant_game
,
14 random_positive_orthant_game
, random_translation
)
17 # Tell pylint to shut up about the large number of methods.
18 class SymmetricLinearGameTest(TestCase
): # pylint: disable=R0904
20 Tests for the SymmetricLinearGame and Solution classes.
22 def assert_within_tol(self
, first
, second
, modifier
=1):
24 Test that ``first`` and ``second`` are equal within a multiple of
25 our default tolerances.
31 The first number to compare.
34 The second number to compare.
37 A scaling factor (default: 1) applied to the default
38 tolerance for this comparison. If you have a poorly-
39 conditioned matrix, for example, you may want to set this
43 self
.assertTrue(abs(first
- second
) < options
.ABS_TOL
*modifier
)
47 def test_condition_lower_bound(self
):
49 Ensure that the condition number of a game is greater than or
52 It should be safe to compare these floats directly: we compute
53 the condition number as the ratio of one nonnegative real number
54 to a smaller nonnegative real number.
56 G
= random_orthant_game()
57 self
.assertTrue(G
.condition() >= 1.0)
58 G
= random_icecream_game()
59 self
.assertTrue(G
.condition() >= 1.0)
62 def assert_scaling_works(self
, G
):
64 Test that scaling ``L`` by a nonnegative number scales the value
65 of the game by the same number.
67 (alpha
, H
) = random_nn_scaling(G
)
68 value1
= G
.solution().game_value()
69 value2
= H
.solution().game_value()
70 modifier
= 4*max(abs(alpha
), 1)
71 self
.assert_within_tol(alpha
*value1
, value2
, modifier
)
74 def test_scaling_orthant(self
):
76 Test that scaling ``L`` by a nonnegative number scales the value
77 of the game by the same number over the nonnegative orthant.
79 G
= random_orthant_game()
80 self
.assert_scaling_works(G
)
83 def test_scaling_icecream(self
):
85 The same test as :meth:`test_nonnegative_scaling_orthant`,
86 except over the ice cream cone.
88 G
= random_icecream_game()
89 self
.assert_scaling_works(G
)
92 def assert_translation_works(self
, G
):
94 Check that translating ``L`` by alpha*(e1*e2.trans()) increases
95 the value of the associated game by alpha.
97 # We need to use ``L`` later, so make sure we transpose it
98 # before passing it in as a column-indexed matrix.
100 value1
= soln1
.game_value()
101 x_bar
= soln1
.player1_optimal()
102 y_bar
= soln1
.player2_optimal()
104 # This is the "correct" representation of ``M``, but COLUMN
106 (alpha
, H
) = random_translation(G
)
107 value2
= H
.solution().game_value()
109 modifier
= 4*max(abs(alpha
), 1)
110 self
.assert_within_tol(value1
+ alpha
, value2
, modifier
)
112 # Make sure the same optimal pair works.
113 self
.assert_within_tol(value2
, H
.payoff(x_bar
, y_bar
), modifier
)
116 def test_translation_orthant(self
):
118 Test that translation works over the nonnegative orthant.
120 G
= random_orthant_game()
121 self
.assert_translation_works(G
)
124 def test_translation_icecream(self
):
126 The same as :meth:`test_translation_orthant`, except over the
129 G
= random_icecream_game()
130 self
.assert_translation_works(G
)
133 def assert_opposite_game_works(self
, G
):
135 Check the value of the "opposite" game that gives rise to a
136 value that is the negation of the original game. Comes from
139 # This is the "correct" representation of ``M``, but
143 # so we have to transpose it when we feed it to the constructor.
144 # Note: the condition number of ``H`` should be comparable to ``G``.
145 H
= SymmetricLinearGame(M
.trans(), G
.K(), G
.e2(), G
.e1())
148 x_bar
= soln1
.player1_optimal()
149 y_bar
= soln1
.player2_optimal()
152 # The modifier of 4 is because each could be off by 2*ABS_TOL,
153 # which is how far apart the primal/dual objectives have been
155 self
.assert_within_tol(-soln1
.game_value(), soln2
.game_value(), 4)
157 # Make sure the switched optimal pair works. Since x_bar and
158 # y_bar come from G, we use the same modifier.
159 self
.assert_within_tol(soln2
.game_value(), H
.payoff(y_bar
, x_bar
), 4)
163 def test_opposite_game_orthant(self
):
165 Test the value of the "opposite" game over the nonnegative
168 G
= random_orthant_game()
169 self
.assert_opposite_game_works(G
)
172 def test_opposite_game_icecream(self
):
174 Like :meth:`test_opposite_game_orthant`, except over the
177 G
= random_icecream_game()
178 self
.assert_opposite_game_works(G
)
181 def assert_orthogonality(self
, G
):
183 Two orthogonality relations hold at an optimal solution, and we
187 x_bar
= soln
.player1_optimal()
188 y_bar
= soln
.player2_optimal()
189 value
= soln
.game_value()
191 ip1
= inner_product(y_bar
, G
.L()*x_bar
- value
*G
.e1())
192 self
.assert_within_tol(ip1
, 0)
194 ip2
= inner_product(value
*G
.e2() - G
.L().trans()*y_bar
, x_bar
)
195 self
.assert_within_tol(ip2
, 0)
198 def test_orthogonality_orthant(self
):
200 Check the orthgonality relationships that hold for a solution
201 over the nonnegative orthant.
203 G
= random_orthant_game()
204 self
.assert_orthogonality(G
)
207 def test_orthogonality_icecream(self
):
209 Check the orthgonality relationships that hold for a solution
210 over the ice-cream cone.
212 G
= random_icecream_game()
213 self
.assert_orthogonality(G
)
216 def test_positive_operator_value(self
):
218 Test that a positive operator on the nonnegative orthant gives
219 rise to a a game with a nonnegative value.
221 This test theoretically applies to the ice-cream cone as well,
222 but we don't know how to make positive operators on that cone.
224 G
= random_positive_orthant_game()
225 self
.assertTrue(G
.solution().game_value() >= -options
.ABS_TOL
)
228 def assert_lyapunov_works(self
, G
):
230 Check that Lyapunov games act the way we expect.
234 # We only check for positive/negative stability if the game
235 # value is not basically zero. If the value is that close to
236 # zero, we just won't check any assertions.
238 # See :meth:`assert_within_tol` for an explanation of the
240 eigs
= eigenvalues_re(G
.L())
242 if soln
.game_value() > options
.ABS_TOL
:
243 # L should be positive stable
244 positive_stable
= all([eig
> -options
.ABS_TOL
for eig
in eigs
])
245 self
.assertTrue(positive_stable
)
246 elif soln
.game_value() < -options
.ABS_TOL
:
247 # L should be negative stable
248 negative_stable
= all([eig
< options
.ABS_TOL
for eig
in eigs
])
249 self
.assertTrue(negative_stable
)
251 # The dual game's value should always equal the primal's.
252 # The modifier of 4 is because even though the games are dual,
253 # CVXOPT doesn't know that, and each could be off by 2*ABS_TOL.
254 dualsoln
= G
.dual().solution()
255 self
.assert_within_tol(dualsoln
.game_value(), soln
.game_value(), 4)
258 def test_lyapunov_orthant(self
):
260 Test that a Lyapunov game on the nonnegative orthant works.
262 G
= random_ll_orthant_game()
263 self
.assert_lyapunov_works(G
)
266 def test_lyapunov_icecream(self
):
268 Test that a Lyapunov game on the ice-cream cone works.
270 G
= random_ll_icecream_game()
271 self
.assert_lyapunov_works(G
)