diff --git a/pyttb/cp_apr.py b/pyttb/cp_apr.py index e9afa110..c5b89ef1 100644 --- a/pyttb/cp_apr.py +++ b/pyttb/cp_apr.py @@ -253,7 +253,7 @@ def tt_cp_apr_mu( kktModeViolations = np.zeros((N,)) if printitn > 0: - print("\nCP_APR:\n") + print("CP_APR:") # Start the wall clock timer. start = time.time() @@ -304,7 +304,7 @@ def tt_cp_apr_mu( # Print status if printinneritn != 0 and divmod(i, printinneritn)[1] == 0: print( - "\t\tMode = {}, Inner Iter = {}, KKT violation = {}\n".format( + "\t\tMode = {}, Inner Iter = {}, KKT violation = {}".format( n, i, kktModeViolations[n] ) ) @@ -325,11 +325,11 @@ def tt_cp_apr_mu( # Check for convergence if isConverged: if printitn > 0: - print("Exiting because all subproblems reached KKT tol.\n") + print("Exiting because all subproblems reached KKT tol.") break if nTimes[iter] > stoptime: if printitn > 0: - print("Exiting because time limit exceeded.\n") + print("Exiting because time limit exceeded.") break t_stop = time.time() - start @@ -345,12 +345,12 @@ def tt_cp_apr_mu( normTensor**2 + M.norm() ** 2 - 2 * input_tensor.innerprod(M) ) fit = 1 - (normresidual / normTensor) # fraction explained by model - print("===========================================\n") - print(" Final log-likelihood = {} \n".format(obj)) - print(" Final least squares fit = {} \n".format(fit)) - print(" Final KKT violation = {}\n".format(kktViolations[iter])) - print(" Total inner iterations = {}\n".format(sum(nInnerIters))) - print(" Total execution time = {} secs\n".format(t_stop)) + print("===========================================") + print(" Final log-likelihood = {}".format(obj)) + print(" Final least squares fit = {}".format(fit)) + print(" Final KKT violation = {}".format(kktViolations[iter])) + print(" Total inner iterations = {}".format(sum(nInnerIters))) + print(" Total execution time = {} secs".format(t_stop)) output = {} output["params"] = ( @@ -472,7 +472,7 @@ def tt_cp_apr_pdnr( times = np.zeros((maxiters, 1)) if printitn > 0: - print("\nCP_PDNR (alternating Poisson regression using damped Newton)\n") + print("CP_PDNR (alternating Poisson regression using damped Newton)") dispLineWarn = printinneritn > 0 @@ -493,7 +493,7 @@ def tt_cp_apr_pdnr( sparseIx.append(row_indices) if printitn > 0: - print("done\n") + print("done") e_vec = np.ones((1, rank)) @@ -578,13 +578,16 @@ def tt_cp_apr_pdnr( kktModeViolations[n] = kkt_violation if printinneritn > 0 and np.mod(i, printinneritn) == 0: - print("\tMode = {}, Row = {}, InnerIt = {}".format(n, jj, i)) + print( + "\tMode = {}, Row = {}, InnerIt = {}".format(n, jj, i), + end="", + ) if i == 0: - print(", RowKKT = {}\n".format(kkt_violation)) + print(", RowKKT = {}".format(kkt_violation)) else: print( - ", RowKKT = {}, RowObj = {}\n".format( + ", RowKKT = {}, RowObj = {}".format( kkt_violation, -f_new ) ) @@ -667,7 +670,7 @@ def tt_cp_apr_pdnr( if printitn > 0 and np.mod(iter, printitn) == 0: fnVals[iter] = -tt_loglikelihood(input_tensor, M) print( - "{}. Ttl Inner Its: {}, KKT viol = {}, obj = {}, nz: {}\n".format( + "{}. Ttl Inner Its: {}, KKT viol = {}, obj = {}, nz: {}".format( iter, nInnerIters[iter], kktViolations[iter], @@ -684,7 +687,7 @@ def tt_cp_apr_pdnr( if isConverged and inexact and rowsubprobStopTol <= stoptol: break if times[iter] > stoptime: - print("EXiting because time limit exceeded\n") + print("EXiting because time limit exceeded") break t_stop = time.time() - start @@ -700,12 +703,12 @@ def tt_cp_apr_pdnr( normTensor**2 + M.norm() ** 2 - 2 * input_tensor.innerprod(M) ) fit = 1 - (normresidual / normTensor) # fraction explained by model - print("===========================================\n") - print(" Final log-likelihood = {} \n".format(obj)) - print(" Final least squares fit = {} \n".format(fit)) - print(" Final KKT violation = {}\n".format(kktViolations[iter])) - print(" Total inner iterations = {}\n".format(sum(nInnerIters))) - print(" Total execution time = {} secs\n".format(t_stop)) + print("===========================================") + print(" Final log-likelihood = {}".format(obj)) + print(" Final least squares fit = {}".format(fit)) + print(" Final KKT violation = {}".format(kktViolations[iter])) + print(" Total inner iterations = {}".format(sum(nInnerIters))) + print(" Total execution time = {} secs".format(t_stop)) output = {} output["params"] = ( @@ -840,7 +843,7 @@ def tt_cp_apr_pqnr( times = np.zeros((maxiters, 1)) if printitn > 0: - print("\nCP_PQNR (alternating Poisson regression using quasi-Newton)\n") + print("CP_PQNR (alternating Poisson regression using quasi-Newton)") dispLineWarn = printinneritn > 0 @@ -861,7 +864,7 @@ def tt_cp_apr_pqnr( sparseIx.append(row_indices) if printitn > 0: - print("done\n") + print("done") # Main loop: iterate until convergence or a max threshold is reached for iter in range(maxiters): @@ -958,20 +961,22 @@ def tt_cp_apr_pqnr( # We now use \| KKT \|_{inf}: kkt_violation = np.max(np.abs(np.minimum(m_row, gradM))) - # print("Intermediate Printing m_row: {}\n and gradM{}".format(m_row, gradM)) # Report largest row subproblem initial violation if i == 0 and kkt_violation > kktModeViolations[n]: kktModeViolations[n] = kkt_violation if printinneritn > 0 and np.mod(i, printinneritn) == 0: - print("\tMode = {}, Row = {}, InnerIt = {}".format(n, jj, i)) + print( + "\tMode = {}, Row = {}, InnerIt = {}".format(n, jj, i), + end="", + ) if i == 0: - print(", RowKKT = {}\n".format(kkt_violation)) + print(", RowKKT = {}".format(kkt_violation)) else: print( - ", RowKKT = {}, RowObj = {}\n".format( + ", RowKKT = {}, RowObj = {}".format( kkt_violation, -f_new ) ) @@ -1075,7 +1080,7 @@ def tt_cp_apr_pqnr( if printitn > 0 and np.mod(iter, printitn) == 0: fnVals[iter] = -tt_loglikelihood(input_tensor, M) print( - "{}. Ttl Inner Its: {}, KKT viol = {}, obj = {}, nz: {}\n".format( + "{}. Ttl Inner Its: {}, KKT viol = {}, obj = {}, nz: {}".format( iter, nInnerIters[iter], kktViolations[iter], fnVals[iter], num_zero ) ) @@ -1086,7 +1091,7 @@ def tt_cp_apr_pqnr( if isConverged: break if times[iter] > stoptime: - print("Exiting because time limit exceeded\n") + print("Exiting because time limit exceeded") break t_stop = time.time() - start @@ -1102,12 +1107,12 @@ def tt_cp_apr_pqnr( normTensor**2 + M.norm() ** 2 - 2 * input_tensor.innerprod(M) ) fit = 1 - (normresidual / normTensor) # fraction explained by model - print("===========================================\n") - print(" Final log-likelihood = {} \n".format(obj)) - print(" Final least squares fit = {} \n".format(fit)) - print(" Final KKT violation = {}\n".format(kktViolations[iter])) - print(" Total inner iterations = {}\n".format(sum(nInnerIters))) - print(" Total execution time = {} secs\n".format(t_stop)) + print("===========================================") + print(" Final log-likelihood = {}".format(obj)) + print(" Final least squares fit = {}".format(fit)) + print(" Final KKT violation = {}".format(kktViolations[iter])) + print(" Total inner iterations = {}".format(sum(nInnerIters))) + print(" Total execution time = {} secs".format(t_stop)) output = {} output["params"] = ( diff --git a/pyttb/export_data.py b/pyttb/export_data.py index c8a6ccbb..f35926b9 100644 --- a/pyttb/export_data.py +++ b/pyttb/export_data.py @@ -15,6 +15,9 @@ def export_data(data, filename, fmt_data=None, fmt_weights=None): """ Export tensor-related data to a file. """ + if not isinstance(data, (ttb.tensor, ttb.sptensor, ttb.ktensor, np.ndarray)): + assert False, f"Invalid data type for export: {type(data)}" + # open file fp = open(filename, "w") @@ -54,9 +57,6 @@ def export_data(data, filename, fmt_data=None, fmt_weights=None): export_size(fp, data.shape) export_array(fp, data, fmt_data) - else: - assert False, "Invalid data type for export" - def export_size(fp, shape): # Export the size of something to a file diff --git a/pyttb/import_data.py b/pyttb/import_data.py index bdb2aa7d..b218b0b7 100644 --- a/pyttb/import_data.py +++ b/pyttb/import_data.py @@ -24,23 +24,27 @@ def import_data(filename): data_type = import_type(fp) if data_type not in ["tensor", "sptensor", "matrix", "ktensor"]: + fp.close() assert False, f"Invalid data type found: {data_type}" if data_type == "tensor": shape = import_shape(fp) data = import_array(fp, np.prod(shape)) + fp.close() return ttb.tensor().from_data(data, shape) elif data_type == "sptensor": shape = import_shape(fp) nz = import_nnz(fp) subs, vals = import_sparse_array(fp, len(shape), nz) + fp.close() return ttb.sptensor().from_data(subs, vals, shape) elif data_type == "matrix": shape = import_shape(fp) mat = import_array(fp, np.prod(shape)) mat = np.reshape(mat, np.array(shape)) + fp.close() return mat elif data_type == "ktensor": @@ -54,11 +58,9 @@ def import_data(filename): fac = import_array(fp, np.prod(fac_shape)) fac = np.reshape(fac, np.array(fac_shape)) factor_matrices.append(fac) + fp.close() return ttb.ktensor().from_data(weights, factor_matrices) - # Close file - fp.close() - def import_type(fp): # Import IO data type diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index 59bc60d9..1a14234b 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -671,14 +671,11 @@ def logical_and(self, B: Union[float, sptensor, ttb.tensor]) -> sptensor: if not self.shape == B.shape: assert False, "Must be tensors of the same shape" - def is_length_2(x): - return len(x) == 2 - C = sptensor.from_aggregator( np.vstack((self.subs, B.subs)), np.vstack((self.vals, B.vals)), self.shape, - is_length_2, + lambda x: len(x) == 2, ) return C @@ -735,15 +732,11 @@ def logical_or( assert False, "Logical Or requires tensors of the same size" if isinstance(B, ttb.sptensor): - - def is_length_ge_1(x): - return len(x) >= 1 - return sptensor.from_aggregator( np.vstack((self.subs, B.subs)), np.ones((self.subs.shape[0] + B.subs.shape[0], 1)), self.shape, - is_length_ge_1, + lambda x: len(x) >= 1, ) assert False, "Sptensor Logical Or argument must be scalar or sptensor" @@ -780,12 +773,9 @@ def logical_xor( if self.shape != other.shape: assert False, "Logical XOR requires tensors of the same size" - def length1(x): - return len(x) == 1 - subs = np.vstack((self.subs, other.subs)) return ttb.sptensor.from_aggregator( - subs, np.ones((len(subs), 1)), self.shape, length1 + subs, np.ones((len(subs), 1)), self.shape, lambda x: len(x) == 1 ) assert False, "The argument must be an sptensor, tensor or scalar" diff --git a/tests/data/invalid_dims.tns b/tests/data/invalid_dims.tns new file mode 100644 index 00000000..12816c94 --- /dev/null +++ b/tests/data/invalid_dims.tns @@ -0,0 +1,11 @@ +matrix +2 +4 2 1 +1.0000000000000000e+00 +5.0000000000000000e+00 +2.0000000000000000e+00 +6.0000000000000000e+00 +3.0000000000000000e+00 +7.0000000000000000e+00 +4.0000000000000000e+00 +8.0000000000000000e+00 \ No newline at end of file diff --git a/tests/data/invalid_type.tns b/tests/data/invalid_type.tns new file mode 100644 index 00000000..6df86973 --- /dev/null +++ b/tests/data/invalid_type.tns @@ -0,0 +1,11 @@ +list +2 +4 2 +1.0000000000000000e+00 +5.0000000000000000e+00 +2.0000000000000000e+00 +6.0000000000000000e+00 +3.0000000000000000e+00 +7.0000000000000000e+00 +4.0000000000000000e+00 +8.0000000000000000e+00 \ No newline at end of file diff --git a/tests/test_cp_apr.py b/tests/test_cp_apr.py index aa442183..7e1a7fc4 100644 --- a/tests/test_cp_apr.py +++ b/tests/test_cp_apr.py @@ -148,7 +148,7 @@ def test_cpapr_mu(capsys): ktensorInstance = ttb.ktensor.from_data(weights, factor_matrices) tensorInstance = ktensorInstance.full() np.random.seed(123) - M, _, _ = ttb.cp_apr(tensorInstance, 2) + M, _, _ = ttb.cp_apr(tensorInstance, 2, printinneritn=1) # Consume the cp_apr diagnostic printing capsys.readouterr() assert np.isclose(M.full().data, ktensorInstance.full().data).all() @@ -175,7 +175,9 @@ def test_cpapr_pdnr(capsys): ktensorInstance = ttb.ktensor.from_data(weights, factor_matrices) tensorInstance = ktensorInstance.full() np.random.seed(123) - M, _, _ = ttb.cp_apr(tensorInstance, 2, algorithm="pdnr") + M, _, _ = ttb.cp_apr( + tensorInstance, 2, algorithm="pdnr", printinneritn=1, inexact=False + ) capsys.readouterr() assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-04).all() @@ -221,7 +223,7 @@ def test_cpapr_pqnr(capsys): ktensorInstance = ttb.ktensor.from_data(weights, factor_matrices) tensorInstance = ktensorInstance.full() np.random.seed(123) - M, _, _ = ttb.cp_apr(tensorInstance, 2, algorithm="pqnr") + M, _, _ = ttb.cp_apr(tensorInstance, 2, algorithm="pqnr", printinneritn=1) capsys.readouterr() assert np.isclose(M.full().data, ktensorInstance.full().data, rtol=1e-01).all() diff --git a/tests/test_import_export_data.py b/tests/test_import_export_data.py index c75d4d13..e5baaa61 100644 --- a/tests/test_import_export_data.py +++ b/tests/test_import_export_data.py @@ -115,6 +115,32 @@ def test_import_data_array(sample_array): assert (M == X).all() +@pytest.mark.indevelopment +def test_import_invalid(): + # invalid filename + data_filename = os.path.join( + os.path.dirname(__file__), "data", "invalid_filename.tns" + ) + + with pytest.raises(AssertionError) as excinfo: + X = ttb.import_data(data_filename) + assert f"File path {data_filename} does not exist." in str(excinfo) + + # invalid type + data_filename = os.path.join(os.path.dirname(__file__), "data", "invalid_type.tns") + + with pytest.raises(AssertionError) as excinfo: + X = ttb.import_data(data_filename) + assert f"Invalid data type found: list" in str(excinfo) + + # invalid type + data_filename = os.path.join(os.path.dirname(__file__), "data", "invalid_dims.tns") + + with pytest.raises(AssertionError) as excinfo: + X = ttb.import_data(data_filename) + assert "Imported dimensions are not of expected size" in str(excinfo) + + @pytest.mark.indevelopment def test_export_data_tensor(sample_tensor): # truth data @@ -196,3 +222,14 @@ def test_export_data_array(sample_array): X = ttb.import_data(data_filename) assert (M == X).all() os.unlink(data_filename) + + +@pytest.mark.indevelopment +def test_export_invalid(): + # list data is invalid + data = [1, 2, 3] + data_filename = os.path.join(os.path.dirname(__file__), "data", "invalid.out") + + with pytest.raises(AssertionError) as excinfo: + ttb.export_data(data, data_filename) + assert f"Invalid data type for export: {type(data)}" in str(excinfo) diff --git a/tests/test_ktensor.py b/tests/test_ktensor.py index 861a16fc..5444344c 100644 --- a/tests/test_ktensor.py +++ b/tests/test_ktensor.py @@ -374,6 +374,24 @@ def test_ktensor_fixsigns(sample_ktensor_2way): assert np.linalg.norm(K.factor_matrices[0] - factor_matrix10) < 1e-8 assert np.linalg.norm(K.factor_matrices[1] - factor_matrix11) < 1e-8 + # test invalid call with non-ktensor argument + with pytest.raises(AssertionError) as excinfo: + K = K.fixsigns([1, 2, 3]) + assert "other must be a ktensor" in str(excinfo) + + # test odd number of vectors to flip + K = ttb.ktensor.from_data( + np.array([3, 2, 1]), np.ones((3, 3)), np.ones((4, 3)), np.ones((5, 3)) + ) + K2 = K.copy() + # one column to flip + K2.factor_matrices[0][:, 0] = -1 + # 3 columns to flip + K2.factor_matrices[0][:, 0] = -1 + K2.factor_matrices[1][:, 1] = -1 + K2.factor_matrices[2][:, 2] = -1 + K = K.fixsigns(K2) + @pytest.mark.indevelopment def test_ktensor_full(sample_ktensor_2way, sample_ktensor_3way): @@ -814,6 +832,16 @@ def test_ktensor_score(): assert flag == 1 assert (best_perm == np.array([0, 1, 2])).all() + # zero lambda values lead to equal components + A0 = ttb.ktensor.from_data( + np.array([2, 0]), np.ones((3, 2)), np.ones((4, 2)), np.ones((5, 2)) + ) + B0 = ttb.ktensor.from_data( + np.array([2, 0]), np.ones((3, 2)), np.ones((4, 2)), np.ones((5, 2)) + ) + score, Aperm, flag, best_perm = A0.score(B0) + assert score == 1.0 + # compute score using exhaustive search with pytest.raises(AssertionError) as excinfo: score, Aperm, flag, best_perm = A.score(B, greedy=False) @@ -835,6 +863,15 @@ def test_ktensor_score(): score, Aperm, flag, best_perm = A.score(B) assert "Size mismatch" in str(excinfo) + # invalid: number of compnents of first ktensor must be greater than or + # equal to number of components of second ktensor + with pytest.raises(AssertionError) as excinfo: + B = ttb.ktensor.from_data( + np.array([2, 4]), np.ones((3, 2)), np.ones((4, 2)), np.ones((5, 2)) + ) + score, Aperm, flag, best_perm = B.score(A) + assert "Tensor A must have at least as many components as tensor B" in str(excinfo) + pytest.mark.indevelopment