]>
gitweb.michael.orlitzky.com - dunshire.git/blob - src/test/symmetric_linear_game_test.py
1 # These few are used only for tests.
3 from random
import randint
, uniform
4 from unittest
import TestCase
6 from cvxopt
import matrix
7 from dunshire
.cones
import NonnegativeOrthant
, IceCream
8 from dunshire
.games
import SymmetricLinearGame
9 from dunshire
.matrices
import (append_col
, append_row
, eigenvalues_re
,
10 identity
, inner_product
)
11 from dunshire
import options
14 def random_matrix(dims
):
16 Generate a random square matrix.
22 The number of rows/columns you want in the returned matrix.
28 A new matrix whose entries are random floats chosen uniformly from
29 the interval [-10, 10].
34 >>> A = random_matrix(3)
39 return matrix([[uniform(-10, 10) for i
in range(dims
)]
40 for j
in range(dims
)])
43 def random_nonnegative_matrix(dims
):
45 Generate a random square matrix with nonnegative entries.
51 The number of rows/columns you want in the returned matrix.
57 A new matrix whose entries are random floats chosen uniformly from
63 >>> A = random_nonnegative_matrix(3)
66 >>> all([entry >= 0 for entry in A])
70 L
= random_matrix(dims
)
71 return matrix([abs(entry
) for entry
in L
], (dims
, dims
))
74 def random_diagonal_matrix(dims
):
76 Generate a random square matrix with zero off-diagonal entries.
78 These matrices are Lyapunov-like on the nonnegative orthant, as is
85 The number of rows/columns you want in the returned matrix.
91 A new matrix whose diagonal entries are random floats chosen
92 uniformly from the interval [-10, 10] and whose off-diagonal
98 >>> A = random_diagonal_matrix(3)
101 >>> A[0,1] == A[0,2] == A[1,0] == A[2,0] == A[1,2] == A[2,1] == 0
105 return matrix([[uniform(-10, 10)*int(i
== j
) for i
in range(dims
)]
106 for j
in range(dims
)])
109 def random_skew_symmetric_matrix(dims
):
111 Generate a random skew-symmetrix matrix.
117 The number of rows/columns you want in the returned matrix.
123 A new skew-matrix whose strictly above-diagonal entries are
124 random floats chosen uniformly from the interval [-10, 10].
129 >>> A = random_skew_symmetric_matrix(3)
133 >>> from dunshire.matrices import norm
134 >>> A = random_skew_symmetric_matrix(randint(1, 10))
135 >>> norm(A + A.trans()) < options.ABS_TOL
139 strict_ut
= [[uniform(-10, 10)*int(i
< j
) for i
in range(dims
)]
140 for j
in range(dims
)]
142 strict_ut
= matrix(strict_ut
, (dims
, dims
))
143 return strict_ut
- strict_ut
.trans()
146 def random_lyapunov_like_icecream(dims
):
148 Generate a random matrix Lyapunov-like on the ice-cream cone.
150 The form of these matrices is cited in Gowda and Tao
151 [GowdaTao]_. The scalar ``a`` and the vector ``b`` (using their
152 notation) are easy to generate. The submatrix ``D`` is a little
153 trickier, but it can be found noticing that :math:`C + C^{T} = 0`
154 for a skew-symmetric matrix :math:`C` implying that :math:`C + C^{T}
155 + \left(2a\right)I = \left(2a\right)I`. Thus we can stick an
156 :math:`aI` with each of :math:`C,C^{T}` and let those be our
163 The dimension of the ice-cream cone (not of the matrix you want!)
164 on which the returned matrix should be Lyapunov-like.
170 A new matrix, Lyapunov-like on the ice-cream cone in ``dims``
171 dimensions, whose free entries are random floats chosen uniformly
172 from the interval [-10, 10].
177 .. [GowdaTao] M. S. Gowda and J. Tao. On the bilinearity rank of a
178 proper cone and Lyapunov-like transformations. Mathematical
179 Programming, 147:155–170, 2014.
184 >>> L = random_lyapunov_like_icecream(3)
187 >>> x = matrix([1,1,0])
188 >>> s = matrix([1,-1,0])
189 >>> abs(inner_product(L*x, s)) < options.ABS_TOL
193 a
= matrix([uniform(-10, 10)], (1, 1))
194 b
= matrix([uniform(-10, 10) for idx
in range(dims
-1)], (dims
-1, 1))
195 D
= random_skew_symmetric_matrix(dims
-1) + a
*identity(dims
-1)
196 row1
= append_col(a
, b
.trans())
197 row2
= append_col(b
, D
)
198 return append_row(row1
, row2
)
201 def random_orthant_params():
203 Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
204 random game over the nonnegative orthant.
206 ambient_dim
= randint(1, 10)
207 K
= NonnegativeOrthant(ambient_dim
)
208 e1
= [uniform(0.5, 10) for idx
in range(K
.dimension())]
209 e2
= [uniform(0.5, 10) for idx
in range(K
.dimension())]
210 L
= random_matrix(K
.dimension())
211 return (L
, K
, matrix(e1
), matrix(e2
))
214 def random_icecream_params():
216 Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
217 random game over the ice-cream cone.
219 # Use a minimum dimension of two to avoid divide-by-zero in
220 # the fudge factor we make up later.
221 ambient_dim
= randint(2, 10)
222 K
= IceCream(ambient_dim
)
223 e1
= [1] # Set the "height" of e1 to one
224 e2
= [1] # And the same for e2
226 # If we choose the rest of the components of e1,e2 randomly
227 # between 0 and 1, then the largest the squared norm of the
228 # non-height part of e1,e2 could be is the 1*(dim(K) - 1). We
229 # need to make it less than one (the height of the cone) so
230 # that the whole thing is in the cone. The norm of the
231 # non-height part is sqrt(dim(K) - 1), and we can divide by
233 fudge_factor
= 1.0 / (2.0*sqrt(K
.dimension() - 1.0))
234 e1
+= [fudge_factor
*uniform(0, 1) for idx
in range(K
.dimension() - 1)]
235 e2
+= [fudge_factor
*uniform(0, 1) for idx
in range(K
.dimension() - 1)]
236 L
= random_matrix(K
.dimension())
238 return (L
, K
, matrix(e1
), matrix(e2
))
241 # Tell pylint to shut up about the large number of methods.
242 class SymmetricLinearGameTest(TestCase
): # pylint: disable=R0904
244 Tests for the SymmetricLinearGame and Solution classes.
246 def assert_within_tol(self
, first
, second
):
248 Test that ``first`` and ``second`` are equal within our default
251 self
.assertTrue(abs(first
- second
) < options
.ABS_TOL
)
254 def assert_norm_within_tol(self
, first
, second
):
256 Test that ``first`` and ``second`` vectors are equal in the
257 sense that the norm of their difference is within our default
260 self
.assert_within_tol(norm(first
- second
), 0)
263 def assert_solution_exists(self
, L
, K
, e1
, e2
):
265 Given the parameters needed to construct a SymmetricLinearGame,
266 ensure that that game has a solution.
268 # The matrix() constructor assumes that ``L`` is a list of
269 # columns, so we transpose it to agree with what
270 # SymmetricLinearGame() thinks.
271 G
= SymmetricLinearGame(L
.trans(), K
, e1
, e2
)
274 expected
= inner_product(L
*soln
.player1_optimal(),
275 soln
.player2_optimal())
276 self
.assert_within_tol(soln
.game_value(), expected
)
279 def test_solution_exists_orthant(self
):
281 Every linear game has a solution, so we should be able to solve
282 every symmetric linear game over the NonnegativeOrthant. Pick
283 some parameters randomly and give it a shot. The resulting
284 optimal solutions should give us the optimal game value when we
285 apply the payoff operator to them.
287 (L
, K
, e1
, e2
) = random_orthant_params()
288 self
.assert_solution_exists(L
, K
, e1
, e2
)
291 def test_solution_exists_icecream(self
):
293 Like :meth:`test_solution_exists_nonnegative_orthant`, except
294 over the ice cream cone.
296 (L
, K
, e1
, e2
) = random_icecream_params()
297 self
.assert_solution_exists(L
, K
, e1
, e2
)
300 def test_negative_value_z_operator(self
):
302 Test the example given in Gowda/Ravindran of a Z-matrix with
303 negative game value on the nonnegative orthant.
305 K
= NonnegativeOrthant(2)
308 L
= [[1, -2], [-2, 1]]
309 G
= SymmetricLinearGame(L
, K
, e1
, e2
)
310 self
.assertTrue(G
.solution().game_value() < -options
.ABS_TOL
)
313 def assert_scaling_works(self
, L
, K
, e1
, e2
):
315 Test that scaling ``L`` by a nonnegative number scales the value
316 of the game by the same number.
318 game1
= SymmetricLinearGame(L
, K
, e1
, e2
)
319 value1
= game1
.solution().game_value()
321 alpha
= uniform(0.1, 10)
322 game2
= SymmetricLinearGame(alpha
*L
, K
, e1
, e2
)
323 value2
= game2
.solution().game_value()
324 self
.assert_within_tol(alpha
*value1
, value2
)
327 def test_scaling_orthant(self
):
329 Test that scaling ``L`` by a nonnegative number scales the value
330 of the game by the same number over the nonnegative orthant.
332 (L
, K
, e1
, e2
) = random_orthant_params()
333 self
.assert_scaling_works(L
, K
, e1
, e2
)
336 def test_scaling_icecream(self
):
338 The same test as :meth:`test_nonnegative_scaling_orthant`,
339 except over the ice cream cone.
341 (L
, K
, e1
, e2
) = random_icecream_params()
342 self
.assert_scaling_works(L
, K
, e1
, e2
)
345 def assert_translation_works(self
, L
, K
, e1
, e2
):
347 Check that translating ``L`` by alpha*(e1*e2.trans()) increases
348 the value of the associated game by alpha.
350 # We need to use ``L`` later, so make sure we transpose it
351 # before passing it in as a column-indexed matrix.
352 game1
= SymmetricLinearGame(L
.trans(), K
, e1
, e2
)
353 soln1
= game1
.solution()
354 value1
= soln1
.game_value()
355 x_bar
= soln1
.player1_optimal()
356 y_bar
= soln1
.player2_optimal()
358 alpha
= uniform(-10, 10)
359 tensor_prod
= e1
*e2
.trans()
361 # This is the "correct" representation of ``M``, but COLUMN
363 M
= L
+ alpha
*tensor_prod
365 # so we have to transpose it when we feed it to the constructor.
366 game2
= SymmetricLinearGame(M
.trans(), K
, e1
, e2
)
367 value2
= game2
.solution().game_value()
369 self
.assert_within_tol(value1
+ alpha
, value2
)
371 # Make sure the same optimal pair works.
372 self
.assert_within_tol(value2
, inner_product(M
*x_bar
, y_bar
))
375 def test_translation_orthant(self
):
377 Test that translation works over the nonnegative orthant.
379 (L
, K
, e1
, e2
) = random_orthant_params()
380 self
.assert_translation_works(L
, K
, e1
, e2
)
383 def test_translation_icecream(self
):
385 The same as :meth:`test_translation_orthant`, except over the
388 (L
, K
, e1
, e2
) = random_icecream_params()
389 self
.assert_translation_works(L
, K
, e1
, e2
)
392 def assert_opposite_game_works(self
, L
, K
, e1
, e2
):
394 Check the value of the "opposite" game that gives rise to a
395 value that is the negation of the original game. Comes from
398 # We need to use ``L`` later, so make sure we transpose it
399 # before passing it in as a column-indexed matrix.
400 game1
= SymmetricLinearGame(L
.trans(), K
, e1
, e2
)
402 # This is the "correct" representation of ``M``, but
406 # so we have to transpose it when we feed it to the constructor.
407 game2
= SymmetricLinearGame(M
.trans(), K
, e2
, e1
)
409 soln1
= game1
.solution()
410 x_bar
= soln1
.player1_optimal()
411 y_bar
= soln1
.player2_optimal()
412 soln2
= game2
.solution()
414 self
.assert_within_tol(-soln1
.game_value(), soln2
.game_value())
416 # Make sure the switched optimal pair works.
417 self
.assert_within_tol(soln2
.game_value(),
418 inner_product(M
*y_bar
, x_bar
))
421 def test_opposite_game_orthant(self
):
423 Test the value of the "opposite" game over the nonnegative
426 (L
, K
, e1
, e2
) = random_orthant_params()
427 self
.assert_opposite_game_works(L
, K
, e1
, e2
)
430 def test_opposite_game_icecream(self
):
432 Like :meth:`test_opposite_game_orthant`, except over the
435 (L
, K
, e1
, e2
) = random_icecream_params()
436 self
.assert_opposite_game_works(L
, K
, e1
, e2
)
439 def assert_orthogonality(self
, L
, K
, e1
, e2
):
441 Two orthogonality relations hold at an optimal solution, and we
444 # We need to use ``L`` later, so make sure we transpose it
445 # before passing it in as a column-indexed matrix.
446 game
= SymmetricLinearGame(L
.trans(), K
, e1
, e2
)
447 soln
= game
.solution()
448 x_bar
= soln
.player1_optimal()
449 y_bar
= soln
.player2_optimal()
450 value
= soln
.game_value()
452 ip1
= inner_product(y_bar
, L
*x_bar
- value
*e1
)
453 self
.assert_within_tol(ip1
, 0)
455 ip2
= inner_product(value
*e2
- L
.trans()*y_bar
, x_bar
)
456 self
.assert_within_tol(ip2
, 0)
459 def test_orthogonality_orthant(self
):
461 Check the orthgonality relationships that hold for a solution
462 over the nonnegative orthant.
464 (L
, K
, e1
, e2
) = random_orthant_params()
465 self
.assert_orthogonality(L
, K
, e1
, e2
)
468 def test_orthogonality_icecream(self
):
470 Check the orthgonality relationships that hold for a solution
471 over the ice-cream cone.
473 (L
, K
, e1
, e2
) = random_icecream_params()
474 self
.assert_orthogonality(L
, K
, e1
, e2
)
477 def test_positive_operator_value(self
):
479 Test that a positive operator on the nonnegative orthant gives
480 rise to a a game with a nonnegative value.
482 This test theoretically applies to the ice-cream cone as well,
483 but we don't know how to make positive operators on that cone.
485 (K
, e1
, e2
) = random_orthant_params()[1:]
486 L
= random_nonnegative_matrix(K
.dimension())
488 game
= SymmetricLinearGame(L
, K
, e1
, e2
)
489 self
.assertTrue(game
.solution().game_value() >= -options
.ABS_TOL
)
492 def assert_lyapunov_works(self
, L
, K
, e1
, e2
):
494 Check that Lyapunov games act the way we expect.
496 game
= SymmetricLinearGame(L
, K
, e1
, e2
)
497 soln
= game
.solution()
499 # We only check for positive/negative stability if the game
500 # value is not basically zero. If the value is that close to
501 # zero, we just won't check any assertions.
502 eigs
= eigenvalues_re(L
)
503 if soln
.game_value() > options
.ABS_TOL
:
504 # L should be positive stable
505 positive_stable
= all([eig
> -options
.ABS_TOL
for eig
in eigs
])
506 self
.assertTrue(positive_stable
)
507 elif soln
.game_value() < -options
.ABS_TOL
:
508 # L should be negative stable
509 negative_stable
= all([eig
< options
.ABS_TOL
for eig
in eigs
])
510 self
.assertTrue(negative_stable
)
512 # The dual game's value should always equal the primal's.
513 dualsoln
= game
.dual().solution()
514 self
.assert_within_tol(dualsoln
.game_value(), soln
.game_value())
517 def test_lyapunov_orthant(self
):
519 Test that a Lyapunov game on the nonnegative orthant works.
521 (K
, e1
, e2
) = random_orthant_params()[1:]
522 L
= random_diagonal_matrix(K
.dimension())
524 self
.assert_lyapunov_works(L
, K
, e1
, e2
)
527 def test_lyapunov_icecream(self
):
529 Test that a Lyapunov game on the ice-cream cone works.
531 (K
, e1
, e2
) = random_icecream_params()[1:]
532 L
= random_lyapunov_like_icecream(K
.dimension())
534 self
.assert_lyapunov_works(L
, K
, e1
, e2
)