]> gitweb.michael.orlitzky.com - dunshire.git/commitdiff
Clean up and document some of the new test code.
authorMichael Orlitzky <michael@orlitzky.com>
Tue, 1 Nov 2016 21:54:31 +0000 (17:54 -0400)
committerMichael Orlitzky <michael@orlitzky.com>
Tue, 1 Nov 2016 21:54:31 +0000 (17:54 -0400)
.pylintrc
dunshire/matrices.py
test/matrices_test.py
test/randomgen.py

index 16b1ca72a59736ceeb6a31a53835e9678fdf5cd4..782cd4a5de686d04a5e72635d6e3f48f675e10e5 100644 (file)
--- a/.pylintrc
+++ b/.pylintrc
@@ -20,7 +20,10 @@ bad-functions=
 # These are all names from the associated papers. It would be more
 # confusing to name them something else and then have to juggle them
 # in your head as you switch between the source code and the papers.
 # These are all names from the associated papers. It would be more
 # confusing to name them something else and then have to juggle them
 # in your head as you switch between the source code and the papers.
-good-names=a,b,c,D,e1,e2,h,A,C,G,K,_K,L,_L,indented_L,M
+good-names=a,b,c,D,e1,e2,h,A,C,G,H,K,_K,L,_L,indented_L,M
+
+# Regular expression matching correct method names
+method-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z_][a-z0-9_]{2,50})$
 
 [MISCELLANEOUS]
 # List of note tags to take in consideration, separated by a comma.
 
 [MISCELLANEOUS]
 # List of note tags to take in consideration, separated by a comma.
index bcf83778d62752436f7894b5a6fbad4cc3e1012e..29c5867aabe544f768a1b1ce3862f14c0a316712 100644 (file)
@@ -3,6 +3,7 @@ Utility functions for working with CVXOPT matrices (instances of the
 class:`cvxopt.base.matrix` class).
 """
 
 class:`cvxopt.base.matrix` class).
 """
 
+from copy import copy
 from math import sqrt
 from cvxopt import matrix
 from cvxopt.lapack import gees, gesdd, syevr
 from math import sqrt
 from cvxopt import matrix
 from cvxopt.lapack import gees, gesdd, syevr
@@ -142,7 +143,7 @@ def eigenvalues(symmat):
     eigs = matrix(0, (domain_dim, 1), tc='d')
 
     # Create a copy of ``symmat`` here because ``syevr`` clobbers it.
     eigs = matrix(0, (domain_dim, 1), tc='d')
 
     # Create a copy of ``symmat`` here because ``syevr`` clobbers it.
-    dummy = matrix(symmat, symmat.size)
+    dummy = copy(symmat)
     syevr(dummy, eigs)
     return list(eigs)
 
     syevr(dummy, eigs)
     return list(eigs)
 
index 195549f2e5aa0fb38ee34f0c4e85ccff9dac87a1..ce57c7dc32a845b38af4dbad75e362c9e1da9b6f 100644 (file)
 Unit tests for the functions in the ``matrices`` module.
 """
 
 Unit tests for the functions in the ``matrices`` module.
 """
 
+from copy import copy
 from unittest import TestCase
 
 from unittest import TestCase
 
-from cvxopt import matrix
-
 from dunshire.matrices import (append_col, append_row, condition_number,
                                eigenvalues, eigenvalues_re, identity,
                                inner_product, norm)
 from dunshire.options import ABS_TOL
 from dunshire.matrices import (append_col, append_row, condition_number,
                                eigenvalues, eigenvalues_re, identity,
                                inner_product, norm)
 from dunshire.options import ABS_TOL
-from .randomgen import random_matrix, random_natural, random_scalar
+from .randomgen import random_matrix, random_natural
+
 
 class AppendColTest(TestCase):
 
 class AppendColTest(TestCase):
+    """
+    Tests for the :func:`append_col` function.
+    """
 
 
-    def test_size_increases(self):
+    def test_new_dimensions(self):
         """
         """
