]> 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.
-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.
index bcf83778d62752436f7894b5a6fbad4cc3e1012e..29c5867aabe544f768a1b1ce3862f14c0a316712 100644 (file)
@@ -3,6 +3,7 @@ Utility functions for working with CVXOPT matrices (instances of the
 class:`cvxopt.base.matrix` class).
 """
 
+from copy import copy
 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.
-    dummy = matrix(symmat, symmat.size)
+    dummy = copy(symmat)
     syevr(dummy, eigs)
     return list(eigs)
 
index 195549f2e5aa0fb38ee34f0c4e85ccff9dac87a1..ce57c7dc32a845b38af4dbad75e362c9e1da9b6f 100644 (file)
 Unit tests for the functions in the ``matrices`` module.
 """
 
+from copy import copy
 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 .randomgen import random_matrix, random_natural, random_scalar
+from .randomgen import random_matrix, random_natural
+
 
 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)
-        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):
+    """
+    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)
-        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):
+    """
+    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()
-        symmat_copy = matrix(symmat, symmat.size)
-        eigs = eigenvalues(symmat)
+        symmat_copy = copy(symmat)
+        dummy = eigenvalues(symmat)
         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))
-        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]))
 
-
     def test_eigenvalues_of_identity(self):
+        """
+        All eigenvalues of the identity matrix should be one.
+        """
         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):
+    """
+    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):
-        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):
-        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)
index 6513440152d3e693618267d6b839faabaf8417f4..afe0dae25304b33e2f01e3fd2cb46ca824cc8771 100644 (file)
@@ -79,15 +79,19 @@ def random_natural():
     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
     ----------
 
-    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
     -------
@@ -103,21 +107,31 @@ def random_matrix(dims):
         >>> 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
     ----------
 
-    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
     -------
@@ -134,10 +148,18 @@ def random_nonnegative_matrix(dims):
         >>> 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):
@@ -280,7 +302,7 @@ def random_orthant_game():
     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)
@@ -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`.
+
+    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.
@@ -330,6 +355,12 @@ def random_icecream_game():
 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())
@@ -349,6 +380,11 @@ def random_ll_orthant_game():
 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())
@@ -366,6 +402,16 @@ def random_ll_icecream_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())
 
@@ -382,6 +428,21 @@ def random_positive_orthant_game():
 
 
 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)
 
@@ -392,7 +453,24 @@ def random_nn_scaling(G):
 
     return (alpha, H)
 
+
 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