]> gitweb.michael.orlitzky.com - dunshire.git/blobdiff - dunshire/games.py
Add a new DEBUG_FLOAT_FORMAT and use it when raising game exceptions.
[dunshire.git] / dunshire / games.py
index 1f3a15a0b261c723f7f5c65d66d255fd2b9fdc99..2d6d6dae8d75352e4876954ba0cc2eff454c67f7 100644 (file)
@@ -8,7 +8,8 @@ knows how to solve a linear game.
 from cvxopt import matrix, printing, solvers
 from .cones import CartesianProduct
 from .errors import GameUnsolvableException, PoorScalingException
-from .matrices import append_col, append_row, condition_number, identity
+from .matrices import (append_col, append_row, condition_number, identity,
+                       inner_product)
 from . import options
 
 printing.options['dformat'] = options.FLOAT_FORMAT
@@ -345,6 +346,205 @@ class SymmetricLinearGame:
                           self.condition())
 
 
+    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 *
+            >>> from dunshire.options import ABS_TOL
+            >>> 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()
+            >>> abs(SLG.payoff(x_bar, y_bar) - soln.game_value()) < ABS_TOL
+            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``.
@@ -360,7 +560,7 @@ class SymmetricLinearGame:
         -------
 
         matrix
-            A ``K.dimension()``-by-``1`` column vector of zeros.
+            A ``self.dimension()``-by-``1`` column vector of zeros.
 
         Examples
         --------
@@ -378,7 +578,7 @@ class SymmetricLinearGame:
             <BLANKLINE>
 
         """
-        return matrix(0, (self._K.dimension(), 1), tc='d')
+        return matrix(0, (self.dimension(), 1), tc='d')
 
 
     def _A(self):
@@ -397,7 +597,7 @@ class SymmetricLinearGame:
         -------
 
         matrix
-            A ``1``-by-``(1 + K.dimension())`` row vector. Its first
+            A ``1``-by-``(1 + self.dimension())`` row vector. Its first
             entry is zero, and the rest are the entries of ``e2``.
 
         Examples
@@ -414,7 +614,7 @@ class SymmetricLinearGame:
             <BLANKLINE>
 
         """
-        return matrix([0, self._e2], (1, self._K.dimension() + 1), 'd')
+        return matrix([0, self._e2], (1, self.dimension() + 1), 'd')
 
 
 
@@ -434,7 +634,7 @@ class SymmetricLinearGame:
         -------
 
         matrix
-            A ``2*K.dimension()``-by-``1 + K.dimension()`` matrix.
+            A ``2*self.dimension()``-by-``(1 + self.dimension())`` matrix.
 
         Examples
         --------
@@ -455,8 +655,8 @@ class SymmetricLinearGame:
             <BLANKLINE>
 
         """
-        I = identity(self._K.dimension())
-        return append_row(append_col(self._zero(), -I),
+        identity_matrix = identity(self.dimension())
+        return append_row(append_col(self._zero(), -identity_matrix),
                           append_col(self._e1, -self._L))
 
 
@@ -476,7 +676,7 @@ class SymmetricLinearGame:
         -------
 
         matrix
-            A ``K.dimension()``-by-``1`` column vector.
+            A ``self.dimension()``-by-``1`` column vector.
 
         Examples
         --------
@@ -544,7 +744,7 @@ class SymmetricLinearGame:
         -------
 
         matrix
-            A ``2*K.dimension()``-by-``1`` column vector of zeros.
+            A ``2*self.dimension()``-by-``1`` column vector of zeros.
 
         Examples
         --------
@@ -568,13 +768,19 @@ class SymmetricLinearGame:
 
         return matrix([self._zero(), self._zero()])
 
-    def _b(self):
+
+    @staticmethod
+    def _b():
         """
         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.
 
+        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
@@ -667,24 +873,48 @@ class SymmetricLinearGame:
             >>> norm(s1.player2_optimal() - s2.player2_optimal()) < ABS_TOL
             True
 
+        This game cannot be solved with the default tolerance, but it
+        can be solved with a weaker one::
+
+            >>> from dunshire import *
+            >>> from dunshire.options import ABS_TOL
+            >>> L = [[ 0.58538005706658102767,  1.53764301129883040886],
+            ...      [-1.34901059721452210027,  1.50121179114155500756]]
+            >>> K = NonnegativeOrthant(2)
+            >>> e1 = [1.04537193228494995623, 1.39699624965841895374]
+            >>> e2 = [0.35326554172108337593, 0.11795703527854853321]
+            >>> SLG = SymmetricLinearGame(L,K,e1,e2)
+            >>> print(SLG._try_solution(ABS_TOL / 10))
+            Traceback (most recent call last):
+            ...
+            dunshire.errors.GameUnsolvableException: Solution failed...
+            >>> print(SLG._try_solution(ABS_TOL))
+            Game value: 9.1100945
+            Player 1 optimal:
+              [-0.0000000]
+              [ 8.4776631]
+            Player 2 optimal:
+              [0.0000000]
+              [0.7158216]
+
         """
         try:
-            solvers.options['show_progress'] = options.VERBOSE
-            solvers.options['abs_tol'] = tolerance
+            opts = {'show_progress': options.VERBOSE, 'abstol': tolerance}
             soln_dict = solvers.conelp(self._c(),
                                        self._G(),
                                        self._h(),
                                        self._C().cvxopt_dims(),
                                        self._A(),
-                                       self._b())
-        except ValueError as e:
-            if str(e) == 'math domain error':
+                                       self._b(),
+                                       options=opts)
+        except ValueError as error:
+            if str(error) == 'math domain error':
                 # Oops, CVXOPT tried to take the square root of a
                 # negative number. Report some details about the game
                 # rather than just the underlying CVXOPT crash.
                 raise PoorScalingException(self)
             else:
-                raise e
+                raise error
 
         # The optimal strategies are named ``p`` and ``q`` in the
         # background documentation, and we need to extract them from
@@ -695,7 +925,7 @@ class SymmetricLinearGame:
         p1_value = -soln_dict['primal objective']
         p2_value = -soln_dict['dual objective']
         p1_optimal = soln_dict['x'][1:]
-        p2_optimal = soln_dict['z'][self._K.dimension():]
+        p2_optimal = soln_dict['z'][self.dimension():]
 
         # The "status" field contains "optimal" if everything went
         # according to plan. Other possible values are "primal
@@ -798,9 +1028,14 @@ class SymmetricLinearGame:
             return self._try_solution(options.ABS_TOL / 10)
 
         except (PoorScalingException, GameUnsolvableException):
-            # Ok, that didn't work. Let's try it with the default
-            # tolerance, and whatever happens, happens.
-            return self._try_solution(options.ABS_TOL)
+            # Ok, that didn't work. Let's try it with the default tolerance..
+            try:
+                return self._try_solution(options.ABS_TOL / 10)
+            except (PoorScalingException, GameUnsolvableException) as error:
+                # Well, that didn't work either. Let's verbosify the matrix
+                # output format before we allow the exception to be raised.
+                printing.options['dformat'] = options.DEBUG_FLOAT_FORMAT
+                raise error
 
 
     def condition(self):