-        If we append a column to a matrix, the result should be bigger
-        than the original matrix.
+        If we append one matrix to another side-by-side, then the result
+        should have the same number of rows as the two original
+        matrices. However, the number of their columns should add up to
+        the number of columns in the new combined matrix.
         """
         """
-        dims = random_natural()
-        mat1 = random_matrix(dims)
-        mat2 = random_matrix(dims)
+        rows = random_natural()
+        cols1 = random_natural()
+        cols2 = random_natural()
+        mat1 = random_matrix(rows, cols1)
+        mat2 = random_matrix(rows, cols2)
         bigmat = append_col(mat1, mat2)
         bigmat = append_col(mat1, mat2)
-        self.assertTrue(bigmat.size[0] >= mat1.size[0])
-        self.assertTrue(bigmat.size[1] >= mat1.size[1])
+        self.assertTrue(bigmat.size[0] == rows)
+        self.assertTrue(bigmat.size[1] == cols1+cols2)
 
 
 class AppendRowTest(TestCase):
 
 
 class AppendRowTest(TestCase):
+    """
+    Tests for the :func:`append_row` function.
+    """
 
 
-    def test_size_increases(self):
+    def test_new_dimensions(self):
         """
         """
-        If we append a row to a matrix, the result should be bigger
-        than the original matrix.
+        If we append one matrix to another top-to-bottom, then
+        the result should have the same number of columns as the two
+        original matrices. However, the number of their rows should add
+        up to the number of rows in the the new combined matrix.
         """
         """
-        dims = random_natural()
-        mat1 = random_matrix(dims)
-        mat2 = random_matrix(dims)
+        rows1 = random_natural()
+        rows2 = random_natural()
+        cols = random_natural()
+        mat1 = random_matrix(rows1, cols)
+        mat2 = random_matrix(rows2, cols)
         bigmat = append_row(mat1, mat2)
         bigmat = append_row(mat1, mat2)
-        self.assertTrue(bigmat.size[0] >= mat1.size[0])
-        self.assertTrue(bigmat.size[1] >= mat1.size[1])
+        self.assertTrue(bigmat.size[0] == rows1+rows2)
+        self.assertTrue(bigmat.size[1] == cols)
 
 
 class EigenvaluesTest(TestCase):
 
 
 class EigenvaluesTest(TestCase):
+    """
+    Tests for the :func:`eigenvalues` function.
+    """
 
 
-    def test_eigenvalues_input_not_clobbered(self):
+    def test_eigenvalues_input_untouched(self):
+        """
+        The eigenvalue functions provided by CVXOPT/LAPACK like to
+        overwrite the matrices that you pass into them as
+        arguments. This test makes sure that our :func:`eigenvalues`
+        function does not do the same.
+        """
         mat = random_matrix(random_natural())
         symmat = mat + mat.trans()
         mat = random_matrix(random_natural())
         symmat = mat + mat.trans()
-        symmat_copy = matrix(symmat, symmat.size)
-        eigs = eigenvalues(symmat)
+        symmat_copy = copy(symmat)
+        dummy = eigenvalues(symmat)
         self.assertTrue(norm(symmat - symmat_copy) < ABS_TOL)
 
         self.assertTrue(norm(symmat - symmat_copy) < ABS_TOL)
 
-    def test_eigenvalues_re_input_not_clobbered(self):
-        mat = random_matrix(random_natural())
-        mat_copy = matrix(mat, mat.size)
-        eigs = eigenvalues_re(mat)
-        self.assertTrue(norm(mat - mat_copy) < ABS_TOL)
-
-    def test_eigenvalues_of_symmetric_are_real(self):
+    def test_eigenvalues_of_symmat_are_real(self):
+        """
+        A real symmetric matrix has real eigenvalues, so if we start
+        with a symmetric matrix, then the two functions :func:`eigenvalues`
+        and :func:`eigenvalues_re` should agree on it.
+        """
         mat = random_matrix(random_natural())
         symmat = mat + mat.trans()
         eigs1 = sorted(eigenvalues(symmat))
         eigs2 = sorted(eigenvalues_re(symmat))
         mat = random_matrix(random_natural())
         symmat = mat + mat.trans()
         eigs1 = sorted(eigenvalues(symmat))
         eigs2 = sorted(eigenvalues_re(symmat))
