2 Symmetric linear games and their solutions.
4 This module contains the main :class:`SymmetricLinearGame` class that
5 knows how to solve a linear game.
8 from cvxopt
import matrix
, printing
, solvers
9 from .cones
import CartesianProduct
10 from .errors
import GameUnsolvableException
, PoorScalingException
11 from .matrices
import (append_col
, append_row
, condition_number
, identity
,
15 printing
.options
['dformat'] = options
.FLOAT_FORMAT
19 A representation of the solution of a linear game. It should contain
20 the value of the game, and both players' strategies.
25 >>> print(Solution(10, matrix([1,2]), matrix([3,4])))
26 Game value: 10.0000000
35 def __init__(self
, game_value
, p1_optimal
, p2_optimal
):
37 Create a new Solution object from a game value and two optimal
38 strategies for the players.
40 self
._game
_value
= game_value
41 self
._player
1_optimal
= p1_optimal
42 self
._player
2_optimal
= p2_optimal
46 Return a string describing the solution of a linear game.
48 The three data that are described are,
50 * The value of the game.
51 * The optimal strategy of player one.
52 * The optimal strategy of player two.
54 The two optimal strategy vectors are indented by two spaces.
56 tpl
= 'Game value: {:.7f}\n' \
57 'Player 1 optimal:{:s}\n' \
58 'Player 2 optimal:{:s}'
60 p1_str
= '\n{!s}'.format(self
.player1_optimal())
61 p1_str
= '\n '.join(p1_str
.splitlines())
62 p2_str
= '\n{!s}'.format(self
.player2_optimal())
63 p2_str
= '\n '.join(p2_str
.splitlines())
65 return tpl
.format(self
.game_value(), p1_str
, p2_str
)
70 Return the game value for this solution.
75 >>> s = Solution(10, matrix([1,2]), matrix([3,4]))
80 return self
._game
_value
83 def player1_optimal(self
):
85 Return player one's optimal strategy in this solution.
90 >>> s = Solution(10, matrix([1,2]), matrix([3,4]))
91 >>> print(s.player1_optimal())
97 return self
._player
1_optimal
100 def player2_optimal(self
):
102 Return player two's optimal strategy in this solution.
107 >>> s = Solution(10, matrix([1,2]), matrix([3,4]))
108 >>> print(s.player2_optimal())
114 return self
._player
2_optimal
117 class SymmetricLinearGame
:
119 A representation of a symmetric linear game.
121 The data for a symmetric linear game are,
123 * A "payoff" operator ``L``.
124 * A symmetric cone ``K``.
125 * Two points ``e1`` and ``e2`` in the interior of ``K``.
127 The ambient space is assumed to be the span of ``K``.
129 With those data understood, the game is played as follows. Players
130 one and two choose points :math:`x` and :math:`y` respectively, from
131 their respective strategy sets,
138 x \in K \ \middle|\ \left\langle x, e_{2} \right\rangle = 1
143 y \in K \ \middle|\ \left\langle y, e_{1} \right\rangle = 1
147 Afterwards, a "payout" is computed as :math:`\left\langle
148 L\left(x\right), y \right\rangle` and is paid to player one out of
149 player two's pocket. The game is therefore zero sum, and we suppose
150 that player one would like to guarantee himself the largest minimum
151 payout possible. That is, player one wishes to,
156 &\underset{y \in \Delta_{2}}{\min}\left(
157 \left\langle L\left(x\right), y \right\rangle
159 \text{subject to } & x \in \Delta_{1}.
162 Player two has the simultaneous goal to,
167 &\underset{x \in \Delta_{1}}{\max}\left(
168 \left\langle L\left(x\right), y \right\rangle
170 \text{subject to } & y \in \Delta_{2}.
173 These goals obviously conflict (the game is zero sum), but an
174 existence theorem guarantees at least one optimal min-max solution
175 from which neither player would like to deviate. This class is
176 able to find such a solution.
181 L : list of list of float
182 A matrix represented as a list of ROWS. This representation
183 agrees with (for example) SageMath and NumPy, but not with CVXOPT
184 (whose matrix constructor accepts a list of columns).
186 K : :class:`SymmetricCone`
187 The symmetric cone instance over which the game is played.
190 The interior point of ``K`` belonging to player one; it
191 can be of any iterable type having the correct length.
194 The interior point of ``K`` belonging to player two; it
195 can be of any enumerable type having the correct length.
201 If either ``e1`` or ``e2`` lie outside of the cone ``K``.
206 >>> from dunshire import *
207 >>> K = NonnegativeOrthant(3)
208 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
211 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
213 The linear game (L, K, e1, e2) where
217 K = Nonnegative orthant in the real 3-space,
224 Condition((L, K, e1, e2)) = 31.834...
226 Lists can (and probably should) be used for every argument::
228 >>> from dunshire import *
229 >>> K = NonnegativeOrthant(2)
230 >>> L = [[1,0],[0,1]]
233 >>> G = SymmetricLinearGame(L, K, e1, e2)
235 The linear game (L, K, e1, e2) where
238 K = Nonnegative orthant in the real 2-space,
243 Condition((L, K, e1, e2)) = 1.707...
245 The points ``e1`` and ``e2`` can also be passed as some other
246 enumerable type (of the correct length) without much harm, since
247 there is no row/column ambiguity::
251 >>> from dunshire import *
252 >>> K = NonnegativeOrthant(2)
253 >>> L = [[1,0],[0,1]]
254 >>> e1 = cvxopt.matrix([1,1])
255 >>> e2 = numpy.matrix([1,1])
256 >>> G = SymmetricLinearGame(L, K, e1, e2)
258 The linear game (L, K, e1, e2) where
261 K = Nonnegative orthant in the real 2-space,
266 Condition((L, K, e1, e2)) = 1.707...
268 However, ``L`` will always be intepreted as a list of rows, even
269 if it is passed as a :class:`cvxopt.base.matrix` which is
270 otherwise indexed by columns::
273 >>> from dunshire import *
274 >>> K = NonnegativeOrthant(2)
275 >>> L = [[1,2],[3,4]]
278 >>> G = SymmetricLinearGame(L, K, e1, e2)
280 The linear game (L, K, e1, e2) where
283 K = Nonnegative orthant in the real 2-space,
288 Condition((L, K, e1, e2)) = 6.073...
289 >>> L = cvxopt.matrix(L)
294 >>> G = SymmetricLinearGame(L, K, e1, e2)
296 The linear game (L, K, e1, e2) where
299 K = Nonnegative orthant in the real 2-space,
304 Condition((L, K, e1, e2)) = 6.073...
307 def __init__(self
, L
, K
, e1
, e2
):
309 Create a new SymmetricLinearGame object.
312 self
._e
1 = matrix(e1
, (K
.dimension(), 1))
313 self
._e
2 = matrix(e2
, (K
.dimension(), 1))
315 # Our input ``L`` is indexed by rows but CVXOPT matrices are
316 # indexed by columns, so we need to transpose the input before
317 # feeding it to CVXOPT.
318 self
._L = matrix(L
, (K
.dimension(), K
.dimension())).trans()
320 if not self
._e
1 in K
:
321 raise ValueError('the point e1 must lie in the interior of K')
323 if not self
._e
2 in K
:
324 raise ValueError('the point e2 must lie in the interior of K')
330 Return a string representation of this game.
332 tpl
= 'The linear game (L, K, e1, e2) where\n' \
337 ' Condition((L, K, e1, e2)) = {:f}.'
338 indented_L
= '\n '.join(str(self
._L).splitlines())
339 indented_e1
= '\n '.join(str(self
._e
1).splitlines())
340 indented_e2
= '\n '.join(str(self
._e
2).splitlines())
342 return tpl
.format(indented_L
,
351 Return the matrix ``L`` passed to the constructor.
357 The matrix that defines this game's :meth:`payoff` operator.
362 >>> from dunshire import *
363 >>> K = NonnegativeOrthant(3)
364 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
367 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
380 Return the cone over which this game is played.
386 The :class:`SymmetricCone` over which this game is played.
391 >>> from dunshire import *
392 >>> K = NonnegativeOrthant(3)
393 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
396 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
398 Nonnegative orthant in the real 3-space
406 Return player one's interior point.
412 The point interior to :meth:`K` affiliated with player one.
417 >>> from dunshire import *
418 >>> K = NonnegativeOrthant(3)
419 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
422 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
435 Return player two's interior point.
441 The point interior to :meth:`K` affiliated with player one.
446 >>> from dunshire import *
447 >>> K = NonnegativeOrthant(3)
448 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
451 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
462 def payoff(self
, strategy1
, strategy2
):
464 Return the payoff associated with ``strategy1`` and ``strategy2``.
466 The payoff operator takes pairs of strategies to a real
467 number. For example, if player one's strategy is :math:`x` and
468 player two's strategy is :math:`y`, then the associated payoff
469 is :math:`\left\langle L\left(x\right),y \right\rangle` \in
470 \mathbb{R}. Here, :math:`L` denotes the same linear operator as
471 :meth:`L`. This method computes the payoff given the two
478 Player one's strategy.
481 Player two's strategy.
487 The payoff for the game when player one plays ``strategy1``
488 and player two plays ``strategy2``.
493 The value of the game should be the payoff at the optimal
496 >>> from dunshire import *
497 >>> from dunshire.options import ABS_TOL
498 >>> K = NonnegativeOrthant(3)
499 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
502 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
503 >>> soln = SLG.solution()
504 >>> x_bar = soln.player1_optimal()
505 >>> y_bar = soln.player2_optimal()
506 >>> abs(SLG.payoff(x_bar, y_bar) - soln.game_value()) < ABS_TOL
510 return inner_product(self
.L()*strategy1
, strategy2
)
515 Return the dimension of this game.
517 The dimension of a game is not needed for the theory, but it is
518 useful for the implementation. We define the dimension of a game
519 to be the dimension of its underlying cone. Or what is the same,
520 the dimension of the space from which the strategies are chosen.
526 The dimension of the cone :meth:`K`, or of the space where
532 The dimension of a game over the nonnegative quadrant in the
533 plane should be two (the dimension of the plane)::
535 >>> from dunshire import *
536 >>> K = NonnegativeOrthant(2)
537 >>> L = [[1,-5],[-1,2]]
540 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
545 return self
.K().dimension()
550 Return a column of zeros that fits ``K``.
552 This is used in our CVXOPT construction.
556 It is not safe to cache any of the matrices passed to
557 CVXOPT, because it can clobber them.
563 A ``self.dimension()``-by-``1`` column vector of zeros.
568 >>> from dunshire import *
569 >>> K = NonnegativeOrthant(3)
573 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
574 >>> print(SLG._zero())
581 return matrix(0, (self
.dimension(), 1), tc
='d')
586 Return the matrix ``A`` used in our CVXOPT construction.
588 This matrix ``A`` appears on the right-hand side of ``Ax = b``
589 in the statement of the CVXOPT conelp program.
593 It is not safe to cache any of the matrices passed to
594 CVXOPT, because it can clobber them.
600 A ``1``-by-``(1 + self.dimension())`` row vector. Its first
601 entry is zero, and the rest are the entries of ``e2``.
606 >>> from dunshire import *
607 >>> K = NonnegativeOrthant(3)
608 >>> L = [[1,1,1],[1,1,1],[1,1,1]]
611 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
613 [0.0000000 1.0000000 2.0000000 3.0000000]
617 return matrix([0, self
._e
2], (1, self
.dimension() + 1), 'd')
623 Return the matrix ``G`` used in our CVXOPT construction.
625 Thus matrix ``G`` appears on the left-hand side of ``Gx + s = h``
626 in the statement of the CVXOPT conelp program.
630 It is not safe to cache any of the matrices passed to
631 CVXOPT, because it can clobber them.
637 A ``2*self.dimension()``-by-``(1 + self.dimension())`` matrix.
642 >>> from dunshire import *
643 >>> K = NonnegativeOrthant(3)
644 >>> L = [[4,5,6],[7,8,9],[10,11,12]]
647 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
649 [ 0.0000000 -1.0000000 0.0000000 0.0000000]
650 [ 0.0000000 0.0000000 -1.0000000 0.0000000]
651 [ 0.0000000 0.0000000 0.0000000 -1.0000000]
652 [ 1.0000000 -4.0000000 -5.0000000 -6.0000000]
653 [ 2.0000000 -7.0000000 -8.0000000 -9.0000000]
654 [ 3.0000000 -10.0000000 -11.0000000 -12.0000000]
658 identity_matrix
= identity(self
.dimension())
659 return append_row(append_col(self
._zero
(), -identity_matrix
),
660 append_col(self
._e
1, -self
._L))
665 Return the vector ``c`` used in our CVXOPT construction.
667 The column vector ``c`` appears in the objective function
668 value ``<c,x>`` in the statement of the CVXOPT conelp program.
672 It is not safe to cache any of the matrices passed to
673 CVXOPT, because it can clobber them.
679 A ``self.dimension()``-by-``1`` column vector.
684 >>> from dunshire import *
685 >>> K = NonnegativeOrthant(3)
686 >>> L = [[4,5,6],[7,8,9],[10,11,12]]
689 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
698 return matrix([-1, self
._zero
()])
703 Return the cone ``C`` used in our CVXOPT construction.
705 The cone ``C`` is the cone over which the conelp program takes
712 The cartesian product of ``K`` with itself.
717 >>> from dunshire import *
718 >>> K = NonnegativeOrthant(3)
719 >>> L = [[4,5,6],[7,8,9],[10,11,12]]
722 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
724 Cartesian product of dimension 6 with 2 factors:
725 * Nonnegative orthant in the real 3-space
726 * Nonnegative orthant in the real 3-space
729 return CartesianProduct(self
._K
, self
._K
)
733 Return the ``h`` vector used in our CVXOPT construction.
735 The ``h`` vector appears on the right-hand side of :math:`Gx + s
736 = h` in the statement of the CVXOPT conelp program.
740 It is not safe to cache any of the matrices passed to
741 CVXOPT, because it can clobber them.
747 A ``2*self.dimension()``-by-``1`` column vector of zeros.
752 >>> from dunshire import *
753 >>> K = NonnegativeOrthant(3)
754 >>> L = [[4,5,6],[7,8,9],[10,11,12]]
757 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
769 return matrix([self
._zero
(), self
._zero
()])
775 Return the ``b`` vector used in our CVXOPT construction.
777 The vector ``b`` appears on the right-hand side of :math:`Ax =
778 b` in the statement of the CVXOPT conelp program.
780 This method is static because the dimensions and entries of
781 ``b`` are known beforehand, and don't depend on any other
782 properties of the game.
786 It is not safe to cache any of the matrices passed to
787 CVXOPT, because it can clobber them.
793 A ``1``-by-``1`` matrix containing a single entry ``1``.
798 >>> from dunshire import *
799 >>> K = NonnegativeOrthant(3)
800 >>> L = [[4,5,6],[7,8,9],[10,11,12]]
803 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
809 return matrix([1], tc
='d')
812 def _try_solution(self
, tolerance
):
814 Solve this linear game within ``tolerance``, if possible.
816 This private function is the one that does all of the actual
817 work for :meth:`solution`. This method accepts a ``tolerance``,
818 and what :meth:`solution` does is call this method twice with
819 two different tolerances. First it tries a strict tolerance, and
820 then it tries a looser one.
824 If you try to be smart and precompute the matrices used by
825 this function (the ones passed to ``conelp``), then you're
826 going to shoot yourself in the foot. CVXOPT can and will
827 clobber some (but not all) of its input matrices. This isn't
828 performance sensitive, so play it safe.
834 The absolute tolerance to pass to the CVXOPT solver.
840 A :class:`Solution` object describing the game's value and
841 the optimal strategies of both players.
845 GameUnsolvableException
846 If the game could not be solved (if an optimal solution to its
847 associated cone program was not found).
850 If the game could not be solved because CVXOPT crashed while
851 trying to take the square root of a negative number.
856 This game can be solved easily, so the first attempt in
857 :meth:`solution` should succeed::
859 >>> from dunshire import *
860 >>> from dunshire.matrices import norm
861 >>> from dunshire.options import ABS_TOL
862 >>> K = NonnegativeOrthant(3)
863 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
866 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
867 >>> s1 = SLG.solution()
868 >>> s2 = SLG._try_solution(options.ABS_TOL)
869 >>> abs(s1.game_value() - s2.game_value()) < ABS_TOL
871 >>> norm(s1.player1_optimal() - s2.player1_optimal()) < ABS_TOL
873 >>> norm(s1.player2_optimal() - s2.player2_optimal()) < ABS_TOL
876 This game cannot be solved with the default tolerance, but it
877 can be solved with a weaker one::
879 >>> from dunshire import *
880 >>> from dunshire.options import ABS_TOL
881 >>> L = [[ 0.58538005706658102767, 1.53764301129883040886],
882 ... [-1.34901059721452210027, 1.50121179114155500756]]
883 >>> K = NonnegativeOrthant(2)
884 >>> e1 = [1.04537193228494995623, 1.39699624965841895374]
885 >>> e2 = [0.35326554172108337593, 0.11795703527854853321]
886 >>> SLG = SymmetricLinearGame(L,K,e1,e2)
887 >>> print(SLG._try_solution(ABS_TOL / 10))
888 Traceback (most recent call last):
890 dunshire.errors.GameUnsolvableException: Solution failed...
891 >>> print(SLG._try_solution(ABS_TOL))
892 Game value: 9.1100945
902 opts
= {'show_progress': options.VERBOSE, 'abstol': tolerance}
903 soln_dict
= solvers
.conelp(self
._c
(),
906 self
._C
().cvxopt_dims(),
910 except ValueError as error
:
911 if str(error
) == 'math domain error':
912 # Oops, CVXOPT tried to take the square root of a
913 # negative number. Report some details about the game
914 # rather than just the underlying CVXOPT crash.
915 raise PoorScalingException(self
)
919 # The optimal strategies are named ``p`` and ``q`` in the
920 # background documentation, and we need to extract them from
921 # the CVXOPT ``x`` and ``z`` variables. The objective values
922 # :math:`nu` and :math:`omega` can also be found in the CVXOPT
923 # ``x`` and ``y`` variables; however, they're stored
924 # conveniently as separate entries in the solution dictionary.
925 p1_value
= -soln_dict
['primal objective']
926 p2_value
= -soln_dict
['dual objective']
927 p1_optimal
= soln_dict
['x'][1:]
928 p2_optimal
= soln_dict
['z'][self
.dimension():]
930 # The "status" field contains "optimal" if everything went
931 # according to plan. Other possible values are "primal
932 # infeasible", "dual infeasible", "unknown", all of which mean
933 # we didn't get a solution. The "infeasible" ones are the
934 # worst, since they indicate that CVXOPT is convinced the
935 # problem is infeasible (and that cannot happen).
936 if soln_dict
['status'] in ['primal infeasible', 'dual infeasible']:
937 raise GameUnsolvableException(self
, soln_dict
)
938 elif soln_dict
['status'] == 'unknown':
939 # When we get a status of "unknown", we may still be able
940 # to salvage a solution out of the returned
941 # dictionary. Often this is the result of numerical
942 # difficulty and we can simply check that the primal/dual
943 # objectives match (within a tolerance) and that the
944 # primal/dual optimal solutions are within the cone (to a
945 # tolerance as well).
947 # The fudge factor of two is basically unjustified, but
948 # makes intuitive sense when you imagine that the primal
949 # value could be under the true optimal by ``ABS_TOL``
950 # and the dual value could be over by the same amount.
952 if abs(p1_value
- p2_value
) > tolerance
:
953 raise GameUnsolvableException(self
, soln_dict
)
954 if (p1_optimal
not in self
._K
) or (p2_optimal
not in self
._K
):
955 raise GameUnsolvableException(self
, soln_dict
)
957 return Solution(p1_value
, p1_optimal
, p2_optimal
)
962 Solve this linear game and return a :class:`Solution`.
968 A :class:`Solution` object describing the game's value and
969 the optimal strategies of both players.
973 GameUnsolvableException
974 If the game could not be solved (if an optimal solution to its
975 associated cone program was not found).
978 If the game could not be solved because CVXOPT crashed while
979 trying to take the square root of a negative number.
984 This example is computed in Gowda and Ravindran in the section
985 "The value of a Z-transformation"::
987 >>> from dunshire import *
988 >>> K = NonnegativeOrthant(3)
989 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
992 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
993 >>> print(SLG.solution())
994 Game value: -6.1724138
1004 The value of the following game can be computed using the fact
1005 that the identity is invertible::
1007 >>> from dunshire import *
1008 >>> K = NonnegativeOrthant(3)
1009 >>> L = [[1,0,0],[0,1,0],[0,0,1]]
1012 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
1013 >>> print(SLG.solution())
1014 Game value: 0.0312500
1026 # First try with a stricter tolerance. Who knows, it might
1027 # work. If it does, we prefer that solution.
1028 return self
._try
_solution
(options
.ABS_TOL
/ 10)
1030 except (PoorScalingException
, GameUnsolvableException
):
1031 # Ok, that didn't work. Let's try it with the default tolerance..
1033 return self
._try
_solution
(options
.ABS_TOL
/ 10)
1034 except (PoorScalingException
, GameUnsolvableException
) as error
:
1035 # Well, that didn't work either. Let's verbosify the matrix
1036 # output format before we allow the exception to be raised.
1037 printing
.options
['dformat'] = options
.DEBUG_FLOAT_FORMAT
1041 def condition(self
):
1043 Return the condition number of this game.
1045 In the CVXOPT construction of this game, two matrices ``G`` and
1046 ``A`` appear. When those matrices are nasty, numerical problems
1047 can show up. We define the condition number of this game to be
1048 the average of the condition numbers of ``G`` and ``A`` in the
1049 CVXOPT construction. If the condition number of this game is
1050 high, then you can expect numerical difficulty (such as
1051 :class:`PoorScalingException`).
1057 A real number greater than or equal to one that measures how
1058 bad this game is numerically.
1063 >>> from dunshire import *
1064 >>> K = NonnegativeOrthant(1)
1068 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
1069 >>> actual = SLG.condition()
1070 >>> expected = 1.8090169943749477
1071 >>> abs(actual - expected) < options.ABS_TOL
1075 return (condition_number(self
._G
()) + condition_number(self
._A
()))/2
1080 Return the dual game to this game.
1082 If :math:`G = \left(L,K,e_{1},e_{2}\right)` is a linear game,
1083 then its dual is :math:`G^{*} =
1084 \left(L^{*},K^{*},e_{2},e_{1}\right)`. However, since this cone
1085 is symmetric, :math:`K^{*} = K`.
1090 >>> from dunshire import *
1091 >>> K = NonnegativeOrthant(3)
1092 >>> L = [[1,-5,-15],[-1,2,-3],[-12,-15,1]]
1095 >>> SLG = SymmetricLinearGame(L, K, e1, e2)
1096 >>> print(SLG.dual())
1097 The linear game (L, K, e1, e2) where
1101 K = Nonnegative orthant in the real 3-space,
1108 Condition((L, K, e1, e2)) = 44.476...
1111 # We pass ``self._L`` right back into the constructor, because
1112 # it will be transposed there. And keep in mind that ``self._K``
1114 return SymmetricLinearGame(self
._L,