]> gitweb.michael.orlitzky.com - dunshire.git/commitdiff
Add setup.py and reorganize everything to make its "test" command happy.
authorMichael Orlitzky <michael@orlitzky.com>
Wed, 26 Oct 2016 18:05:20 +0000 (14:05 -0400)
committerMichael Orlitzky <michael@orlitzky.com>
Wed, 26 Oct 2016 18:05:20 +0000 (14:05 -0400)
12 files changed:
dunshire/__init__.py [moved from src/dunshire/__init__.py with 100% similarity]
dunshire/cones.py [moved from src/dunshire/cones.py with 100% similarity]
dunshire/errors.py [moved from src/dunshire/errors.py with 100% similarity]
dunshire/games.py [moved from src/dunshire/games.py with 100% similarity]
dunshire/matrices.py [moved from src/dunshire/matrices.py with 100% similarity]
dunshire/options.py [moved from src/dunshire/options.py with 100% similarity]
makefile
setup.py [new file with mode: 0644]
src/test/__init__.py [deleted file]
src/test/symmetric_linear_game_test.py [deleted file]
test/__init__.py [moved from src/test/suite.py with 73% similarity]
test/__main__.py [new file with mode: 0644]

similarity index 100%
rename from src/dunshire/cones.py
rename to dunshire/cones.py
similarity index 100%
rename from src/dunshire/errors.py
rename to dunshire/errors.py
similarity index 100%
rename from src/dunshire/games.py
rename to dunshire/games.py
similarity index 100%
rename from src/dunshire/options.py
rename to dunshire/options.py
index 7fe104e2140476613e6a08c00c97fc5304e7ed6a..0cd51823d36ae7faabae243778b6d66319fc7a35 100644 (file)
--- a/makefile
+++ b/makefile
@@ -1,5 +1,5 @@
 PN := dunshire
 PN := dunshire