-        diffs = [abs(e1-e2) for (e1,e2) in zip(eigs1,eigs2)]
+        diffs = [abs(e1 - e2) for (e1, e2) in zip(eigs1, eigs2)]
         self.assertTrue(all([diff < ABS_TOL for diff in diffs]))
 
         self.assertTrue(all([diff < ABS_TOL for diff in diffs]))
 
-
     def test_eigenvalues_of_identity(self):
     def test_eigenvalues_of_identity(self):
+        """
+        All eigenvalues of the identity matrix should be one.
+        """
         mat = identity(random_natural(), typecode='d')
         mat = identity(random_natural(), typecode='d')
-        eigs1 = eigenvalues(mat)
-        eigs2 = eigenvalues_re(mat)
-        self.assertTrue(all([abs(e1 - 1) < ABS_TOL for e1 in eigs1]))
-        self.assertTrue(all([abs(e2 - 1) < ABS_TOL for e2 in eigs2]))
+        eigs = eigenvalues(mat)
+        self.assertTrue(all([abs(ev - 1) < ABS_TOL for ev in eigs]))
+
+
+class EigenvaluesRealPartTest(TestCase):
+    """
+    Tests for the :func:`eigenvalues_re` function.
+    """
+
+    def test_eigenvalues_re_input_not_clobbered(self):
+        """
+        The eigenvalue functions provided by CVXOPT/LAPACK like to
+        overwrite the matrices that you pass into them as
+        arguments. This test makes sure that our :func:`eigenvalues_re`
+        function does not do the same.
+        """
+        mat = random_matrix(random_natural())
+        mat_copy = copy(mat)
+        dummy = eigenvalues_re(mat)
+        self.assertTrue(norm(mat - mat_copy) < ABS_TOL)
+
+    def test_eigenvalues_re_of_identity(self):
+        """
+        All eigenvalues of the identity matrix should be one.
+        """
+        mat = identity(random_natural(), typecode='d')
+        eigs = eigenvalues_re(mat)
+        self.assertTrue(all([abs(ev - 1) < ABS_TOL for ev in eigs]))
 
 
 class InnerProductTest(TestCase):
 
 
 class InnerProductTest(TestCase):
+    """
+    Tests for the :func:`inner_product` function.
+    """
+
+    def test_inner_product_with_self_is_norm_squared(self):
+        """
+        Ensure that the func:`inner_product` and :func:`norm` functions
+        are compatible by checking that the square of the norm of a
+        vector is its inner product with itself.
+        """
+        vec = random_matrix(random_natural(), 1)
+        actual = inner_product(vec, vec)
+        expected = norm(vec)**2
+        self.assertTrue(abs(actual - expected) < ABS_TOL)
+
+
+class NormTest(TestCase):
+    """
+    Tests for the :func:`norm` function.
+    """
 
     def test_norm_is_nonnegative(self):
 
     def test_norm_is_nonnegative(self):
-        vec = matrix([random_scalar() for _ in range(random_natural())])
-        self.assertTrue(inner_product(vec,vec) >= 0)
+        """
+        Test one of the properties of a norm, that it is nonnegative.
+        """
+        mat = random_matrix(random_natural(), random_natural())
+        self.assertTrue(norm(mat) >= 0)
 
 
 
 
-def ConditionNumberTest(TestCase):
+class ConditionNumberTest(TestCase):
+    """
+    Tests for the :func:`condition_number` function.
+    """
 
     def test_condition_number_ge_one(self):
 
     def test_condition_number_ge_one(self):
