]>
gitweb.michael.orlitzky.com - dunshire.git/blob - src/dunshire/symmetric_linear_game.py
7be55d76ffb36e058b27b85b82dd9caecf2971c5
2 Symmetric linear games and their solutions.
4 This module contains the main SymmetricLinearGame class that knows how
5 to solve a linear game.
8 # These few are used only for tests.
10 from random
import randint
, uniform
11 from unittest
import TestCase
13 # These are mostly actually needed.
14 from cvxopt
import matrix
, printing
, solvers
15 from cones
import CartesianProduct
, IceCream
, NonnegativeOrthant
16 from errors
import GameUnsolvableException
17 from matrices
import append_col
, append_row
, identity
, inner_product
20 printing
.options
['dformat'] = options
.FLOAT_FORMAT
21 solvers
.options
['show_progress'] = options
.VERBOSE
26 A representation of the solution of a linear game. It should contain
27 the value of the game, and both players' strategies.
29 def __init__(self
, game_value
, p1_optimal
, p2_optimal
):
31 Create a new Solution object from a game value and two optimal
32 strategies for the players.
34 self
._game
_value
= game_value
35 self
._player
1_optimal
= p1_optimal
36 self
._player
2_optimal
= p2_optimal
40 Return a string describing the solution of a linear game.
42 The three data that are described are,
44 * The value of the game.
45 * The optimal strategy of player one.
46 * The optimal strategy of player two.
50 >>> print(Solution(10, matrix([1,2]), matrix([3,4])))
51 Game value: 10.0000000
60 tpl
= 'Game value: {:.7f}\n' \
61 'Player 1 optimal:{:s}\n' \
62 'Player 2 optimal:{:s}'
64 p1_str
= '\n{!s}'.format(self
.player1_optimal())
65 p1_str
= '\n '.join(p1_str
.splitlines())
66 p2_str
= '\n{!s}'.format(self
.player2_optimal())
67 p2_str
= '\n '.join(p2_str
.splitlines())
69 return tpl
.format(self
.game_value(), p1_str
, p2_str
)
74 Return the game value for this solution.
76 return self
._game
_value
79 def player1_optimal(self
):
81 Return player one's optimal strategy in this solution.
83 return self
._player
1_optimal
86 def player2_optimal(self
):
88 Return player two's optimal strategy in this solution.
90 return self
._player
2_optimal
93 class SymmetricLinearGame
:
95 A representation of a symmetric linear game.
97 The data for a linear game are,
99 * A "payoff" operator ``L``.
101 * A point ``e`` in the interior of ``K``.
102 * A point ``f`` in the interior of the dual of ``K``.
104 In a symmetric game, the cone ``K`` is be self-dual. We therefore
105 name the two interior points ``e1`` and ``e2`` to indicate that
106 they come from the same cone but are "chosen" by players one and
109 The ambient space is assumed to be the span of ``K``.
111 def __init__(self
, L
, K
, e1
, e2
):
115 - ``L`` -- an square matrix represented as a list of lists
116 of real numbers. ``L`` itself is interpreted as a list of
117 ROWS, which agrees with (for example) SageMath and NumPy,
118 but not with CVXOPT (whose matrix constructor accepts a
121 - ``K`` -- a SymmetricCone instance.
123 - ``e1`` -- the interior point of ``K`` belonging to player one;
124 it can be of any enumerable type having the correct length.
126 - ``e2`` -- the interior point of ``K`` belonging to player two;
127 it can be of any enumerable type having the correct length.
131 Lists can (and probably should) be used for every argument:
133 >>> from cones import NonnegativeOrthant
134 >>> K = NonnegativeOrthant(2)
135 >>> L = [[1,0],[0,1]]
138 >>> G = SymmetricLinearGame(L, K, e1, e2)
140 The linear game (L, K, e1, e2) where
143 K = Nonnegative orthant in the real 2-space,
149 The points ``e1`` and ``e2`` can also be passed as some other
150 enumerable type (of the correct length) without much harm, since
151 there is no row/column ambiguity:
155 >>> from cones import NonnegativeOrthant
156 >>> K = NonnegativeOrthant(2)
157 >>> L = [[1,0],[0,1]]
158 >>> e1 = cvxopt.matrix([1,1])
159 >>> e2 = numpy.matrix([1,1])
160 >>> G = SymmetricLinearGame(L, K, e1, e2)
162 The linear game (L, K, e1, e2) where
165 K = Nonnegative orthant in the real 2-space,
171 However, ``L`` will always be intepreted as a list of rows, even
172 if it is passed as a ``cvxopt.base.matrix`` which is otherwise
176 >>> from cones import NonnegativeOrthant
177 >>> K = NonnegativeOrthant(2)
178 >>> L = [[1,2],[3,4]]
181 >>> G = SymmetricLinearGame(L, K, e1, e2)
183 The linear game (L, K, e1, e2) where
186 K = Nonnegative orthant in the real 2-space,
191 >>> L = cvxopt.matrix(L)
196 >>> G = SymmetricLinearGame(L, K, e1, e2)
198 The linear game (L, K, e1, e2) where
201 K = Nonnegative orthant in the real 2-space,
208 self
._e
1 = matrix(e1
, (K
.dimension(), 1))
209 self
._e
2 = matrix(e2
, (K
.dimension(), 1))
211 # Our input ``L`` is indexed by rows but CVXOPT matrices are
212 # indexed by columns, so we need to transpose the input before
213 # feeding it to CVXOPT.
214 self
._L = matrix(L
, (K
.dimension(), K
.dimension())).trans()
216 if not K
.contains_strict(self
._e
1):
217 raise ValueError('the point e1 must lie in the interior of K')
219 if not K
.contains_strict(self
._e
2):
220 raise ValueError('the point e2 must lie in the interior of K')
224 Return a string representatoin of this game.
228 >>> from cones import NonnegativeOrthant
229 >>> K = NonnegativeOrthant(3)
230 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
233 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
235 The linear game (L, K, e1, e2) where
239 K = Nonnegative orthant in the real 3-space,
248 tpl
= 'The linear game (L, K, e1, e2) where\n' \
253 indented_L
= '\n '.join(str(self
._L).splitlines())
254 indented_e1
= '\n '.join(str(self
._e
1).splitlines())
255 indented_e2
= '\n '.join(str(self
._e
2).splitlines())
256 return tpl
.format(indented_L
, str(self
._K
), indented_e1
, indented_e2
)
261 Solve this linear game and return a Solution object.
265 If the cone program associated with this game could be
266 successfully solved, then a Solution object containing the
267 game's value and optimal strategies is returned. If the game
268 could *not* be solved -- which should never happen -- then a
269 GameUnsolvableException is raised. It can be printed to get the
270 raw output from CVXOPT.
274 This example is computed in Gowda and Ravindran in the section
275 "The value of a Z-transformation":
277 >>> from cones import NonnegativeOrthant
278 >>> K = NonnegativeOrthant(3)
279 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
282 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
283 >>> print(SLG.solution())
284 Game value: -6.1724138
294 The value of the following game can be computed using the fact
295 that the identity is invertible:
297 >>> from cones import NonnegativeOrthant
298 >>> K = NonnegativeOrthant(3)
299 >>> L = [[1,0,0],[0,1,0],[0,0,1]]
302 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
303 >>> print(SLG.solution())
304 Game value: 0.0312500
315 # The cone "C" that appears in the statement of the CVXOPT
317 C
= CartesianProduct(self
._K
, self
._K
)
319 # The column vector "b" that appears on the right-hand side of
320 # Ax = b in the statement of the CVXOPT conelp program.
321 b
= matrix([1], tc
='d')
323 # A column of zeros that fits K.
324 zero
= matrix(0, (self
._K
.dimension(), 1), tc
='d')
326 # The column vector "h" that appears on the right-hand side of
327 # Gx + s = h in the statement of the CVXOPT conelp program.
328 h
= matrix([zero
, zero
])
330 # The column vector "c" that appears in the objective function
331 # value <c,x> in the statement of the CVXOPT conelp program.
332 c
= matrix([-1, zero
])
334 # The matrix "G" that appears on the left-hand side of Gx + s = h
335 # in the statement of the CVXOPT conelp program.
336 G
= append_row(append_col(zero
, -identity(self
._K
.dimension())),
337 append_col(self
._e
1, -self
._L))
339 # The matrix "A" that appears on the right-hand side of Ax = b
340 # in the statement of the CVXOPT conelp program.
341 A
= matrix([0, self
._e
2], (1, self
._K
.dimension() + 1), 'd')
343 # Actually solve the thing and obtain a dictionary describing
345 soln_dict
= solvers
.conelp(c
, G
, h
, C
.cvxopt_dims(), A
, b
)
347 # The "status" field contains "optimal" if everything went
348 # according to plan. Other possible values are "primal
349 # infeasible", "dual infeasible", "unknown", all of which
350 # mean we didn't get a solution. That should never happen,
351 # because by construction our game has a solution, and thus
352 # the cone program should too.
353 if soln_dict
['status'] != 'optimal':
354 raise GameUnsolvableException(soln_dict
)
356 p1_value
= soln_dict
['x'][0]
357 p1_optimal
= soln_dict
['x'][1:]
358 p2_optimal
= soln_dict
['z'][self
._K
.dimension():]
360 return Solution(p1_value
, p1_optimal
, p2_optimal
)
364 Return the dual game to this game.
368 >>> from cones import NonnegativeOrthant
369 >>> K = NonnegativeOrthant(3)
370 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
373 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
374 >>> print(SLG.dual())
375 The linear game (L, K, e1, e2) where
379 K = Nonnegative orthant in the real 3-space,
388 return SymmetricLinearGame(self
._L, # It will be transposed in __init__().
389 self
._K
, # Since "K" is symmetric.
394 class SymmetricLinearGameTest(TestCase
):
396 Tests for the SymmetricLinearGame and Solution classes.
399 def assert_within_tol(self
, first
, second
):
401 Test that ``first`` and ``second`` are equal within our default
404 self
.assertTrue(abs(first
- second
) < options
.ABS_TOL
)
407 def assert_solution_exists(self
, L
, K
, e1
, e2
):
409 Given the parameters needed to construct a SymmetricLinearGame,
410 ensure that that game has a solution.
412 G
= SymmetricLinearGame(L
, K
, e1
, e2
)
414 L_matrix
= matrix(L
).trans()
415 expected
= inner_product(L_matrix
*soln
.player1_optimal(),
416 soln
.player2_optimal())
417 self
.assert_within_tol(soln
.game_value(), expected
)
419 def test_solution_exists_nonnegative_orthant(self
):
421 Every linear game has a solution, so we should be able to solve
422 every symmetric linear game over the NonnegativeOrthant. Pick
423 some parameters randomly and give it a shot. The resulting
424 optimal solutions should give us the optimal game value when we
425 apply the payoff operator to them.
427 ambient_dim
= randint(1, 10)
428 K
= NonnegativeOrthant(ambient_dim
)
429 e1
= [uniform(0.1, 10) for idx
in range(K
.dimension())]
430 e2
= [uniform(0.1, 10) for idx
in range(K
.dimension())]
431 L
= [[uniform(-10, 10) for i
in range(K
.dimension())]
432 for j
in range(K
.dimension())]
433 self
.assert_solution_exists(L
, K
, e1
, e2
)
435 def test_solution_exists_ice_cream(self
):
437 Like :meth:`test_solution_exists_nonnegative_orthant`, except
438 over the ice cream cone.
440 # Use a minimum dimension of two to avoid divide-by-zero in
441 # the fudge factor we make up later.
442 ambient_dim
= randint(2, 10)
443 K
= IceCream(ambient_dim
)
446 # If we choose the rest of the components of e1,e2 randomly
447 # between 0 and 1, then the largest the squared norm of the
448 # non-height part of e1,e2 could be is the 1*(dim(K) - 1). We
449 # need to make it less than one (the height of the cone) so
450 # that the whole thing is in the cone. The norm of the
451 # non-height part is sqrt(dim(K) - 1), and we can divide by
453 fudge_factor
= 1.0 / (2.0*sqrt(K
.dimension() - 1.0))
454 e1
+= [fudge_factor
*uniform(0, 1) for idx
in range(K
.dimension() - 1)]
455 e2
+= [fudge_factor
*uniform(0, 1) for idx
in range(K
.dimension() - 1)]
456 L
= [[uniform(-10, 10) for i
in range(K
.dimension())]
457 for j
in range(K
.dimension())]
458 self
.assert_solution_exists(L
, K
, e1
, e2
)