+ ' 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())
+
+ return tpl.format(indented_L,
+ str(self.K()),
+ indented_e1,
+ indented_e2)
+
+
+ def L(self):
+ """
+ Return the matrix ``L`` passed to the constructor.
+
+ Returns
+ -------
+
+ matrix
+ The matrix that defines this game's :meth:`payoff` operator.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,2,3]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.L())
+ [ 1 -5 -15]
+ [ -1 2 -3]
+ [-12 -15 1]
+ <BLANKLINE>
+
+ """
+ return self._L
+
+
+ def K(self):
+ """
+ Return the cone over which this game is played.
+
+ Returns
+ -------
+
+ SymmetricCone
+ The :class:`SymmetricCone` over which this game is played.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,2,3]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.K())
+ Nonnegative orthant in the real 3-space
+
+ """
+ return self._K
+
+
+ def e1(self):
+ """
+ Return player one's interior point.
+
+ Returns
+ -------
+
+ matrix
+ The point interior to :meth:`K` affiliated with player one.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,2,3]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.e1())
+ [ 1]
+ [ 1]
+ [ 1]
+ <BLANKLINE>
+
+ """
+ return self._e1
+
+
+ def e2(self):
+ """
+ Return player two's interior point.
+
+ Returns
+ -------
+
+ matrix
+ The point interior to :meth:`K` affiliated with player one.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,2,3]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.e2())
+ [ 1]
+ [ 2]
+ [ 3]
+ <BLANKLINE>
+
+ """
+ return self._e2
+
+
+ def payoff(self, strategy1, strategy2):
+ r"""
+ Return the payoff associated with ``strategy1`` and ``strategy2``.
+
+ 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
+ :meth:`L`. This method computes the payoff given the two
+ players' strategies.
+
+ Parameters
+ ----------
+
+ strategy1 : matrix
+ Player one's strategy.
+
+ strategy2 : matrix
+ Player two's strategy.
+
+ Returns
+ -------
+
+ float
+ The payoff for the game when player one plays ``strategy1``
+ and player two plays ``strategy2``.
+
+ Examples
+ --------
+
+ The value of the game should be the payoff at the optimal
+ strategies::
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> soln = SLG.solution()
+ >>> x_bar = soln.player1_optimal()
+ >>> y_bar = soln.player2_optimal()
+ >>> SLG.payoff(x_bar, y_bar) == soln.game_value()
+ True
+
+ """
+ return inner_product(self.L()*strategy1, strategy2)
+
+
+ def dimension(self):
+ """
+ Return the dimension of this game.
+
+ The dimension of a game is not needed for the theory, but it is
+ useful for the implementation. We define the dimension of a game
+ to be the dimension of its underlying cone. Or what is the same,
+ the dimension of the space from which the strategies are chosen.
+
+ Returns
+ -------
+
+ int
+ The dimension of the cone :meth:`K`, or of the space where
+ this game is played.
+
+ Examples
+ --------
+
+ The dimension of a game over the nonnegative quadrant in the
+ plane should be two (the dimension of the plane)::
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(2)
+ >>> L = [[1,-5],[-1,2]]
+ >>> e1 = [1,1]
+ >>> e2 = [1,4]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> SLG.dimension()
+ 2
+
+ """
+ return self.K().dimension()
+
+
+ def _zero(self):
+ """
+ Return a column of zeros that fits ``K``.
+
+ This is used in our CVXOPT construction.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A ``self.dimension()``-by-``1`` column vector of zeros.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = identity(3)
+ >>> e1 = [1,1,1]
+ >>> e2 = e1
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG._zero())
+ [0.0000000]
+ [0.0000000]
+ [0.0000000]
+ <BLANKLINE>
+
+ """
+ return matrix(0, (self.dimension(), 1), tc='d')
+
+
+ def A(self):
+ r"""
+ Return the matrix ``A`` used in our CVXOPT construction.
+
+ This matrix :math:`A` appears on the right-hand side of
+ :math:`Ax = b` in the `statement of the CVXOPT conelp program
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A ``1``-by-``(1 + self.dimension())`` row vector. Its first
+ entry is zero, and the rest are the entries of :meth:`e2`.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[1,1,1],[1,1,1],[1,1,1]]
+ >>> e1 = [1,1,1]
+ >>> e2 = [1,2,3]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.A())
+ [0.0000000 1.0000000 2.0000000 3.0000000]
+ <BLANKLINE>
+
+ """
+ return matrix([0, self.e2()], (1, self.dimension() + 1), 'd')
+
+
+
+ def G(self):
+ r"""
+ Return the matrix ``G`` used in our CVXOPT construction.
+
+ Thus matrix :math:`G` appears on the left-hand side of :math:`Gx
+ + s = h` in the `statement of the CVXOPT conelp program
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A ``2*self.dimension()``-by-``(1 + self.dimension())`` matrix.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[4,5,6],[7,8,9],[10,11,12]]
+ >>> e1 = [1,2,3]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> 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]
+ [ 1.0000000 -4.0000000 -5.0000000 -6.0000000]
+ [ 2.0000000 -7.0000000 -8.0000000 -9.0000000]
+ [ 3.0000000 -10.0000000 -11.0000000 -12.0000000]
+ <BLANKLINE>
+
+ """
+ identity_matrix = identity(self.dimension())
+ return append_row(append_col(self._zero(), -identity_matrix),
+ append_col(self.e1(), -self.L()))
+
+
+ def c(self):
+ r"""
+ Return the vector ``c`` used in our CVXOPT construction.
+
+ 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
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A :meth:`dimension`-by-``1`` column vector.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[4,5,6],[7,8,9],[10,11,12]]
+ >>> e1 = [1,2,3]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.c())
+ [-1.0000000]
+ [ 0.0000000]
+ [ 0.0000000]
+ [ 0.0000000]
+ <BLANKLINE>
+
+ """
+ return matrix([-1, self._zero()])
+
+
+ def C(self):
+ """
+ Return the cone ``C`` used in our CVXOPT construction.
+
+ This is the cone over which the `CVXOPT conelp program
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_
+ takes place.
+
+ Returns
+ -------
+
+ CartesianProduct
+ The cartesian product of ``K`` with itself.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[4,5,6],[7,8,9],[10,11,12]]
+ >>> e1 = [1,2,3]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.C())
+ Cartesian product of dimension 6 with 2 factors:
+ * Nonnegative orthant in the real 3-space
+ * Nonnegative orthant in the real 3-space
+
+ """
+ return CartesianProduct(self._K, self._K)
+
+ def h(self):
+ r"""
+ Return the ``h`` vector used in our CVXOPT construction.
+
+ The :math:`h` vector appears on the right-hand side of :math:`Gx
+ + s = h` in the `statement of the CVXOPT conelp program
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A ``2*self.dimension()``-by-``1`` column vector of zeros.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[4,5,6],[7,8,9],[10,11,12]]
+ >>> e1 = [1,2,3]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.h())
+ [0.0000000]
+ [0.0000000]
+ [0.0000000]
+ [0.0000000]
+ [0.0000000]
+ [0.0000000]
+ <BLANKLINE>
+
+ """
+
+ return matrix([self._zero(), self._zero()])
+
+
+ @staticmethod
+ def b():
+ r"""
+ Return the ``b`` vector used in our CVXOPT construction.
+
+ The vector :math:`b` appears on the right-hand side of :math:`Ax
+ = b` in the `statement of the CVXOPT conelp program
+ <https://cvxopt.org/userguide/coneprog.html#linear-cone-programs>`_.
+
+ This method is static because the dimensions and entries of
+ ``b`` are known beforehand, and don't depend on any other
+ properties of the game.
+
+ .. warning::
+
+ It is not safe to cache any of the matrices passed to
+ CVXOPT, because it can clobber them.
+
+ Returns
+ -------
+
+ matrix
+ A ``1``-by-``1`` matrix containing a single entry ``1``.
+
+ Examples
+ --------
+
+ >>> from dunshire import *
+ >>> K = NonnegativeOrthant(3)
+ >>> L = [[4,5,6],[7,8,9],[10,11,12]]
+ >>> e1 = [1,2,3]
+ >>> e2 = [1,1,1]
+ >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+ >>> print(SLG.b())
+ [1.0000000]
+ <BLANKLINE>
+
+ """
+ return matrix([1], tc='d')
+
+
+ def player1_start(self):
+ """
+ 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 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
+
+ return {'x': x, 's': s}
+
+
+ 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())
+ omega = self._L_specnorm()/(dist*norm(self.e1()))
+ y = matrix([omega])
+ z2 = q
+ z1 = y*self.e2() - self.L().trans()*z2
+ z = matrix([z1, z2], (self.dimension()*2, 1))
+
+ return {'y': y, 'z': z}
+
+
+ def _L_specnorm(self):
+ """
+ Compute the spectral norm of :meth:`L` and cache it.
+
+ The spectral norm of the matrix :meth:`L` is used in a few
+ places. Since it can be expensive to compute, we want to cache
+ its value. That is not possible in :func:`specnorm`, which lies
+ outside of a class, so this is the place to do it.
+
+ Returns
+ -------
+
+ float
+ 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())
+ return self._L_specnorm_value
+
+
+ def tolerance_scale(self, solution):
+ r"""
+
+ 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
+
+ .. math::
+
+ \left\lVert L \right\rVert_{2}
+ \left( \left\lVert \bar{x} \right\rVert
+ + \left\lVert \bar{y} \right\rVert
+ \right),
+
+ where :math:`\bar{x}` and :math:`\bar{y}` are optimal solutions
+ for players one and two respectively. This scaling factor is not
+ formally justified, but attempting anything smaller leads to
+ test failures.
+
+ .. warning::
+
+ Optimal solutions are not unique, so the scaling factor
+ obtained from ``solution`` may not work when comparing other
+ solutions.
+
+ Parameters
+ ----------
+
+ solution : Solution
+ A solution of this game, used to obtain the norms of the
+ optimal strategies.
+
+ Returns
+ -------
+
+ float
+ 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())
+ scale = self._L_specnorm()*(norm_p1_opt + norm_p2_opt)
+
+ # Don't return anything smaller than 1... we can't go below
+ # out "minimum tolerance."
+ return max(1, scale)