-        mat = random_matrix(random_natural())
+        """
+        From the way that it is defined, the condition number should
+        always be greater than or equal to one.
+        """
+        mat = random_matrix(random_natural(), random_natural())
         self.assertTrue(condition_number(mat) >= 1)
         self.assertTrue(condition_number(mat) >= 1)
index 6513440152d3e693618267d6b839faabaf8417f4..afe0dae25304b33e2f01e3fd2cb46ca824cc8771 100644 (file)
@@ -79,15 +79,19 @@ def random_natural():
     return randint(1, RANDOM_MAX)
 
 
     return randint(1, RANDOM_MAX)
 
 
-def random_matrix(dims):
+def random_matrix(row_count, column_count=None):
     """
     """
-    Generate a random square matrix.
+    Generate a random matrix.
 
     Parameters
     ----------
 
 
     Parameters
     ----------
 
-    dims : int
-        The number of rows/columns you want in the returned matrix.
+    row_count : int
+        The number of rows you want in the returned matrix.
+
+    column_count: int
+        The number of columns you want in the returned matrix (default:
+        the same as ``row_count``).
 
     Returns
     -------
 
     Returns
     -------
@@ -103,21 +107,31 @@ def random_matrix(dims):
         >>> A.size
         (3, 3)
 
         >>> A.size
         (3, 3)
 
+        >>> A = random_matrix(3,2)
+        >>> A.size
+        (3, 2)
+
     """
     """
-    return matrix([[random_scalar()
-                    for _ in range(dims)]
-                   for _ in range(dims)])
+    if column_count is None:
+        column_count = row_count
+
+    entries = [random_scalar() for _ in range(row_count*column_count)]
+    return matrix(entries, (row_count, column_count))
 
 
 
 
-def random_nonnegative_matrix(dims):
+def random_nonnegative_matrix(row_count, column_count=None):
     """
     """
-    Generate a random square matrix with nonnegative entries.
+    Generate a random matrix with nonnegative entries.
 
     Parameters
     ----------
 
 
     Parameters
     ----------
 
-    dims : int
-        The number of rows/columns you want in the returned matrix.
+    row_count : int
+        The number of rows you want in the returned matrix.
+
+    column_count : int
+        The number of columns you want in the returned matrix (default:
+        the same as ``row_count``).
 
     Returns
     -------
 
     Returns
     -------
@@ -134,10 +148,18 @@ def random_nonnegative_matrix(dims):
         >>> all([entry >= 0 for entry in A])
         True
 
         >>> all([entry >= 0 for entry in A])
         True
 
+        >>> A = random_nonnegative_matrix(3,2)
+        >>> A.size
+        (3, 2)
+        >>> all([entry >= 0 for entry in A])
+        True
+
     """
     """
-    return matrix([[random_nn_scalar()
-                    for _ in range(dims)]
-                   for _ in range(dims)])
+    if column_count is None:
+        column_count = row_count
+
+    entries = [random_nn_scalar() for _ in range(row_count*column_count)]
+    return matrix(entries, (row_count, column_count))
 
 
 def random_diagonal_matrix(dims):
 
 
 def random_diagonal_matrix(dims):
@@ -280,7 +302,7 @@ def random_orthant_game():
     corresponding :class:`SymmetricLinearGame`.
 
     We keep going until we generate a game with a condition number under
     corresponding :class:`SymmetricLinearGame`.
 
     We keep going until we generate a game with a condition number under
-    5000.
+    MAX_COND.
     """
     ambient_dim = random_natural() + 1
     K = NonnegativeOrthant(ambient_dim)
     """
     ambient_dim = random_natural() + 1
     K = NonnegativeOrthant(ambient_dim)
@@ -300,6 +322,9 @@ def random_icecream_game():
     Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
     random game over the ice-cream cone, and return the corresponding
     :class:`SymmetricLinearGame`.
     Generate the ``L``, ``K``, ``e1``, and ``e2`` parameters for a
     random game over the ice-cream cone, and return the corresponding
     :class:`SymmetricLinearGame`.
