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)
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
-------
>>> 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
-------
>>> 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):
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)
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.
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())
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())
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())
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)
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