-SRCS := src/$(PN)/*.py src/test/*.py
+SRCS := $(PN)/*.py test/*.py
 
 doc: $(SRCS) doc/source/conf.py doc/makefile
        cd doc && $(MAKE) html
 
 doc: $(SRCS) doc/source/conf.py doc/makefile
        cd doc && $(MAKE) html
@@ -10,7 +10,7 @@ doctest:
 
 .PHONY: check
 check:
 
 .PHONY: check
 check:
-       PYTHONPATH=src python src/test/suite.py
+       PYTHONPATH="." python test/
 
 .PHONY: lint
 lint:
 
 .PHONY: lint
 lint:
@@ -18,4 +18,5 @@ lint:
 
 .PHONY: clean
 clean:
 
 .PHONY: clean
 clean:
-       rm -rf src/$(PN)/__pycache__ src/test/__pycache__ doc/build
+       rm -rf $(PN)/__pycache__ test/__pycache__ doc/build
+       rm -rf $(PN).egg-info
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..8e81428
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,14 @@
+from setuptools import setup
+
+setup(
+    name         = 'dunshire',
+    version      = '0.0.1',
+    author       = 'Michael Orlitzky',
+    author_email = 'michael@orlitzky.com',
+    url          = 'http://michael.orlitzky.com/code/dunshire.php',
+    packages     = ['dunshire'],
+    description  = 'A library for solving linear (cone) games',
+    license      = 'doc/LICENSE',
+    install_requires = [ 'cvxopt >= 1.1.8' ],
+    test_suite = 'test.build_suite'
+)
diff --git a/src/test/__init__.py b/src/test/__init__.py
deleted file mode 100644 (file)
index c5fbe98..0000000
+++ /dev/null
@@ -1 +0,0 @@
-# <3 git.
diff --git a/src/test/symmetric_linear_game_test.py b/src/test/symmetric_linear_game_test.py
deleted file mode 100644 (file)
index 647d013..0000000
+++ /dev/null
@@ -1,528 +0,0 @@
-"""
-Unit tests for the :class:`SymmetricLinearGame` class.
-"""
-
-from math import sqrt
-from random import randint, uniform
-from unittest import TestCase
-
-from cvxopt import matrix
-from dunshire.cones import NonnegativeOrthant, IceCream
-from dunshire.games import SymmetricLinearGame
-from dunshire.matrices import (append_col, append_row, eigenvalues_re,
-                               identity, inner_product)
-from dunshire import options
-
-
-def random_matrix(dims):
-    """
-    Generate a random square matrix.
-
-    Parameters
-    ----------
-
-    dims : int
-        The number of rows/columns you want in the returned matrix.
-
-    Returns
-    -------
-
-    matrix
-        A new matrix whose entries are random floats chosen uniformly from
-        the interval [-10, 10].
-
-    Examples
-    --------
-
-        >>> A = random_matrix(3)
-        >>> A.size
-        (3, 3)
-
-    """
-    return matrix([[uniform(-10, 10) for i in range(dims)]
-                   for j in range(dims)])
-
-
-def random_nonnegative_matrix(dims):
-    """
-    Generate a random square matrix with nonnegative entries.
-
-    Parameters
-    ----------
-
-    dims : int
-        The number of rows/columns you want in the returned matrix.
-
-    Returns
-    -------
-
-    matrix
-        A new matrix whose entries are random floats chosen uniformly from
-        the interval [0, 10].
-
-    Examples
-    --------
-
-        >>> A = random_nonnegative_matrix(3)
-        >>> A.size
-        (3, 3)
-        >>> all([entry >= 0 for entry in A])
-        True
-
-    """
-    L = random_matrix(dims)
-    return matrix([abs(entry) for entry in L], (dims, dims))
-
-
-def random_diagonal_matrix(dims):
-    """
-    Generate a random square matrix with zero off-diagonal entries.
-
-    These matrices are Lyapunov-like on the nonnegative orthant, as is
-    fairly easy to see.
-
-    Parameters
-    ----------
-
-    dims : int
-        The number of rows/columns you want in the returned matrix.
-
-    Returns
-    -------
-
-    matrix
-        A new matrix whose diagonal entries are random floats chosen
-        uniformly from the interval [-10, 10] and whose off-diagonal
-        entries are zero.
-
-    Examples
-    --------
-
-        >>> A = random_diagonal_matrix(3)
-        >>> A.size
-        (3, 3)
-        >>> A[0,1] == A[0,2] == A[1,0] == A[2,0] == A[1,2] == A[2,1] == 0
-        True
-
-    """
-    return matrix([[uniform(-10, 10)*int(i == j) for i in range(dims)]
-                   for j in range(dims)])
-
-
-def random_skew_symmetric_matrix(dims):
-    """
-    Generate a random skew-symmetrix matrix.
-
-    Parameters
-    ----------
-
-    dims : int
-        The number of rows/columns you want in the returned matrix.
-
-    Returns
-    -------
-
-    matrix
-        A new skew-matrix whose strictly above-diagonal entries are
-        random floats chosen uniformly from the interval [-10, 10].
-
-    Examples
-    --------
-
-        >>> A = random_skew_symmetric_matrix(3)
-        >>> A.size
-        (3, 3)
-
-        >>> from dunshire.matrices import norm
-        >>> A = random_skew_symmetric_matrix(randint(1, 10))
-        >>> norm(A + A.trans()) < options.ABS_TOL
-        True
-
-    """
-    strict_ut = [[uniform(-10, 10)*int(i < j) for i in range(dims)]
-                 for j in range(dims)]
-
-    strict_ut = matrix(strict_ut, (dims, dims))
-    return strict_ut - strict_ut.trans()
-
-
-def random_lyapunov_like_icecream(dims):
-    r"""
-    Generate a random matrix Lyapunov-like on the ice-cream cone.
-
-    The form of these matrices is cited in Gowda and Tao
-    [GowdaTao]_. The scalar ``a`` and the vector ``b`` (using their
-    notation) are easy to generate. The submatrix ``D`` is a little
-    trickier, but it can be found noticing that :math:`C + C^{T} = 0`
-    for a skew-symmetric matrix :math:`C` implying that :math:`C + C^{T}
-    + \left(2a\right)I = \left(2a\right)I`. Thus we can stick an
-    :math:`aI` with each of :math:`C,C^{T}` and let those be our
-    :math:`D,D^{T}`.
-
-    Parameters
-    ----------
-
-    dims : int
-        The dimension of the ice-cream cone (not of the matrix you want!)
-        on which the returned matrix should be Lyapunov-like.
-
-    Returns
-    -------
-
-    matrix
-        A new matrix, Lyapunov-like on the ice-cream cone in ``dims``
-        dimensions, whose free entries are random floats chosen uniformly
-        from the interval [-10, 10].
-
-    References
-    ----------
-
-    .. [GowdaTao] M. S. Gowda and J. Tao. On the bilinearity rank of a
-       proper cone and Lyapunov-like transformations. Mathematical
-       Programming, 147:155–170, 2014.
-
-    Examples
-    --------
-
-        >>> L = random_lyapunov_like_icecream(3)
-        >>> L.size
-        (3, 3)
-        >>> x = matrix([1,1,0])
-        >>> s = matrix([1,-1,0])
-        >>> abs(inner_product(L*x, s)) < options.ABS_TOL
-        True
-
-    """
-    a = matrix([uniform(-10, 10)], (1, 1))
-    b = matrix([uniform(-10, 10) for idx in range(dims-1)], (dims-1, 1))
-    D = random_skew_symmetric_matrix(dims-1) + a*identity(dims-1)
-    row1 = append_col(a, b.trans())
-    row2 = append_col(b, D)
-    return append_row(row1, row2)
-
-
-def random_orthant_params():
-    """
-    Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
-    random game over the nonnegative orthant.
-    """
-    ambient_dim = randint(1, 10)
-    K = NonnegativeOrthant(ambient_dim)
-    e1 = [uniform(0.5, 10) for idx in range(K.dimension())]
-    e2 = [uniform(0.5, 10) for idx in range(K.dimension())]
-    L = random_matrix(K.dimension())
-    return (L, K, matrix(e1), matrix(e2))
-
-
-def random_icecream_params():
-    """
-    Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
-    random game over the ice-cream cone.
-    """
-    # Use a minimum dimension of two to avoid divide-by-zero in
-    # the fudge factor we make up later.
-    ambient_dim = randint(2, 10)
-    K = IceCream(ambient_dim)
-    e1 = [1] # Set the "height" of e1 to one
-    e2 = [1] # And the same for e2
-
-    # If we choose the rest of the components of e1,e2 randomly
-    # between 0 and 1, then the largest the squared norm of the
-    # non-height part of e1,e2 could be is the 1*(dim(K) - 1). We
-    # need to make it less than one (the height of the cone) so
-    # that the whole thing is in the cone. The norm of the
-    # non-height part is sqrt(dim(K) - 1), and we can divide by
-    # twice that.
-    fudge_factor = 1.0 / (2.0*sqrt(K.dimension() - 1.0))
-    e1 += [fudge_factor*uniform(0, 1) for idx in range(K.dimension() - 1)]
-    e2 += [fudge_factor*uniform(0, 1) for idx in range(K.dimension() - 1)]
-    L = random_matrix(K.dimension())
-
-    return (L, K, matrix(e1), matrix(e2))
-
-
-# Tell pylint to shut up about the large number of methods.
-class SymmetricLinearGameTest(TestCase): # pylint: disable=R0904
-    """
-    Tests for the SymmetricLinearGame and Solution classes.
-    """
-    def assert_within_tol(self, first, second):
-        """
-        Test that ``first`` and ``second`` are equal within our default
-        tolerance.
-        """
-        self.assertTrue(abs(first - second) < options.ABS_TOL)
-
-
-    def assert_solution_exists(self, L, K, e1, e2):
-        """
-        Given the parameters needed to construct a SymmetricLinearGame,
-        ensure that that game has a solution.
-        """
-        # The matrix() constructor assumes that ``L`` is a list of
-        # columns, so we transpose it to agree with what
-        # SymmetricLinearGame() thinks.
-        G = SymmetricLinearGame(L.trans(), K, e1, e2)
-        soln = G.solution()
-
-        expected = inner_product(L*soln.player1_optimal(),
-                                 soln.player2_optimal())
-        self.assert_within_tol(soln.game_value(), expected)
-
-
-    def test_solution_exists_orthant(self):
-        """
-        Every linear game has a solution, so we should be able to solve
-        every symmetric linear game over the NonnegativeOrthant. Pick
-        some parameters randomly and give it a shot. The resulting
-        optimal solutions should give us the optimal game value when we
-        apply the payoff operator to them.
-        """
-        (L, K, e1, e2) = random_orthant_params()
-        self.assert_solution_exists(L, K, e1, e2)
-
-
-    def test_solution_exists_icecream(self):
-        """
-        Like :meth:`test_solution_exists_nonnegative_orthant`, except
-        over the ice cream cone.
-        """
-        (L, K, e1, e2) = random_icecream_params()
-        self.assert_solution_exists(L, K, e1, e2)
-
-
-    def test_negative_value_z_operator(self):
-        """
-        Test the example given in Gowda/Ravindran of a Z-matrix with
-        negative game value on the nonnegative orthant.
-        """
-        K = NonnegativeOrthant(2)
-        e1 = [1, 1]
-        e2 = e1
-        L = [[1, -2], [-2, 1]]
-        G = SymmetricLinearGame(L, K, e1, e2)
-        self.assertTrue(G.solution().game_value() < -options.ABS_TOL)
-
-
-    def assert_scaling_works(self, L, K, e1, e2):
-        """
-        Test that scaling ``L`` by a nonnegative number scales the value
-        of the game by the same number.
-        """
-        game1 = SymmetricLinearGame(L, K, e1, e2)
-        value1 = game1.solution().game_value()
-
-        alpha = uniform(0.1, 10)
-        game2 = SymmetricLinearGame(alpha*L, K, e1, e2)
-        value2 = game2.solution().game_value()
-        self.assert_within_tol(alpha*value1, value2)
-
-
-    def test_scaling_orthant(self):
-        """
-        Test that scaling ``L`` by a nonnegative number scales the value
-        of the game by the same number over the nonnegative orthant.
-        """
-        (L, K, e1, e2) = random_orthant_params()
-        self.assert_scaling_works(L, K, e1, e2)
-
-
-    def test_scaling_icecream(self):
-        """
-        The same test as :meth:`test_nonnegative_scaling_orthant`,
-        except over the ice cream cone.
-        """
-        (L, K, e1, e2) = random_icecream_params()
-        self.assert_scaling_works(L, K, e1, e2)
-
-
-    def assert_translation_works(self, L, K, e1, e2):
-        """
-        Check that translating ``L`` by alpha*(e1*e2.trans()) increases
-        the value of the associated game by alpha.
-        """
-        # We need to use ``L`` later, so make sure we transpose it
-        # before passing it in as a column-indexed matrix.
-        game1 = SymmetricLinearGame(L.trans(), K, e1, e2)
-        soln1 = game1.solution()
-        value1 = soln1.game_value()
-        x_bar = soln1.player1_optimal()
-        y_bar = soln1.player2_optimal()
-
-        alpha = uniform(-10, 10)
-        tensor_prod = e1*e2.trans()
-
-        # This is the "correct" representation of ``M``, but COLUMN
-        # indexed...
-        M = L + alpha*tensor_prod
-
-        # so we have to transpose it when we feed it to the constructor.
-        game2 = SymmetricLinearGame(M.trans(), K, e1, e2)
-        value2 = game2.solution().game_value()
-
-        self.assert_within_tol(value1 + alpha, value2)
-
-        # Make sure the same optimal pair works.
-        self.assert_within_tol(value2, inner_product(M*x_bar, y_bar))
-
-
-    def test_translation_orthant(self):
-        """
-        Test that translation works over the nonnegative orthant.
-        """
-        (L, K, e1, e2) = random_orthant_params()
-        self.assert_translation_works(L, K, e1, e2)
-
-
-    def test_translation_icecream(self):
-        """
-        The same as :meth:`test_translation_orthant`, except over the
-        ice cream cone.
-        """
-        (L, K, e1, e2) = random_icecream_params()
-        self.assert_translation_works(L, K, e1, e2)
-
-
-    def assert_opposite_game_works(self, L, K, e1, e2):
-        """
-        Check the value of the "opposite" game that gives rise to a
-        value that is the negation of the original game. Comes from
-        some corollary.
-        """
-        # We need to use ``L`` later, so make sure we transpose it
-        # before passing it in as a column-indexed matrix.
-        game1 = SymmetricLinearGame(L.trans(), K, e1, e2)
-
-        # This is the "correct" representation of ``M``, but
-        # COLUMN indexed...
-        M = -L.trans()
-
-        # so we have to transpose it when we feed it to the constructor.
-        game2 = SymmetricLinearGame(M.trans(), K, e2, e1)
-
-        soln1 = game1.solution()
-        x_bar = soln1.player1_optimal()
-        y_bar = soln1.player2_optimal()
-        soln2 = game2.solution()
-
-        self.assert_within_tol(-soln1.game_value(), soln2.game_value())
-
-        # Make sure the switched optimal pair works.
-        self.assert_within_tol(soln2.game_value(),
-                               inner_product(M*y_bar, x_bar))
-
-
-    def test_opposite_game_orthant(self):
-        """
-        Test the value of the "opposite" game over the nonnegative
-        orthant.
-        """
-        (L, K, e1, e2) = random_orthant_params()
-        self.assert_opposite_game_works(L, K, e1, e2)
-
-
-    def test_opposite_game_icecream(self):
-        """
-        Like :meth:`test_opposite_game_orthant`, except over the
-        ice-cream cone.
-        """
-        (L, K, e1, e2) = random_icecream_params()
-        self.assert_opposite_game_works(L, K, e1, e2)
-
-
-    def assert_orthogonality(self, L, K, e1, e2):
-        """
-        Two orthogonality relations hold at an optimal solution, and we
-        check them here.
-        """
-        # We need to use ``L`` later, so make sure we transpose it
-        # before passing it in as a column-indexed matrix.
-        game = SymmetricLinearGame(L.trans(), K, e1, e2)
-        soln = game.solution()
-        x_bar = soln.player1_optimal()
-        y_bar = soln.player2_optimal()
-        value = soln.game_value()
-
-        ip1 = inner_product(y_bar, L*x_bar - value*e1)
-        self.assert_within_tol(ip1, 0)
-
-        ip2 = inner_product(value*e2 - L.trans()*y_bar, x_bar)
-        self.assert_within_tol(ip2, 0)
-
-
-    def test_orthogonality_orthant(self):
-        """
-        Check the orthgonality relationships that hold for a solution
-        over the nonnegative orthant.
-        """
-        (L, K, e1, e2) = random_orthant_params()
-        self.assert_orthogonality(L, K, e1, e2)
-
-
-    def test_orthogonality_icecream(self):
-        """
-        Check the orthgonality relationships that hold for a solution
-        over the ice-cream cone.
-        """
-        (L, K, e1, e2) = random_icecream_params()
-        self.assert_orthogonality(L, K, e1, e2)
-
-
-    def test_positive_operator_value(self):
-        """
-        Test that a positive operator on the nonnegative orthant gives
-        rise to a a game with a nonnegative value.
-
-        This test theoretically applies to the ice-cream cone as well,
-        but we don't know how to make positive operators on that cone.
-        """
-        (K, e1, e2) = random_orthant_params()[1:]
-        L = random_nonnegative_matrix(K.dimension())
-
-        game = SymmetricLinearGame(L, K, e1, e2)
-        self.assertTrue(game.solution().game_value() >= -options.ABS_TOL)
-
-
-    def assert_lyapunov_works(self, L, K, e1, e2):
-        """
-        Check that Lyapunov games act the way we expect.
-        """
-        game = SymmetricLinearGame(L, K, e1, e2)
-        soln = game.solution()
-
-        # We only check for positive/negative stability if the game
-        # value is not basically zero. If the value is that close to
-        # zero, we just won't check any assertions.
-        eigs = eigenvalues_re(L)
-        if soln.game_value() > options.ABS_TOL:
-            # L should be positive stable
-            positive_stable = all([eig > -options.ABS_TOL for eig in eigs])
-            self.assertTrue(positive_stable)
-        elif soln.game_value() < -options.ABS_TOL:
-            # L should be negative stable
-            negative_stable = all([eig < options.ABS_TOL for eig in eigs])
-            self.assertTrue(negative_stable)
-
-        # The dual game's value should always equal the primal's.
-        dualsoln = game.dual().solution()
-        self.assert_within_tol(dualsoln.game_value(), soln.game_value())
-
-
-    def test_lyapunov_orthant(self):
-        """
-        Test that a Lyapunov game on the nonnegative orthant works.
-        """
-        (K, e1, e2) = random_orthant_params()[1:]
-        L = random_diagonal_matrix(K.dimension())
-
-        self.assert_lyapunov_works(L, K, e1, e2)
-
-
-    def test_lyapunov_icecream(self):
-        """
-        Test that a Lyapunov game on the ice-cream cone works.
-        """
-        (K, e1, e2) = random_icecream_params()[1:]
-        L = random_lyapunov_like_icecream(K.dimension())
-
-        self.assert_lyapunov_works(L, K, e1, e2)
similarity index 73%
rename from src/test/suite.py
rename to test/__init__.py
index e10c38d98a3401a2f0a37fa033f77d3e2270863a..6942bf2f2af83c8749bae65c586f685ec5b95e08 100644 (file)
@@ -2,7 +2,9 @@
 The whole test suite.
 
 This module compiles the doctests and unittests from the rest of the
 The whole test suite.
 
 This module compiles the doctests and unittests from the rest of the
-codebase into one big TestSuite() and the runs it.
+codebase into one big TestSuite() and the runs it. It also provides a
+function :func:`build_suite` that merely builds the suite; the result
+can be used by setuptools.
 """
 
 from unittest import TestLoader, TestSuite, TextTestRunner
 """
 
 from unittest import TestLoader, TestSuite, TextTestRunner
@@ -14,10 +16,9 @@ from dunshire import matrices
 from dunshire import games
 from test import symmetric_linear_game_test
 
 from dunshire import games
 from test import symmetric_linear_game_test
 
-def run_suite():
+def build_suite():
     """
     """
-    Run all of the unit and doctests for the ``dunshire`` and ``test``
-    packages.
+    Build our test suite, separately from running it.
     """
     suite = TestSuite()
     suite.addTest(DocTestSuite(cones))
     """
     suite = TestSuite()
     suite.addTest(DocTestSuite(cones))
@@ -27,8 +28,12 @@ def run_suite():
     suite.addTest(DocTestSuite(symmetric_linear_game_test))
     slg_tests = TestLoader().loadTestsFromModule(symmetric_linear_game_test)
     suite.addTest(slg_tests)
     suite.addTest(DocTestSuite(symmetric_linear_game_test))
     slg_tests = TestLoader().loadTestsFromModule(symmetric_linear_game_test)
     suite.addTest(slg_tests)
-    runner = TextTestRunner(verbosity=1)
-    runner.run(suite)
+    return suite
 
 
-if __name__ == '__main__':
-    run_suite()
+def run_suite(s):
+    """
+    Run all of the unit and doctests for the ``dunshire`` and ``test``
+    packages.
+    """
+    runner = TextTestRunner(verbosity=1)
+    runner.run(s)
diff --git a/test/__main__.py b/test/__main__.py
new file mode 100644 (file)
index 0000000..175c884
--- /dev/null
@@ -0,0 +1,3 @@
+from test import build_suite, run_suite
+
+run_suite(build_suite())