+
+    We keep going until we generate a game with a condition number under
+    MAX_COND.
     """
     # Use a minimum dimension of two to avoid divide-by-zero in
     # the fudge factor we make up later.
     """
     # Use a minimum dimension of two to avoid divide-by-zero in
     # the fudge factor we make up later.
@@ -330,6 +355,12 @@ def random_icecream_game():
 def random_ll_orthant_game():
     """
     Return a random Lyapunov game over some nonnegative orthant.
 def random_ll_orthant_game():
     """
     Return a random Lyapunov game over some nonnegative orthant.
+
+    We first construct a :func:`random_orthant_game` and then modify it
+    to have a :func:`random_diagonal_matrix` as its operator. Such
+    things are Lyapunov-like on the nonnegative orthant. That process is
+    repeated until the condition number of the resulting game is within
+    ``MAX_COND``.
     """
     G = random_orthant_game()
     L = random_diagonal_matrix(G._K.dimension())
     """
     G = random_orthant_game()
     L = random_diagonal_matrix(G._K.dimension())
@@ -349,6 +380,11 @@ def random_ll_orthant_game():
 def random_ll_icecream_game():
     """
     Return a random Lyapunov game over some ice-cream cone.
 def random_ll_icecream_game():
     """
     Return a random Lyapunov game over some ice-cream cone.
+
+    We first construct a :func:`random_icecream_game` and then modify it
+    to have a :func:`random_lyapunov_like_icecream` operator. That
+    process is repeated until the condition number of the resulting game
+    is within ``MAX_COND``.
     """
     G = random_icecream_game()
     L = random_lyapunov_like_icecream(G._K.dimension())
     """
     G = random_icecream_game()
     L = random_lyapunov_like_icecream(G._K.dimension())
@@ -366,6 +402,16 @@ def random_ll_icecream_game():
 
 
 def random_positive_orthant_game():
 
 
 def random_positive_orthant_game():
+    """
+    Return a random game over the nonnegative orthant with a positive
+    operator.
+
+    We first construct a :func:`random_orthant_game` and then modify it
+    to have a :func:`random_nonnegative_matrix` as its operator. That
+    process is repeated until the condition number of the resulting game
+    is within ``MAX_COND``.
+    """
+
     G = random_orthant_game()
     L = random_nonnegative_matrix(G._K.dimension())
 
     G = random_orthant_game()
     L = random_nonnegative_matrix(G._K.dimension())
 
@@ -382,6 +428,21 @@ def random_positive_orthant_game():
 
 
 def random_nn_scaling(G):
 
 
 def random_nn_scaling(G):
+    """
+    Scale the given game by a random nonnegative amount.
+
+    Parameters
+    ----------
+
+    G : :class:`SymmetricLinearGame`
+        The game that you would like to scale.
+
+    Returns
+    -------
+    (float, :class:`SymmetricLinearGame`)
+        A pair containing the both the scaling factor and the new scaled game.
+
+    """
     alpha = random_nn_scalar()
     H = SymmetricLinearGame(alpha*G._L.trans(), G._K, G._e1, G._e2)
 
     alpha = random_nn_scalar()
     H = SymmetricLinearGame(alpha*G._L.trans(), G._K, G._e1, G._e2)
 
@@ -392,7 +453,24 @@ def random_nn_scaling(G):
 
     return (alpha, H)
 
 
     return (alpha, H)
 
+
 def random_translation(G):
 def random_translation(G):
+    """
+    Translate the given game by a random amount.
+
+    Parameters
+    ----------
+
+    G : :class:`SymmetricLinearGame`
+        The game that you would like to translate.
+
+    Returns
+    -------
+    (float, :class:`SymmetricLinearGame`)
+        A pair containing the both the translation distance and the new
+        scaled game.
+
+    """
     alpha = random_scalar()
     tensor_prod = G._e1 * G._e2.trans()
     M = G._L + alpha*tensor_prod
     alpha = random_scalar()
     tensor_prod = G._e1 * G._e2.trans()
     M = G._L + alpha*tensor_prod