]> gitweb.michael.orlitzky.com - dunshire.git/blobdiff - dunshire/games.py
Fix a bug in my two-tolerance strategy and add a test case for it.
[dunshire.git] / dunshire / games.py
index e25db28a8e7349e88b9bdd18f16d0eea0b5c03c1..08a6d6f342b59f93d26fbdeae320034f95004321 100644 (file)
@@ -12,8 +12,6 @@ from .matrices import append_col, append_row, condition_number, identity
 from . import options
 
 printing.options['dformat'] = options.FLOAT_FORMAT
-solvers.options['show_progress'] = options.VERBOSE
-
 
 class Solution:
     """
@@ -222,7 +220,7 @@ class SymmetricLinearGame:
           e2 = [ 1]
                [ 2]
                [ 3],
-          Condition((L, K, e1, e2)) = 31.834895.
+          Condition((L, K, e1, e2)) = 31.834...
 
     Lists can (and probably should) be used for every argument::
 
@@ -241,7 +239,7 @@ class SymmetricLinearGame:
                [ 1],
           e2 = [ 1]
                [ 1],
-          Condition((L, K, e1, e2)) = 1.707107.
+          Condition((L, K, e1, e2)) = 1.707...
 
     The points ``e1`` and ``e2`` can also be passed as some other
     enumerable type (of the correct length) without much harm, since
@@ -264,7 +262,7 @@ class SymmetricLinearGame:
                [ 1],
           e2 = [ 1]
                [ 1],
-          Condition((L, K, e1, e2)) = 1.707107.
+          Condition((L, K, e1, e2)) = 1.707...
 
     However, ``L`` will always be intepreted as a list of rows, even
     if it is passed as a :class:`cvxopt.base.matrix` which is
@@ -286,7 +284,7 @@ class SymmetricLinearGame:
                [ 1],
           e2 = [ 1]
                [ 1],
-          Condition((L, K, e1, e2)) = 6.073771.
+          Condition((L, K, e1, e2)) = 6.073...
         >>> L = cvxopt.matrix(L)
         >>> print(L)
         [ 1  3]
@@ -302,7 +300,7 @@ class SymmetricLinearGame:
                [ 1],
           e2 = [ 1]
                [ 1],
-          Condition((L, K, e1, e2)) = 6.073771.
+          Condition((L, K, e1, e2)) = 6.073...
 
     """
     def __init__(self, L, K, e1, e2):
@@ -324,8 +322,6 @@ class SymmetricLinearGame:
         if not self._e2 in K:
             raise ValueError('the point e2 must lie in the interior of K')
 
-        # Cached result of the self._zero() method.
-        self._zero_col = None
 
 
     def __str__(self):
@@ -354,11 +350,35 @@ class SymmetricLinearGame:
         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 ``K.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>
+
         """
-        if self._zero_col is None:
-            # Cache it, it's constant.
-            self._zero_col = matrix(0, (self._K.dimension(), 1), tc='d')
-        return self._zero_col
+        return matrix(0, (self._K.dimension(), 1), tc='d')
 
 
     def _A(self):
@@ -367,25 +387,245 @@ class SymmetricLinearGame:
 
         This matrix ``A``  appears on the right-hand side of ``Ax = b``
         in the statement of the CVXOPT conelp program.
+
+        .. 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 + K.dimension())`` row vector. Its first
+            entry is zero, and the rest are the entries of ``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._K.dimension() + 1), 'd')
 
 
+
     def _G(self):
         r"""
         Return the matrix ``G`` used in our CVXOPT construction.
 
-        Thus matrix ``G``that appears on the left-hand side of ``Gx + s = h``
+        Thus matrix ``G`` appears on the left-hand side of ``Gx + s = h``
         in the statement of the CVXOPT conelp program.
+
+        .. warning::
+
+            It is not safe to cache any of the matrices passed to
+            CVXOPT, because it can clobber them.
+
+        Returns
+        -------
+
+        matrix
+            A ``2*K.dimension()``-by-``1 + K.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>
+
         """
         I = identity(self._K.dimension())
         return append_row(append_col(self._zero(), -I),
                           append_col(self._e1, -self._L))
 
 
-    def solution(self):
+    def _c(self):
         """
