diff --git a/pyttb/cp_apr.py b/pyttb/cp_apr.py index fbe1c044..d1eb6e9b 100644 --- a/pyttb/cp_apr.py +++ b/pyttb/cp_apr.py @@ -15,7 +15,7 @@ def cp_apr(tensor, rank, algorithm='mu', stoptol=1e-4, stoptime=1e6, maxiters=10 kappa=0.01, kappatol=1e-10, epsActive=1e-8, mu0=1e-5, precompinds=True, inexact=True, lbfgsMem=3): """ - Compute nonnegative CP with alternating Poisson regression. + Compute non-negative CP with alternating Poisson regression. Parameters ---------- @@ -376,9 +376,10 @@ def tt_cp_apr_pdnr(tensor, rank, init, stoptol, stoptime, maxiters, maxinneriter sparseIx = [] for n in range(N): num_rows = M[n].shape[0] - sparseIx.append(np.zeros((num_rows, 1))) + row_indices = [] for jj in range(num_rows): - sparseIx[n][jj] = np.where(tensor.subs[:, n] == jj)[0] + row_indices.append(np.where(tensor.subs[:, n] == jj)[0]) + sparseIx.append(row_indices) if printitn > 0: print('done\n') @@ -426,7 +427,7 @@ def tt_cp_apr_pdnr(tensor, rank, init, stoptol, stoptime, maxiters, maxinneriter M.factor_matrices[n][jj, :] = 0 continue - x_row = tensor.vals(sparse_indices) + x_row = tensor.vals[sparse_indices] # Calculate just the columns of Pi needed for this row. Pi = ttb.tt_calcpi_prowsubprob(tensor, M, rank, n, N, isSparse, sparse_indices) @@ -683,9 +684,10 @@ def tt_cp_apr_pqnr(tensor, rank, init, stoptol, stoptime, maxiters, maxinneriter sparseIx = [] for n in range(N): num_rows = M[n].shape[0] - sparseIx.append(np.zeros((num_rows, 1))) + row_indices = [] for jj in range(num_rows): - sparseIx[n][jj] = np.where(tensor.subs[:, n] == jj)[0] + row_indices.append(np.where(tensor.subs[:, n] == jj)[0]) + sparseIx.append(row_indices) if printitn > 0: print('done\n') @@ -726,7 +728,7 @@ def tt_cp_apr_pqnr(tensor, rank, init, stoptol, stoptime, maxiters, maxinneriter M.factor_matrices[n][jj, :] = 0 continue - x_row = tensor.vals(sparse_indices) + x_row = tensor.vals[sparse_indices] # Calculate just the columns of Pi needed for this row. Pi = ttb.tt_calcpi_prowsubprob(tensor, M, rank, n, N, isSparse, sparse_indices) @@ -1227,7 +1229,7 @@ def tt_loglikelihood_row(isSparse, data_row, model_row, Pi): """ term1 = -np.sum(model_row) if isSparse: - term2 = np.sum(data_row.transpose() * np.log(model_row.dot(Pi))) + term2 = np.sum(data_row.transpose() * np.log(model_row.dot(Pi.transpose()))) else: b_pi = model_row.dot(Pi.transpose()) term2 = 0 diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index 38e586d0..4092753a 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -380,7 +380,6 @@ def end(self, k = None): Parameters ---------- k: int Dimension for subscript indexing - n: int 1 for linear indexing, ndims for subscript Returns ------- @@ -419,11 +418,14 @@ def extract(self, searchsubs): invalid = (searchsubs < 0) | (searchsubs >= np.array(self.shape)) badloc = np.where(np.sum(invalid, axis=1) > 0) if badloc[0].size > 0: - print('The following subscripts are invalid: \n') + error_msg = "The following subscripts are invalid: \n" badsubs = searchsubs[badloc, :] for i in np.arange(0, badloc[0].size): - print('\tsubscript = {}) \n'.format(tt_intvec2str(badsubs[i, :]))) - assert False, 'Invalid subscripts' + error_msg += f"\tsubscript = {tt_intvec2str(badsubs[i, :])} \n" + assert False, ( + f"{error_msg}" + 'Invalid subscripts' + ) # Set the default answer to zero a = np.zeros(shape=(p, 1), dtype=self.vals.dtype) @@ -687,7 +689,7 @@ def mttkrp(self, U, n): >>> subs = np.array([[1, 1, 1], [1, 1, 3], [2, 2, 2], [3, 3, 3]]) >>> vals = np.array([[0.5], [1.5], [2.5], [3.5]]) >>> shape = (4, 4, 4) - >>> sptensorInstance.from_data(subs, vals, shape) + >>> sptensorInstance = sptensor.from_data(subs, vals, shape) >>> sptensorInstance.mttkrp(np.array([matrix, matrix, matrix]), 0) [[0, 0, 0, 0], [2, 2, 2, 2], @@ -962,7 +964,7 @@ def subdims(self, region): -------- >>> subs = np.array([[1, 1, 1], [1, 1, 3], [2, 2, 2], [3, 3, 3]]) >>> vals = np.array([[0.5], [1.5], [2.5], [3.5]]) - >>> shape = np.array([4, 4, 4]) + >>> shape = (4, 4, 4) >>> sp = sptensor.from_data(subs,vals,shape) >>> region = np.ndarray([1, [1], [1,3]]) >>> loc = sp.subdims(region) @@ -1706,7 +1708,7 @@ def __mul__(self, other): Parameters ---------- - :class:`pyttb.sptensor`, :class:`pyttb.tensor`, float, int + other: :class:`pyttb.sptensor`, :class:`pyttb.tensor`, float, int Returns ------- @@ -1748,7 +1750,7 @@ def __rmul__(self, other): Parameters ---------- - float, int + other: float, int Returns ------- diff --git a/pyttb/tenmat.py b/pyttb/tenmat.py index 9e48f849..c5458794 100644 --- a/pyttb/tenmat.py +++ b/pyttb/tenmat.py @@ -13,7 +13,7 @@ class tenmat(object): """ - def __init__(self, *args): + def __init__(self): """ TENSOR Create empty tensor. """ @@ -190,10 +190,7 @@ def ndims(self): ------- int """ - if self.shape == (0,): - return 0 - else: - return len(self.shape) + return len(self.shape) def norm(self): """ diff --git a/pyttb/tensor.py b/pyttb/tensor.py index dbb39a8d..0cdb49f4 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -103,7 +103,6 @@ def from_tensor_type(cls, source): order = np.hstack([source.rindices, source.cindices]) data = np.reshape(source.data.copy(), np.array(shape)[order], order='F') if order.size > 1: - # data = ipermute(data, order) data = np.transpose(data, np.argsort(order)) return cls.from_data(data, shape) @@ -944,7 +943,7 @@ def ttm(self, matrix, dims=None, transpose=False): def ttt(self, other, selfdims=None, otherdims=None): """ - Tensor mulitplication (tensor times tensor) + Tensor multiplication (tensor times tensor) Parameters ---------- @@ -1455,7 +1454,7 @@ def tensor_add(x, y): def __radd__(self, other): """ - Reverse binary addition (+) for tensors + Right binary addition (+) for tensors Parameters ---------- diff --git a/tests/test_cp_apr.py b/tests/test_cp_apr.py index ed1d28ce..1139e983 100644 --- a/tests/test_cp_apr.py +++ b/tests/test_cp_apr.py @@ -125,6 +125,13 @@ def test_cpapr_mu(capsys): capsys.readouterr() assert output['nTotalIters'] == 2 + # Edge cases + # Confirm timeout works + non_correct_answer = ktensorInstance*2 + _ = ttb.cp_apr(tensorInstance, 2, init=non_correct_answer, stoptime=-1) + out, _ = capsys.readouterr() + assert "time limit exceeded" in out + @pytest.mark.indevelopment def test_cpapr_pdnr(capsys): # Test simple case @@ -139,6 +146,23 @@ def test_cpapr_pdnr(capsys): capsys.readouterr() assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-04).all() + # Try solve with sptensor + sptensorInstance = ttb.sptensor.from_tensor_type(tensorInstance) + np.random.seed(123) + M, _, _ = ttb.cp_apr(sptensorInstance, 2, algorithm="pdnr") + capsys.readouterr() + assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-04).all() + M, _, _ = ttb.cp_apr(sptensorInstance, 2, algorithm="pdnr", precompinds=False) + capsys.readouterr() + assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-04).all() + + # Edge cases + # Confirm timeout works + non_correct_answer = ktensorInstance*2 + _ = ttb.cp_apr(tensorInstance, 2, init=non_correct_answer, algorithm="pdnr", stoptime=-1) + out, _ = capsys.readouterr() + assert "time limit exceeded" in out + @pytest.mark.indevelopment def test_cpapr_pqnr(capsys): # Test simple case @@ -165,6 +189,22 @@ def test_cpapr_pqnr(capsys): capsys.readouterr() assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-01).all() + # Try solve with sptensor + sptensorInstance = ttb.sptensor.from_tensor_type(tensorInstance) + np.random.seed(123) + M, _, _ = ttb.cp_apr(sptensorInstance, 2, algorithm="pqnr") + capsys.readouterr() + assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-01).all() + M, _, _ = ttb.cp_apr(sptensorInstance, 2, algorithm="pqnr", precompinds=False) + capsys.readouterr() + assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-01).all() + + # Edge cases + # Confirm timeout works + _ = ttb.cp_apr(tensorInstance, 2, algorithm="pqnr", stoptime=-1) + out, _ = capsys.readouterr() + assert "time limit exceeded" in out + # PDNR tests below @pytest.mark.indevelopment @@ -367,3 +407,22 @@ def test_getSearchDirPqnr(): search, pred = ttb.getSearchDirPqnr(model_row, phi, 1e-6, delta_model, delta_grad, phi, 1, 5, False) # This only verifies that for the right shaped input nothing crashes. Doesn't verify correctness assert True + +def test_cp_apr_negative_tests(): + dense_tensor = ttb.tensor.from_data(np.ones((2, 2, 2))) + bad_weights = np.array([8.]) + bad_factors = [np.array([[1.]])] * 3 + bad_initial_guess_shape = ttb.ktensor.from_data(bad_weights, bad_factors) + with pytest.raises(AssertionError): + ttb.cp_apr(dense_tensor, init=bad_initial_guess_shape, rank=1) + good_weights = np.array([8.] * 3) + good_factor = np.array([[1., 1., 1.], [1., 1., 1.]]) + bad_initial_guess_factors = ttb.ktensor.from_data(good_weights, [-1. * good_factor] * 3) + with pytest.raises(AssertionError): + ttb.cp_apr(dense_tensor, init=bad_initial_guess_factors, rank=3) + bad_initial_guess_weight = ttb.ktensor.from_data(-1. * good_weights, [good_factor] * 3) + with pytest.raises(AssertionError): + ttb.cp_apr(dense_tensor, init=bad_initial_guess_weight, rank=3) + + with pytest.raises(AssertionError): + ttb.cp_apr(dense_tensor, rank=1, algorithm='UNSUPPORTED_ALG') diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 00000000..3585af32 --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,10 @@ +import pyttb as ttb +import pytest + + +def test_package_smoke(): + """A few sanity checks to make sure things don't explode""" + assert len(ttb.__version__) > 0 + # Make sure warnings filter doesn't crash + ttb.ignore_warnings(False) + ttb.ignore_warnings(True) diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py index e1982257..b7c52a2b 100644 --- a/tests/test_pyttb_utils.py +++ b/tests/test_pyttb_utils.py @@ -3,6 +3,7 @@ # U.S. Government retains certain rights in this software. import pyttb as ttb +import logging import numpy as np import pytest import scipy.sparse as sparse @@ -172,8 +173,6 @@ def tensor_max(x): assert "Tensor 2 is not the same size as the first tensor input" in str(excinfo) - - @pytest.mark.indevelopment def test_tt_setdiff_rows(): a = np.array([[4, 6], [1, 9], [2, 6], [2, 6], [99, 0]]) @@ -184,6 +183,10 @@ def test_tt_setdiff_rows(): b = np.array([[1, 7], [1, 8]]) assert (ttb.tt_setdiff_rows(a, b) == np.array([0, 1])).all() + a = np.array([[4, 6], [1, 9]]) + b = np.array([]) + assert (ttb.tt_setdiff_rows(a, b) == np.arange(a.shape[0])).all() + assert (ttb.tt_setdiff_rows(b, a) == b).all() @pytest.mark.indevelopment def test_tt_intersect_rows(): @@ -191,6 +194,11 @@ def test_tt_intersect_rows(): b = np.array([[1, 7], [1, 8], [2, 6], [2, 1], [2, 4], [4, 6], [4, 7], [5, 9], [5, 2], [5, 1]]) assert (ttb.tt_intersect_rows(a, b) == np.array([2, 0])).all() + a = np.array([[4, 6], [1, 9]]) + b = np.array([]) + assert (ttb.tt_intersect_rows(a, b) == b).all() + assert (ttb.tt_intersect_rows(a, b) == ttb.tt_intersect_rows(b, a)).all() + @pytest.mark.indevelopment def test_tt_ismember_rows(): @@ -330,13 +338,13 @@ def test_tt_ind2sub_valid(): subs = np.array([[0, 0, 0], [1, 1, 1], [3, 3, 3]]) idx = np.array([0, 21, 63]) shape = (4, 4, 4) - print(f'\nttb.tt_ind2sub(shape, idx): {ttb.tt_ind2sub(shape, idx)}') + logging.debug(f'\nttb.tt_ind2sub(shape, idx): {ttb.tt_ind2sub(shape, idx)}') assert (ttb.tt_ind2sub(shape, idx) == subs).all() subs = np.array([[1, 0], [0, 1]]) idx = np.array([1, 2]) shape = (2, 2) - print(f'\nttb.tt_ind2sub(shape, idx): {ttb.tt_ind2sub(shape, idx)}') + logging.debug(f'\nttb.tt_ind2sub(shape, idx): {ttb.tt_ind2sub(shape, idx)}') assert (ttb.tt_ind2sub(shape, idx) == subs).all() empty = np.array([]) diff --git a/tests/test_sptensor.py b/tests/test_sptensor.py index 1b008e23..d755ee14 100644 --- a/tests/test_sptensor.py +++ b/tests/test_sptensor.py @@ -3,6 +3,7 @@ # U.S. Government retains certain rights in this software. import pyttb as ttb +import logging import numpy as np import pytest import scipy.sparse as sparse @@ -47,9 +48,9 @@ def test_sptensor_initialization_from_tensor_type(sample_sptensor): inputData = np.array([[1, 2, 3], [4, 5, 6]]) tensorInstance = ttb.tensor.from_data(inputData) sptensorFromTensor = ttb.sptensor.from_tensor_type(tensorInstance) - print(f'inputData = {inputData}') - print(f'tensorInstance = {tensorInstance}') - print(f'sptensorFromTensor = {sptensorFromTensor}') + logging.debug(f'inputData = {inputData}') + logging.debug(f'tensorInstance = {tensorInstance}') + logging.debug(f'sptensorFromTensor = {sptensorFromTensor}') assert (sptensorFromTensor.subs == ttb.tt_ind2sub(inputData.shape, np.arange(0, inputData.size))).all() assert (sptensorFromTensor.vals == inputData.reshape((inputData.size, 1), order='F')).all() assert (sptensorFromTensor.shape == inputData.shape) @@ -59,6 +60,11 @@ def test_sptensor_initialization_from_tensor_type(sample_sptensor): sptensorFromCOOMatrix = ttb.sptensor.from_tensor_type(sparse.coo_matrix(inputData)) assert (sptensorFromCOOMatrix.spmatrix() != sparse.coo_matrix(inputData)).nnz == 0 + # Negative Tests + with pytest.raises(AssertionError): + invalid_tensor_type = [] + ttb.sptensor.from_tensor_type(invalid_tensor_type) + @pytest.mark.indevelopment def test_sptensor_initialization_from_function(): @@ -247,6 +253,8 @@ def test_sptensor__getitem__(sample_sptensor): ## Case 2 Linear Indexing ind = ttb.tt_sub2ind(data['shape'], np.array([[1, 1, 1], [1, 1, 3], [2, 2, 2]])) assert (sptensorInstance[ind] == np.array([[0.5], [1.5], [2.5]])).all() + list_ind = list(ind) + assert (sptensorInstance[list_ind] == np.array([[0.5], [1.5], [2.5]])).all() ind2 = ttb.tt_sub2ind(data['shape'], np.array([[1, 1, 1], [1, 1, 3]])) assert (sptensorInstance[ind2] == np.array([[0.5], [1.5]])).all() with pytest.raises(AssertionError) as excinfo: @@ -622,15 +630,15 @@ def test_sptensor__eq__(sample_sptensor): denseTensor = ttb.tensor.from_tensor_type(sptensorInstance) eqSptensor = sptensorInstance == denseTensor - print(f"\ndenseTensor = {denseTensor}") - print(f"\nsptensorInstance = {sptensorInstance}") - print(f"\ntype(eqSptensor.subs) = \n{type(eqSptensor.subs)}") + logging.debug(f"\ndenseTensor = {denseTensor}") + logging.debug(f"\nsptensorInstance = {sptensorInstance}") + logging.debug(f"\ntype(eqSptensor.subs) = \n{type(eqSptensor.subs)}") for i in range(eqSptensor.subs.shape[0]): - print(f"{i}\t{eqSptensor.subs[i,:]}") - print(f"\neqSptensor.subs = \n{eqSptensor.subs}") - print(f"\neqSptensor.subs.shape[0] = {eqSptensor.subs.shape[0]}") - print(f"\nsptensorInstance.shape = {sptensorInstance.shape}") - print(f"\nnp.prod(sptensorInstance.shape) = {np.prod(sptensorInstance.shape)}") + logging.debug(f"{i}\t{eqSptensor.subs[i,:]}") + logging.debug(f"\neqSptensor.subs = \n{eqSptensor.subs}") + logging.debug(f"\neqSptensor.subs.shape[0] = {eqSptensor.subs.shape[0]}") + logging.debug(f"\nsptensorInstance.shape = {sptensorInstance.shape}") + logging.debug(f"\nnp.prod(sptensorInstance.shape) = {np.prod(sptensorInstance.shape)}") assert eqSptensor.subs.shape[0] == np.prod(sptensorInstance.shape) denseTensor = ttb.tensor.from_data(np.ones((5, 5, 5))) @@ -1371,7 +1379,7 @@ def test_sptensor_ttm(sample_sptensor): # This is a multiway multiplication yielding a sparse tensor, yielding a dense tensor relies on tensor.ttm matrix = sparse.coo_matrix(np.eye(4)) list_of_matrices = [matrix, matrix, matrix] - assert sptensorInstance.ttm(list_of_matrices, dims=np.array([0, 1, 2])).isequal(sptensorInstance) + assert sptensorInstance.ttm(list_of_matrices, dims=[0, 1, 2]).isequal(sptensorInstance) with pytest.raises(AssertionError) as excinfo: sptensorInstance.ttm(sparse.coo_matrix(np.ones((5, 5))), dims=0) diff --git a/tests/test_tensor.py b/tests/test_tensor.py index 46d7cf5d..315fa0a7 100644 --- a/tests/test_tensor.py +++ b/tests/test_tensor.py @@ -199,7 +199,7 @@ def test_tensor_ndims(sample_tensor_2way, sample_tensor_3way, sample_tensor_4way assert ttb.tensor.from_data(np.array([])) == 0 @pytest.mark.indevelopment -def test_tensor_setitem(sample_tensor_2way): +def test_tensor__setitem__(sample_tensor_2way): (params, tensorInstance) = sample_tensor_2way # Subtensor assign with constant @@ -567,6 +567,9 @@ def test_tensor__add__(sample_tensor_2way): # Tensor + scalar assert ((tensorInstance + 1).data == 1 + (params['data'])).all() + # scalar + Tensor + assert ((1 + tensorInstance).data == 1 + (params['data'])).all() + @pytest.mark.indevelopment def test_tensor__sub__(sample_tensor_2way): (params, tensorInstance) = sample_tensor_2way @@ -864,6 +867,14 @@ def test_tensor_collapse(sample_tensor_2way, sample_tensor_3way, sample_tensor_4 tensorCollapse4Max = tensorInstance4.collapse(np.array([0, 2]), fun=np.max) assert (tensorCollapse4Max.data == data4max).all() + # Empty tensor collapse + empty_data = np.array([]) + empty_tensor = ttb.tensor.from_data(empty_data) + assert np.all(empty_tensor.collapse() == empty_data) + + # Empty dims + assert tensorInstance2.collapse(empty_data).isequal(tensorInstance2) + @pytest.mark.indevelopment def test_tensor_contract(sample_tensor_2way, sample_tensor_3way, sample_tensor_4way): (params2, tensorInstance2) = sample_tensor_2way @@ -1076,6 +1087,17 @@ def test_tensor_ttt(sample_tensor_2way, sample_tensor_3way, sample_tensor_4way): assert (TTT1[:,1,1,1,1,1].data == data12).all() assert (TTT1[:,2,1,2,3,1].data == data13).all() + TTT1_with_dims = M31.ttt(M31, selfdims=np.array([0,1,2]), otherdims=np.array([0,1,2])) + assert np.allclose(TTT1_with_dims, M31.innerprod(M31)) + + # Negative tests + with pytest.raises(AssertionError): + invalid_tensor_type = [] + M31.ttt(invalid_tensor_type) + + with pytest.raises(AssertionError): + M31.ttt(M31, selfdims=np.array([0, 1, 2]), otherdims=np.array([0, 2, 1])) + @pytest.mark.indevelopment def test_tensor_ttv(sample_tensor_2way, sample_tensor_3way, sample_tensor_4way):