From f2d4185b8c1a263500848554d4283c7ec3201b1c Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Tue, 1 Nov 2016 17:54:31 -0400 Subject: [PATCH] Clean up and document some of the new test code. --- .pylintrc | 5 +- dunshire/matrices.py | 3 +- test/matrices_test.py | 156 +++++++++++++++++++++++++++++++----------- test/randomgen.py | 108 +++++++++++++++++++++++++---- 4 files changed, 216 insertions(+), 56 deletions(-) diff --git a/.pylintrc b/.pylintrc index 16b1ca7..782cd4a 100644 --- 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. diff --git a/dunshire/matrices.py b/dunshire/matrices.py index bcf8377..29c5867 100644 --- a/dunshire/matrices.py +++ b/dunshire/matrices.py @@ -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) diff --git a/test/matrices_test.py b/test/matrices_test.py index 195549f..ce57c7d 100644 --- a/test/matrices_test.py +++ b/test/matrices_test.py @@ -2,87 +2,165 @@ 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) diff --git a/test/randomgen.py b/test/randomgen.py index 6513440..afe0dae 100644 --- a/test/randomgen.py +++ b/test/randomgen.py @@ -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 -- 2.43.2