-        Solve this linear game and return a :class:`Solution`.
+        Return the vector ``c`` used in our CVXOPT construction.
+
+        The column vector ``c``  appears in the objective function
+        value ``<c,x>`` in the statement of the CVXOPT conelp program.
+
+        .. warning::
+
+            It is not safe to cache any of the matrices passed to
+            CVXOPT, because it can clobber them.
+
+        Returns
+        -------
+
+        matrix
+            A ``K.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.
+
+        The cone ``C`` is the cone over which the conelp program 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):
+        """
+        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.
+
+        .. warning::
+
+            It is not safe to cache any of the matrices passed to
+            CVXOPT, because it can clobber them.
+
+        Returns
+        -------
+
+        matrix
+            A ``2*K.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()])
+
+    def _b(self):
+        """
+        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.
+
+        .. 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 _try_solution(self, tolerance):
+        """
+        Solve this linear game within ``tolerance``, if possible.
+
+        This private function is the one that does all of the actual
+        work for :meth:`solution`. This method accepts a ``tolerance``,
+        and what :meth:`solution` does is call this method twice with
+        two different tolerances. First it tries a strict tolerance, and
+        then it tries a looser one.
+
+        .. warning::
+
+            If you try to be smart and precompute the matrices used by
+            this function (the ones passed to ``conelp``), then you're
+            going to shoot yourself in the foot. CVXOPT can and will
+            clobber some (but not all) of its input matrices. This isn't
+            performance sensitive, so play it safe.
+
+        Parameters
+        ----------
+
+        tolerance : float
+            The absolute tolerance to pass to the CVXOPT solver.
 
         Returns
         -------
@@ -407,76 +647,68 @@ class SymmetricLinearGame:
         Examples
         --------
 
-        This example is computed in Gowda and Ravindran in the section
-        "The value of a Z-transformation"::
+        This game can be solved easily, so the first attempt in
+        :meth:`solution` should succeed::
 
             >>> from dunshire import *
+            >>> from dunshire.matrices import norm
+            >>> 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)
-            >>> print(SLG.solution())
-            Game value: -6.1724138
-            Player 1 optimal:
-              [ 0.5517241]
-              [-0.0000000]
-              [ 0.4482759]
-            Player 2 optimal:
-              [0.4482759]
-              [0.0000000]
-              [0.5517241]
-
-        The value of the following game can be computed using the fact
-        that the identity is invertible::
+            >>> s1 = SLG.solution()
+            >>> s2 = SLG._try_solution(options.ABS_TOL)
+            >>> abs(s1.game_value() - s2.game_value()) < ABS_TOL
+            True
+            >>> norm(s1.player1_optimal() - s2.player1_optimal()) < ABS_TOL
+            True
+            >>> 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 *
-            >>> K = NonnegativeOrthant(3)
-            >>> L = [[1,0,0],[0,1,0],[0,0,1]]
-            >>> e1 = [1,2,3]
-            >>> e2 = [4,5,6]
-            >>> SLG = SymmetricLinearGame(L, K, e1, e2)
-            >>> print(SLG.solution())
-            Game value: 0.0312500
+            >>> 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.0312500]
-              [0.0625000]
-              [0.0937500]
+              [-0.0000000]
+              [ 8.4776631]
             Player 2 optimal:
-              [0.1250000]
-              [0.1562500]
-              [0.1875000]
+              [0.0000000]
+              [0.7158216]
 
         """
-        # The cone "C" that appears in the statement of the CVXOPT
-        # conelp program.
-        C = CartesianProduct(self._K, self._K)
-
-        # The column vector "b" that appears on the right-hand side of
-        # Ax = b in the statement of the CVXOPT conelp program.
-        b = matrix([1], tc='d')
-
-        # The column vector "h" that appears on the right-hand side of
-        # Gx + s = h in the statement of the CVXOPT conelp program.
-        h = matrix([self._zero(), self._zero()])
-
-        # The column vector "c" that appears in the objective function
-        # value <c,x> in the statement of the CVXOPT conelp program.
-        c = matrix([-1, self._zero()])
-
-        # Actually solve the thing and obtain a dictionary describing
-        # what happened.
         try:
-            soln_dict = solvers.conelp(c, self._G(), h,
-                                       C.cvxopt_dims(), self._A(), b)
-        except ValueError as e:
-            if str(e) == 'math domain error':
+            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(),
+                                       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
@@ -511,7 +743,7 @@ class SymmetricLinearGame:
             # value could be under the true optimal by ``ABS_TOL``
             # and the dual value could be over by the same amount.
             #
-            if abs(p1_value - p2_value) > 2*options.ABS_TOL:
+            if abs(p1_value - p2_value) > tolerance:
                 raise GameUnsolvableException(self, soln_dict)
             if (p1_optimal not in self._K) or (p2_optimal not in self._K):
                 raise GameUnsolvableException(self, soln_dict)
@@ -519,6 +751,82 @@ class SymmetricLinearGame:
         return Solution(p1_value, p1_optimal, p2_optimal)
 
 
+    def solution(self):
+        """
+        Solve this linear game and return a :class:`Solution`.
+
+        Returns
+        -------
+
+        :class:`Solution`
+            A :class:`Solution` object describing the game's value and
+            the optimal strategies of both players.
+
+        Raises
+        ------
+        GameUnsolvableException
+            If the game could not be solved (if an optimal solution to its
+            associated cone program was not found).
+
+        PoorScalingException
+            If the game could not be solved because CVXOPT crashed while
+            trying to take the square root of a negative number.
+
+        Examples
+        --------
+
+        This example is computed in Gowda and Ravindran in the section
+        "The value of a Z-transformation"::
+
+            >>> 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)
+            >>> print(SLG.solution())
+            Game value: -6.1724138
+            Player 1 optimal:
+              [ 0.551...]
+              [-0.000...]
+              [ 0.448...]
+            Player 2 optimal:
+              [0.448...]
+              [0.000...]
+              [0.551...]
+
+        The value of the following game can be computed using the fact
+        that the identity is invertible::
+
+            >>> from dunshire import *
+            >>> K = NonnegativeOrthant(3)
+            >>> L = [[1,0,0],[0,1,0],[0,0,1]]
+            >>> e1 = [1,2,3]
+            >>> e2 = [4,5,6]
+            >>> SLG = SymmetricLinearGame(L, K, e1, e2)
+            >>> print(SLG.solution())
+            Game value: 0.0312500
+            Player 1 optimal:
+              [0.031...]
+              [0.062...]
+              [0.093...]
+            Player 2 optimal:
+              [0.125...]
+              [0.156...]
+              [0.187...]
+
+        """
+        try:
+            # First try with a stricter tolerance. Who knows, it might
+            # work. If it does, we prefer that solution.
+            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)
+
+
     def condition(self):
         r"""
         Return the condition number of this game.
@@ -586,7 +894,7 @@ class SymmetricLinearGame:
               e2 = [ 1]
                    [ 1]
                    [ 1],
-              Condition((L, K, e1, e2)) = 44.476765.
+              Condition((L, K, e1, e2)) = 44.476...
 
         """
         # We pass ``self._L`` right back into the constructor, because