X-Git-Url: https://gitweb.michael.orlitzky.com/?p=dunshire.git;a=blobdiff_plain;f=dunshire%2Fgames.py;h=ea7a64f6b8e6451a808b464494c11e9be9f0de78;hp=a0c52279869510090f9d051a92b479bfefa3fbdd;hb=0274de467062ab29d2a41d2a91ec0b28fcd95c8d;hpb=687820b6a899842773f79bc1c830458b65de2ad3 diff --git a/dunshire/games.py b/dunshire/games.py index a0c5227..ea7a64f 100644 --- a/dunshire/games.py +++ b/dunshire/games.py @@ -179,11 +179,15 @@ class SymmetricLinearGame: ---------- L : list of list of float - A matrix represented as a list of ROWS. This representation - agrees with (for example) SageMath and NumPy, but not with CVXOPT - (whose matrix constructor accepts a list of columns). - - K : :class:`SymmetricCone` + A matrix represented as a list of **rows**. This representation + agrees with (for example) `SageMath `_ + and `NumPy `_, but not with CVXOPT (whose + matrix constructor accepts a list of columns). In reality, ``L`` + can be any iterable type of the correct length; however, you + should be extremely wary of the way we interpret anything other + than a list of rows. + + K : dunshire.cones.SymmetricCone The symmetric cone instance over which the game is played. e1 : iterable float @@ -220,8 +224,7 @@ class SymmetricLinearGame: [ 1], e2 = [ 1] [ 2] - [ 3], - Condition((L, K, e1, e2)) = 31.834... + [ 3] Lists can (and probably should) be used for every argument:: @@ -239,8 +242,7 @@ class SymmetricLinearGame: e1 = [ 1] [ 1], e2 = [ 1] - [ 1], - Condition((L, K, e1, e2)) = 1.707... + [ 1] The points ``e1`` and ``e2`` can also be passed as some other enumerable type (of the correct length) without much harm, since @@ -262,8 +264,7 @@ class SymmetricLinearGame: e1 = [ 1] [ 1], e2 = [ 1] - [ 1], - Condition((L, K, e1, e2)) = 1.707... + [ 1] However, ``L`` will always be intepreted as a list of rows, even if it is passed as a :class:`cvxopt.base.matrix` which is @@ -284,8 +285,7 @@ class SymmetricLinearGame: e1 = [ 1] [ 1], e2 = [ 1] - [ 1], - Condition((L, K, e1, e2)) = 6.073... + [ 1] >>> L = cvxopt.matrix(L) >>> print(L) [ 1 3] @@ -300,8 +300,7 @@ class SymmetricLinearGame: e1 = [ 1] [ 1], e2 = [ 1] - [ 1], - Condition((L, K, e1, e2)) = 6.073... + [ 1] """ def __init__(self, L, K, e1, e2): @@ -335,8 +334,7 @@ class SymmetricLinearGame: ' L = {:s},\n' \ ' K = {!s},\n' \ ' e1 = {:s},\n' \ - ' e2 = {:s},\n' \ - ' Condition((L, K, e1, e2)) = {:f}.' + ' e2 = {:s}' indented_L = '\n '.join(str(self.L()).splitlines()) indented_e1 = '\n '.join(str(self.e1()).splitlines()) indented_e2 = '\n '.join(str(self.e2()).splitlines()) @@ -344,8 +342,7 @@ class SymmetricLinearGame: return tpl.format(indented_L, str(self.K()), indented_e1, - indented_e2, - self.condition()) + indented_e2) def L(self): @@ -468,8 +465,8 @@ class SymmetricLinearGame: The payoff operator takes pairs of strategies to a real number. For example, if player one's strategy is :math:`x` and player two's strategy is :math:`y`, then the associated payoff - is :math:`\left\langle L\left(x\right),y \right\rangle` \in - \mathbb{R}. Here, :math:`L` denotes the same linear operator as + is :math:`\left\langle L\left(x\right),y \right\rangle \in + \mathbb{R}`. Here, :math:`L` denotes the same linear operator as :meth:`L`. This method computes the payoff given the two players' strategies. @@ -496,7 +493,6 @@ class SymmetricLinearGame: strategies:: >>> from dunshire import * - >>> from dunshire.options import ABS_TOL >>> K = NonnegativeOrthant(3) >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]] >>> e1 = [1,1,1] @@ -505,7 +501,7 @@ class SymmetricLinearGame: >>> soln = SLG.solution() >>> x_bar = soln.player1_optimal() >>> y_bar = soln.player2_optimal() - >>> abs(SLG.payoff(x_bar, y_bar) - soln.game_value()) < ABS_TOL + >>> SLG.payoff(x_bar, y_bar) == soln.game_value() True """ @@ -584,11 +580,12 @@ class SymmetricLinearGame: def A(self): - """ + r""" Return the matrix ``A`` used in our CVXOPT construction. - This matrix ``A`` appears on the right-hand side of ``Ax = b`` - in the statement of the CVXOPT conelp program. + This matrix :math:`A` appears on the right-hand side of + :math:`Ax = b` in the `statement of the CVXOPT conelp program + `_. .. warning:: @@ -600,7 +597,7 @@ class SymmetricLinearGame: matrix A ``1``-by-``(1 + self.dimension())`` row vector. Its first - entry is zero, and the rest are the entries of ``e2``. + entry is zero, and the rest are the entries of :meth:`e2`. Examples -------- @@ -620,12 +617,13 @@ class SymmetricLinearGame: - def _G(self): + def G(self): r""" Return the matrix ``G`` used in our CVXOPT construction. - Thus matrix ``G`` appears on the left-hand side of ``Gx + s = h`` - in the statement of the CVXOPT conelp program. + Thus matrix :math:`G` appears on the left-hand side of :math:`Gx + + s = h` in the `statement of the CVXOPT conelp program + `_. .. warning:: @@ -647,7 +645,7 @@ class SymmetricLinearGame: >>> e1 = [1,2,3] >>> e2 = [1,1,1] >>> SLG = SymmetricLinearGame(L, K, e1, e2) - >>> print(SLG._G()) + >>> print(SLG.G()) [ 0.0000000 -1.0000000 0.0000000 0.0000000] [ 0.0000000 0.0000000 -1.0000000 0.0000000] [ 0.0000000 0.0000000 0.0000000 -1.0000000] @@ -662,12 +660,14 @@ class SymmetricLinearGame: append_col(self.e1(), -self.L())) - def _c(self): - """ + def c(self): + r""" Return the vector ``c`` used in our CVXOPT construction. - The column vector ``c`` appears in the objective function - value ```` in the statement of the CVXOPT conelp program. + The column vector :math:`c` appears in the objective function + value :math:`\left\langle c,x \right\rangle` in the `statement + of the CVXOPT conelp program + `_. .. warning:: @@ -678,7 +678,7 @@ class SymmetricLinearGame: ------- matrix - A ``self.dimension()``-by-``1`` column vector. + A :meth:`dimension`-by-``1`` column vector. Examples -------- @@ -689,7 +689,7 @@ class SymmetricLinearGame: >>> e1 = [1,2,3] >>> e2 = [1,1,1] >>> SLG = SymmetricLinearGame(L, K, e1, e2) - >>> print(SLG._c()) + >>> print(SLG.c()) [-1.0000000] [ 0.0000000] [ 0.0000000] @@ -704,8 +704,9 @@ class SymmetricLinearGame: """ Return the cone ``C`` used in our CVXOPT construction. - The cone ``C`` is the cone over which the conelp program takes - place. + This is the cone over which the `CVXOPT conelp program + `_ + takes place. Returns ------- @@ -730,12 +731,13 @@ class SymmetricLinearGame: """ return CartesianProduct(self._K, self._K) - def _h(self): + def h(self): r""" Return the ``h`` vector used in our CVXOPT construction. - The ``h`` vector appears on the right-hand side of :math:`Gx + s - = h` in the statement of the CVXOPT conelp program. + The :math:`h` vector appears on the right-hand side of :math:`Gx + + s = h` in the `statement of the CVXOPT conelp program + `_. .. warning:: @@ -757,7 +759,7 @@ class SymmetricLinearGame: >>> e1 = [1,2,3] >>> e2 = [1,1,1] >>> SLG = SymmetricLinearGame(L, K, e1, e2) - >>> print(SLG._h()) + >>> print(SLG.h()) [0.0000000] [0.0000000] [0.0000000] @@ -776,8 +778,9 @@ class SymmetricLinearGame: r""" Return the ``b`` vector used in our CVXOPT construction. - The vector ``b`` appears on the right-hand side of :math:`Ax = - b` in the statement of the CVXOPT conelp program. + The vector :math:`b` appears on the right-hand side of :math:`Ax + = b` in the `statement of the CVXOPT conelp program + `_. This method is static because the dimensions and entries of ``b`` are known beforehand, and don't depend on any other @@ -816,17 +819,32 @@ class SymmetricLinearGame: Return a feasible starting point for player one. This starting point is for the CVXOPT formulation and not for - the original game. The basic premise is that if you normalize - :meth:`e2`, then you get a point in :meth:`K` that makes a unit - inner product with :meth:`e2`. We then get to choose the primal - objective function value such that the constraint involving - :meth:`L` is satisfied. + the original game. The basic premise is that if you scale + :meth:`e2` by the reciprocal of its squared norm, then you get a + point in :meth:`K` that makes a unit inner product with + :meth:`e2`. We then get to choose the primal objective function + value such that the constraint involving :meth:`L` is satisfied. + + Returns + ------- + + dict + A dictionary with two keys, ``'x'`` and ``'s'``, which + contain the vectors of the same name in the CVXOPT primal + problem formulation. + + The vector ``x`` consists of the primal objective function + value concatenated with the strategy (for player one) that + achieves it. The vector ``s`` is essentially a dummy + variable, and is computed from the equality constraing in + the CVXOPT primal problem. + """ p = self.e2() / (norm(self.e2()) ** 2) dist = self.K().ball_radius(self.e1()) nu = - self._L_specnorm()/(dist*norm(self.e2())) x = matrix([nu, p], (self.dimension() + 1, 1)) - s = - self._G()*x + s = - self.G()*x return {'x': x, 's': s} @@ -834,6 +852,29 @@ class SymmetricLinearGame: def player2_start(self): """ Return a feasible starting point for player two. + + This starting point is for the CVXOPT formulation and not for + the original game. The basic premise is that if you scale + :meth:`e1` by the reciprocal of its squared norm, then you get a + point in :meth:`K` that makes a unit inner product with + :meth:`e1`. We then get to choose the dual objective function + value such that the constraint involving :meth:`L` is satisfied. + + Returns + ------- + + dict + A dictionary with two keys, ``'y'`` and ``'z'``, which + contain the vectors of the same name in the CVXOPT dual + problem formulation. + + The ``1``-by-``1`` vector ``y`` consists of the dual + objective function value. The last :meth:`dimension` entries + of the vector ``z`` contain the strategy (for player two) + that achieves it. The remaining entries of ``z`` are + essentially dummy variables, computed from the equality + constraint in the CVXOPT dual problem. + """ q = self.e1() / (norm(self.e1()) ** 2) dist = self.K().ball_radius(self.e2()) @@ -862,6 +903,19 @@ class SymmetricLinearGame: A nonnegative real number; the largest singular value of the matrix :meth:`L`. + Examples + -------- + + >>> from dunshire import * + >>> from dunshire.matrices import specnorm + >>> L = [[1,2],[3,4]] + >>> K = NonnegativeOrthant(2) + >>> e1 = [1,1] + >>> e2 = e1 + >>> SLG = SymmetricLinearGame(L,K,e1,e2) + >>> specnorm(SLG.L()) == SLG._L_specnorm() + True + """ if self._L_specnorm_value is None: self._L_specnorm_value = specnorm(self.L()) @@ -870,19 +924,21 @@ class SymmetricLinearGame: def tolerance_scale(self, solution): r""" - Return a scaling factor that should be applied to ``ABS_TOL`` - for this game. - - When performing certain comparisons, the default tolernace - ``ABS_TOL`` may not be appropriate. For example, if we expect - ``x`` and ``y`` to be within ``ABS_TOL`` of each other, than the - inner product of ``L*x`` and ``y`` can be as far apart as the - spectral norm of ``L`` times the sum of the norms of ``x`` and + + Return a scaling factor that should be applied to + :const:`dunshire.options.ABS_TOL` for this game. + + When performing certain comparisons, the default tolerance + :const:`dunshire.options.ABS_TOL` may not be appropriate. For + example, if we expect ``x`` and ``y`` to be within + :const:`dunshire.options.ABS_TOL` of each other, than the inner + product of ``L*x`` and ``y`` can be as far apart as the spectral + norm of ``L`` times the sum of the norms of ``x`` and ``y``. Such a comparison is made in :meth:`solution`, and in many of our unit tests. - The returned scaling factor found from the inner product mentioned - above is + The returned scaling factor found from the inner product + mentioned above is .. math:: @@ -913,9 +969,27 @@ class SymmetricLinearGame: ------- float - A scaling factor to be multiplied by ``ABS_TOL`` when + A scaling factor to be multiplied by + :const:`dunshire.options.ABS_TOL` when making comparisons involving solutions of this game. + Examples + -------- + + The spectral norm of ``L`` in this case is around ``5.464``, and + the optimal strategies both have norm one, so we expect the + tolerance scale to be somewhere around ``2 * 5.464``, or + ``10.929``:: + + >>> from dunshire import * + >>> L = [[1,2],[3,4]] + >>> K = NonnegativeOrthant(2) + >>> e1 = [1,1] + >>> e2 = e1 + >>> SLG = SymmetricLinearGame(L,K,e1,e2) + >>> SLG.tolerance_scale(SLG.solution()) + 10.929... + """ norm_p1_opt = norm(solution.player1_optimal()) norm_p2_opt = norm(solution.player2_optimal()) @@ -933,7 +1007,7 @@ class SymmetricLinearGame: Returns ------- - :class:`Solution` + Solution A :class:`Solution` object describing the game's value and the optimal strategies of both players. @@ -1081,9 +1155,9 @@ class SymmetricLinearGame: """ try: opts = {'show_progress': False} - soln_dict = solvers.conelp(self._c(), - self._G(), - self._h(), + soln_dict = solvers.conelp(self.c(), + self.G(), + self.h(), self.C().cvxopt_dims(), self.A(), self.b(), @@ -1141,12 +1215,14 @@ class SymmetricLinearGame: # same. Even if CVXOPT bails out due to numerical difficulty, # it will have some candidate points in mind. If those # candidates are good enough, we take them. We do the same - # check (perhaps pointlessly so) for "optimal" results. + # check for "optimal" results. # # First we check that the primal/dual objective values are - # close enough (one could be low by ABS_TOL, the other high by - # it) because otherwise CVXOPT might return "unknown" and give - # us two points in the cone that are nowhere near optimal. + # close enough because otherwise CVXOPT might return "unknown" + # and give us two points in the cone that are nowhere near + # optimal. And in fact, we need to ensure that they're close + # for "optimal" results, too, because we need to know how + # lenient to be in our testing. # if abs(p1_value - p2_value) > self.tolerance_scale(soln)*ABS_TOL: printing.options['dformat'] = DEBUG_FLOAT_FORMAT @@ -1170,8 +1246,12 @@ class SymmetricLinearGame: can show up. We define the condition number of this game to be the average of the condition numbers of ``G`` and ``A`` in the CVXOPT construction. If the condition number of this game is - high, then you can expect numerical difficulty (such as - :class:`PoorScalingException`). + high, you can problems like :class:`PoorScalingException`. + + Random testing shows that a condition number of around ``125`` + is about the best that we can solve reliably. However, the + failures are intermittent, and you may get lucky with an + ill-conditioned game. Returns ------- @@ -1193,7 +1273,7 @@ class SymmetricLinearGame: 1.809... """ - return (condition_number(self._G()) + condition_number(self.A()))/2 + return (condition_number(self.G()) + condition_number(self.A()))/2 def dual(self): @@ -1225,8 +1305,7 @@ class SymmetricLinearGame: [ 3], e2 = [ 1] [ 1] - [ 1], - Condition((L, K, e1, e2)) = 44.476... + [ 1] """ # We pass ``self.L()`` right back into the constructor, because