From 70cab804eba4efc08cbba721120de1a78381af2d Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Sun, 1 Dec 2024 16:15:50 -0500 Subject: [PATCH 01/10] PYTTB_UTILS: Finish adding types without refactor --- pyttb/pyttb_utils.py | 44 +++++++++++++++++++++++++++------------ tests/test_pyttb_utils.py | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index 3fb2fcda..4e13ffd4 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -9,10 +9,12 @@ from enum import Enum from inspect import signature from typing import ( + Any, Iterable, List, Literal, Optional, + Sequence, Tuple, Union, get_args, @@ -49,7 +51,7 @@ def tt_union_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: [1, 2], [3, 4]]) """ - # TODO ismember and uniqe are very similar in function + # TODO ismember and unique are very similar in function if MatrixA.size > 0: MatrixAUnique, idxA = np.unique(MatrixA, axis=0, return_index=True) else: @@ -165,6 +167,7 @@ def tt_dimscheck( return sdims, vidx +# Fixme: Needs types def tt_tenfun(function_handle, *inputs): # noqa: PLR0912 """ Apply a function to each element in a tensor @@ -334,7 +337,9 @@ def tt_intersect_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: return location[valid] -def tt_irenumber(t: ttb.sptensor, shape: Tuple[int, ...], number_range) -> np.ndarray: +def tt_irenumber( + t: ttb.sptensor, shape: Tuple[int, ...], number_range: Sequence[IndexType] +) -> np.ndarray: """ RENUMBER indices for sptensor subsasgn @@ -366,14 +371,14 @@ def tt_irenumber(t: ttb.sptensor, shape: Tuple[int, ...], number_range) -> np.nd # This appears to be inserting new keys as rows to our subs here newsubs = np.insert(newsubs, obj=i, values=r, axis=1) else: - if isinstance(r, list): + if not isinstance(r, np.ndarray): r = np.array(r) # noqa: PLW2901 newsubs[:, i] = r[newsubs[:, i]] return newsubs def tt_renumber( - subs: np.ndarray, shape: Tuple[int, ...], number_range + subs: np.ndarray, shape: Tuple[int, ...], number_range: Sequence[IndexType] ) -> Tuple[np.ndarray, Tuple[int, ...]]: """ RENUMBER indices for sptensor subsref @@ -407,10 +412,18 @@ def tt_renumber( if isinstance(number_range[i], (int, float, np.integer)): newshape[i] = number_range[i] else: - newshape[i] = len(number_range[i]) + # This should be statically determinable but mypy unhappy + # without assert + number_range_i = number_range[i] + assert not isinstance(number_range_i, (int, slice, np.integer)) + newshape[i] = len(number_range_i) else: # TODO get this length without generating the range - newshape[i] = len(range(0, shape[i])[number_range[i]]) + # This should be statically determinable but mypy unhappy + # without assert + number_range_i = number_range[i] + assert isinstance(number_range_i, slice) + newshape[i] = len(range(0, shape[i])[number_range_i]) else: newsubs[:, i], newshape[i] = tt_renumberdim( subs[:, i], shape[i], number_range[i] @@ -419,7 +432,9 @@ def tt_renumber( return newsubs, tuple(newshape) -def tt_renumberdim(idx: np.ndarray, shape: int, number_range) -> Tuple[int, int]: +def tt_renumberdim( + idx: np.ndarray, shape: int, number_range: IndexType +) -> Tuple[int, int]: """ RENUMBERDIM helper function for RENUMBER @@ -436,12 +451,15 @@ def tt_renumberdim(idx: np.ndarray, shape: int, number_range) -> Tuple[int, int] """ # Determine the size of the new range if isinstance(number_range, (int, np.integer)): + number_range = [int(number_range)] newshape = 0 elif isinstance(number_range, slice): - number_range = range(0, shape)[number_range] + number_range = list(range(0, shape))[number_range] newshape = len(number_range) - else: + elif isinstance(number_range, (Sequence, np.ndarray)): newshape = len(number_range) + else: + raise ValueError(f"Bad number range: {number_range}") # Create map from old range to the new range idx_map = np.zeros(shape=shape) @@ -520,7 +538,7 @@ def tt_ind2sub(shape: Tuple[int, ...], idx: np.ndarray) -> np.ndarray: return np.array(np.unravel_index(idx, shape, order="F")).transpose() -def tt_subsubsref(obj, s): +def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: """ Helper function for tensor toolbox subsref. @@ -770,8 +788,8 @@ class IndexVariant(Enum): # We probably want to create a specific file for utility types -LinearIndexType = Union[int, float, np.generic, slice] -IndexType = Union[LinearIndexType, list, np.ndarray] +LinearIndexType = Union[int, np.integer, slice] +IndexType = Union[LinearIndexType, Sequence[int], np.ndarray] def get_index_variant(indices: IndexType) -> IndexVariant: @@ -788,7 +806,7 @@ def get_index_variant(indices: IndexType) -> IndexVariant: variant = IndexVariant.SUBSCRIPTS elif isinstance(indices, tuple): variant = IndexVariant.SUBTENSOR - elif isinstance(indices, list): + elif isinstance(indices, Sequence) and isinstance(indices[0], int): # TODO this is slightly redundant/inefficient key = np.array(indices) if len(key.shape) == 1 or key.shape[1] == 1: diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py index d5358e10..27159893 100644 --- a/tests/test_pyttb_utils.py +++ b/tests/test_pyttb_utils.py @@ -469,7 +469,7 @@ def test_islogical_invalid(): def test_get_index_variant_linear(): assert ttb_utils.get_index_variant(1) == ttb_utils.IndexVariant.LINEAR - assert ttb_utils.get_index_variant(1.0) == ttb_utils.IndexVariant.LINEAR + assert ttb_utils.get_index_variant(1.0) == ttb_utils.IndexVariant.UNKNOWN assert ttb_utils.get_index_variant(slice(1, 5)) == ttb_utils.IndexVariant.LINEAR assert ttb_utils.get_index_variant(np.int32(2)) == ttb_utils.IndexVariant.LINEAR assert ( From 29073f9dcea808e58f3456ebdb94e313114d6274 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:32:50 -0500 Subject: [PATCH 02/10] PYTTB_UTILS: Fix typing on tt_tenfun --- pyttb/pyttb_utils.py | 193 +++++++++++++++++++++++++------------- pyttb/sumtensor.py | 6 ++ tests/test_pyttb_utils.py | 2 +- 3 files changed, 134 insertions(+), 67 deletions(-) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index 4e13ffd4..b73acb81 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -10,6 +10,7 @@ from inspect import signature from typing import ( Any, + Callable, Iterable, List, Literal, @@ -17,6 +18,7 @@ Sequence, Tuple, Union, + cast, get_args, overload, ) @@ -167,93 +169,135 @@ def tt_dimscheck( return sdims, vidx -# Fixme: Needs types -def tt_tenfun(function_handle, *inputs): # noqa: PLR0912 +def tt_tenfun( + function_handle: Union[ + Callable[[np.ndarray, np.ndarray], np.ndarray], + Callable[[np.ndarray], np.ndarray], + ], + *inputs: Union[ + float, + int, + np.ndarray, + ttb.tensor, + ttb.ktensor, + ttb.ttensor, + ttb.sptensor, + ttb.sumtensor, + ], +) -> ttb.tensor: + """ + Apply a function to each element in a tensor or tensors + + See :meth:`tt_tenfun_binary` and :meth:`tt_tenfun_binary_unary` for supported + options. """ - Apply a function to each element in a tensor - - Parameters - ---------- - function_handle: - callable - inputs: - tensor type, or np.array - - Returns - ------- - :class:`pyttb.tensor` - """ - # Allow inputs to be mutable in case of type conversion - inputs = list(inputs) - if len(inputs) == 0: assert False, "Must provide element(s) to perform operation on" assert callable(function_handle), "function_handle must be callable" + # Number of inputs for function handle + nfunin = len(signature(function_handle).parameters) + + # Case I: Binary function + if len(inputs) == 2 and nfunin == 2: + # We manually inspected the function handle for the parameters + # maybe there is a more clever way to convince mypy + binary_function_handle = cast( + Callable[[np.ndarray, np.ndarray], np.ndarray], function_handle + ) + X = inputs[0] + if not isinstance(X, (int, float)): + X = _tt_to_tensor(X) + Y = inputs[1] + if not isinstance(Y, (int, float)): + Y = _tt_to_tensor(Y) + return tt_tenfun_binary(binary_function_handle, X, Y) + # Convert inputs to tensors if they aren't already - for i, an_input in enumerate(inputs): - if isinstance(an_input, (ttb.tensor, float, int)): - continue - if isinstance(an_input, np.ndarray): - inputs[i] = ttb.tensor(an_input) - elif isinstance( + # Allow inputs to be mutable in case of type conversion + input_tensors: list[Union[ttb.tensor]] = [] + for an_input in inputs: + if not isinstance( an_input, ( + np.ndarray, + ttb.tensor, ttb.ktensor, ttb.ttensor, ttb.sptensor, ttb.sumtensor, - ttb.symtensor, - ttb.symktensor, ), ): - inputs[i] = an_input.to_tensor() - else: - assert False, "Invalid input to ten fun" + assert ( + False + ), f"Invalid input to ten fun: {an_input} of type {type(an_input)}" + input_tensors.append(_tt_to_tensor(an_input)) - # It's ok if there are two input and one is a scalar; otherwise all inputs have to - # be the same size - if ( - (len(inputs) == 2) - and isinstance(inputs[0], (float, int)) - and isinstance(inputs[1], ttb.tensor) - ): - sz = inputs[1].shape - elif ( - (len(inputs) == 2) - and isinstance(inputs[1], (float, int)) - and isinstance(inputs[0], ttb.tensor) - ): - sz = inputs[0].shape + # Case II: Expects input to be matrix and applies operation on each columns + if nfunin != 1: + raise ValueError( + "Tenfun only supports binary and unary function handles but provided " + "function handle takes {nfunin} arguments." + ) + unary_function_handle = cast(Callable[[np.ndarray], np.ndarray], function_handle) + return tt_tenfun_unary(unary_function_handle, *input_tensors) + + +def tt_tenfun_binary( + function_handle: Callable[[np.ndarray, np.ndarray], np.ndarray], + first: Union[ttb.tensor, int, float], + second: Union[ttb.tensor, int, float], +) -> ttb.tensor: + """Apply a binary operation to two tensors or a tensor and a scalar. + + Example + ------- + >>> add = lambda x, y: x + y + >>> t0 = ttb.tenones((2, 2)) + >>> t1 = tt_tenfun_binary(add, t0, t0) + >>> t1.isequal(t0 * 2) + True + >>> t2 = tt_tenfun_binary(add, t0, 1) + >>> t2.isequal(t1) + True + """ + if not isinstance(first, (float, int)): + X = first.data else: - for i, an_input in enumerate(inputs): - if isinstance(an_input, (float, int)): - assert False, f"Argument {i} is a scalar but expected a tensor" - elif i == 0: - sz = an_input.shape - elif sz != an_input.shape: - assert ( - False - ), f"Tensor {i} is not the same size as the first tensor input" + X = np.array(first) + if not isinstance(second, (float, int)): + Y = second.data + else: + Y = np.array(second) - # Number of inputs for function handle - nfunin = len(signature(function_handle).parameters) + data = function_handle(X, Y) + Z = ttb.tensor(data, copy=False) + return Z - # Case I: Binary function - if len(inputs) == 2 and nfunin == 2: - X = inputs[0] - Y = inputs[1] - if not isinstance(X, (float, int)): - X = X.data - if not isinstance(Y, (float, int)): - Y = Y.data - data = function_handle(X, Y) - Z = ttb.tensor(data, copy=False) - return Z +def tt_tenfun_unary( + function_handle: Callable[[np.ndarray], np.ndarray], *inputs: ttb.tensor +) -> ttb.tensor: + """Apply a unary operation to multiple tensors columnwise. - # Case II: Expects input to be matrix and applies operation on each columns + Example + ------- + >>> tensor_max = lambda x: np.max(x, axis=0) + >>> data = np.array([[1, 2, 3], [4, 5, 6]]) + >>> t0 = ttb.tensor(data) + >>> t1 = ttb.tensor(data) + >>> t2 = tt_tenfun_unary(tensor_max, t0, t1) + >>> t2.isequal(t1) + True + """ + for i, an_input in enumerate(inputs): + if isinstance(an_input, (float, int)): + assert False, f"Argument {i} is a scalar but expected a tensor" + elif i == 0: + sz = an_input.shape + elif sz != an_input.shape: + assert False, f"Tensor {i} is not the same size as the first tensor input" if len(inputs) == 1: X = inputs[0].data X = np.reshape(X, (1, -1)) @@ -267,6 +311,23 @@ def tt_tenfun(function_handle, *inputs): # noqa: PLR0912 return Z +def _tt_to_tensor( + some_tensor: Union[ + np.ndarray, + ttb.tensor, + ttb.ktensor, + ttb.ttensor, + ttb.sptensor, + ttb.sumtensor, + ], +) -> ttb.tensor: + if isinstance(some_tensor, np.ndarray): + return ttb.tensor(some_tensor) + elif isinstance(some_tensor, ttb.tensor): + return some_tensor + return some_tensor.to_tensor() + + def tt_setdiff_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: """ Helper function to reproduce functionality of MATLABS setdiff(a,b,'rows') diff --git a/pyttb/sumtensor.py b/pyttb/sumtensor.py index 93e68348..50c4055e 100644 --- a/pyttb/sumtensor.py +++ b/pyttb/sumtensor.py @@ -247,6 +247,12 @@ def __radd__(self, other): """ return self.__add__(other) + def to_tensor(self) -> ttb.tensor: + """ + Same as :meth:`pyttb.sumtensor.full`. + """ + return self.full() + def full(self) -> ttb.tensor: """ Convert a :class:`pyttb.sumtensor` to a :class:`pyttb.tensor`. diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py index 27159893..25689426 100644 --- a/tests/test_pyttb_utils.py +++ b/tests/test_pyttb_utils.py @@ -115,7 +115,7 @@ def tensor_max(x): # Scalar argument not in first two positions with pytest.raises(AssertionError) as excinfo: ttb_utils.tt_tenfun(tensor_max, t1, t1, 1) - assert "Argument 2 is a scalar but expected a tensor" in str(excinfo) + assert "Invalid input to ten fun" in str(excinfo) # Tensors of different sizes with pytest.raises(AssertionError) as excinfo: From 8ef3bc23162c60b1f644515bca3ba2e8de3cb938 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:50:50 -0500 Subject: [PATCH 03/10] PYTTB_UTILS: Doc cleanup * Remove unneeded method * Update examples/docs most places * Mark remaining improvements --- pyttb/pyttb_utils.py | 115 ++++++++++++++++++++++++++++---------- pyttb/sptensor.py | 3 +- tests/test_pyttb_utils.py | 6 -- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index b73acb81..a1120dea 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -102,10 +102,37 @@ def tt_dimscheck( Parameters ---------- + N: Tensor order + M: Num of multiplicands + dims: Dimensions to check + exclude_dims: Check all dimensions but these. (Mutually exclusive with dims) Returns ------- + sdims: New dimensions + vidx: Index into the multiplicands (if M defined). + Examples + -------- + # Default captures all dims and no index + + >>> rdims, _ = tt_dimscheck(6) + >>> np.array_equal(rdims, np.arange(6)) + True + + # Exclude single dim and still no index + + >>> rdims, _ = tt_dimscheck(6, exclude_dims=np.array([5])) + >>> np.array_equal(rdims, np.arange(5)) + True + + # Exclude single dim and number of multiplicands equals resulting size + + >>> rdims, ridx = tt_dimscheck(6, 5, exclude_dims=np.array([0])) + >>> np.array_equal(rdims, np.array([1, 2, 3, 4, 5])) + True + >>> np.array_equal(ridx, np.arange(0, 5)) + True """ if dims is not None and exclude_dims is not None: raise ValueError("Either specify dims to include or exclude, but not both") @@ -321,6 +348,7 @@ def _tt_to_tensor( ttb.sumtensor, ], ) -> ttb.tensor: + """Convert a variety of data structures to a dense tensor.""" if isinstance(some_tensor, np.ndarray): return ttb.tensor(some_tensor) elif isinstance(some_tensor, ttb.tensor): @@ -402,7 +430,7 @@ def tt_irenumber( t: ttb.sptensor, shape: Tuple[int, ...], number_range: Sequence[IndexType] ) -> np.ndarray: """ - RENUMBER indices for sptensor subsasgn + RENUMBER indices for sptensor __setitem__ Parameters ---------- @@ -442,7 +470,7 @@ def tt_renumber( subs: np.ndarray, shape: Tuple[int, ...], number_range: Sequence[IndexType] ) -> Tuple[np.ndarray, Tuple[int, ...]]: """ - RENUMBER indices for sptensor subsref + RENUMBER indices for sptensor __getitem__ [NEWSUBS,NEWSZ] = RENUMBER(SUBS,SZ,RANGE) takes a set of original subscripts SUBS with entries from a tensor of size @@ -453,9 +481,11 @@ def tt_renumber( Parameters ---------- subs: + Original subscripts for source tensor. shape: Shape of source tensor. - range: + number_range: + Key from __getitem__ for tensor. Returns ------- @@ -586,12 +616,20 @@ def tt_ind2sub(shape: Tuple[int, ...], idx: np.ndarray) -> np.ndarray: Parameters ---------- - shape: - idx: + shape: Shape of tensor indexing into. + idx: Array of linear indices into the tensor. Returns ------- - :class:`numpy.ndarray` + Multi-dimensional indices for the tensor. + + Example + ------- + >>> shape = (2, 2, 2) + >>> linear_indices = np.array([0, 1]) + >>> tt_ind2sub(shape, linear_indices) + array([[0, 0, 0], + [1, 0, 0]]) """ if idx.size == 0: return np.empty(shape=(0, len(shape)), dtype=int) @@ -625,23 +663,6 @@ def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: return obj -def tt_intvec2str(v: np.ndarray) -> str: - """ - Print integer vector to a string with brackets. Numpy should already handle this so - it is a placeholder stub - - Parameters - ---------- - v: - Integer vector - - Returns - ------- - Formatted string to print - """ - return np.array2string(v) - - def tt_sub2ind(shape: Tuple[int, ...], subs: np.ndarray) -> np.ndarray: """ Converts multidimensional subscripts to linear indices. @@ -653,9 +674,12 @@ def tt_sub2ind(shape: Tuple[int, ...], subs: np.ndarray) -> np.ndarray: subs: Subscripts for tensor - Returns - ------- - :class:`numpy.ndarray` + Examples + -------- + >>> shape = (2, 2, 2) + >>> full_indices = np.array([[0, 0, 0], [1, 0, 0]], dtype=int) + >>> tt_sub2ind(shape, full_indices) + array([0, 1]) See Also -------- @@ -687,6 +711,13 @@ def tt_sizecheck(shape: Tuple[int, ...], nargout: bool = True) -> bool: ------- bool + Examples + -------- + >>> tt_sizecheck((0, -1, 2)) + False + >>> tt_sizecheck((1, 1, 1)) + True + See Also -------- @@ -729,6 +760,13 @@ def tt_subscheck(subs: np.ndarray, nargout: bool = True) -> bool: ------- bool + Examples + -------- + >>> tt_subscheck(np.array([[2, 2], [3, 3]])) + True + >>> tt_subscheck(np.array([[2, 2], [3, -1]])) + False + See Also -------- @@ -769,6 +807,19 @@ def tt_valscheck(vals: np.ndarray, nargout: bool = True) -> bool: Returns ------- bool + + Examples + -------- + >>> tt_valscheck(np.array([[1], [2]])) + True + >>> tt_valscheck(np.array([[1, 2, 3], [2, 2, 2]])) + False + + See Also + -------- + + :func:`tt_sizecheck`: + :func:`tt_subscheck`: """ if vals.size == 0: ok = True @@ -792,9 +843,12 @@ def isrow(v: np.ndarray) -> bool: v: Vector input - Returns - ------- - bool + Examples + -------- + >>> isrow(np.array([[1, 2]])) + True + >>> isrow(np.array([[1, 2], [3, 4]])) + False """ return v.ndim == 2 and v.shape[0] == 1 and v.shape[1] >= 1 @@ -853,6 +907,7 @@ class IndexVariant(Enum): IndexType = Union[LinearIndexType, Sequence[int], np.ndarray] +# Fixme improve doc string def get_index_variant(indices: IndexType) -> IndexVariant: """Decide on intended indexing variant. No correctness checks.""" variant = IndexVariant.UNKNOWN @@ -875,6 +930,7 @@ def get_index_variant(indices: IndexType) -> IndexVariant: return variant +# Fixme improve doc string def get_mttkrp_factors( U: Union[ttb.ktensor, List[np.ndarray]], n: int, ndims: int ) -> List[np.ndarray]: @@ -899,6 +955,7 @@ def get_mttkrp_factors( return U +# Fixme improve doc string def gather_wrap_dims( ndims: int, rdims: Optional[np.ndarray] = None, diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index 3e5ad2d6..b65eed41 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -38,7 +38,6 @@ tt_dimscheck, tt_ind2sub, tt_intersect_rows, - tt_intvec2str, tt_irenumber, tt_ismember_rows, tt_renumber, @@ -641,7 +640,7 @@ def extract(self, searchsubs: np.ndarray) -> np.ndarray: error_msg = "The following subscripts are invalid: \n" badsubs = searchsubs[badloc, :] for i in np.arange(0, badloc[0].size): - error_msg += f"\tsubscript = {tt_intvec2str(badsubs[i, :])} \n" + error_msg += f"\tsubscript = {np.array2string(badsubs[i, :])} \n" assert False, f"{error_msg}" "Invalid subscripts" # Set the default answer to zero diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py index 25689426..fa1a3f13 100644 --- a/tests/test_pyttb_utils.py +++ b/tests/test_pyttb_utils.py @@ -338,12 +338,6 @@ def test_tt_subsubsref_valid(): assert True -def test_tt_intvec2str_valid(): - """This function is slotted to be removed because it is probably unnecessary in python""" - v = np.array([1, 2, 3]) - assert ttb_utils.tt_intvec2str(v) == "[1 2 3]" - - def test_tt_sizecheck_empty(): assert ttb_utils.tt_sizecheck(()) From 6bd6d88ce366b5b3cc3548ffafba3c9f27656063 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:02:16 -0500 Subject: [PATCH 04/10] PYTTB_UTILS: Doc cleanup * Update doc strings for remaining utils --- pyttb/pyttb_utils.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index a1120dea..a7def9fe 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -907,9 +907,12 @@ class IndexVariant(Enum): IndexType = Union[LinearIndexType, Sequence[int], np.ndarray] -# Fixme improve doc string def get_index_variant(indices: IndexType) -> IndexVariant: - """Decide on intended indexing variant. No correctness checks.""" + """Decide on intended indexing variant. No correctness checks. + + See getitem or setitem in :class:`pyttb.tensor` for elaboration of the + various indexing options. + """ variant = IndexVariant.UNKNOWN if isinstance(indices, get_args(LinearIndexType)): variant = IndexVariant.LINEAR @@ -930,7 +933,6 @@ def get_index_variant(indices: IndexType) -> IndexVariant: return variant -# Fixme improve doc string def get_mttkrp_factors( U: Union[ttb.ktensor, List[np.ndarray]], n: int, ndims: int ) -> List[np.ndarray]: @@ -955,13 +957,42 @@ def get_mttkrp_factors( return U -# Fixme improve doc string def gather_wrap_dims( ndims: int, rdims: Optional[np.ndarray] = None, cdims: Optional[np.ndarray] = None, cdims_cyclic: Optional[Union[Literal["fc"], Literal["bc"], Literal["t"]]] = None, ) -> Tuple[np.ndarray, np.ndarray]: + """Utility to extract tensor modes mapped to rows and columns for matricized tensor. + + Parameters + ---------- + ndims: + Number of dimensions. + rdims: + Mapping of row indices. + cdims: + Mapping of column indices. + cdims_cyclic: + When only rdims is specified maps a single rdim to the rows and + the remaining dimensons span the columns. _fc_ (forward cyclic[1]_) + in the order range(rdims,self.ndims()) followed by range(0, rdims). + _bc_ (backward cyclic[2]_) range(rdims-1, -1, -1) then + range(self.ndims(), rdims, -1). + + Notes + ----- + Forward cyclic is defined by Kiers [1]_ and backward cyclic is defined by + De Lathauwer, De Moor, and Vandewalle [2]_. + + References + ---------- + .. [1] KIERS, H. A. L. 2000. Towards a standardized notation and terminology + in multiway analysis. J. Chemometrics 14, 105-122. + .. [2] DE LATHAUWER, L., DE MOOR, B., AND VANDEWALLE, J. 2000b. On the best + rank-1 and rank-(R1, R2, ... , RN ) approximation of higher-order + tensors. SIAM J. Matrix Anal. Appl. 21, 4, 1324-1342. + """ alldims = np.array([range(ndims)]) if rdims is not None and cdims is None: From 251573dbc6b7751b3d68b643f054c108d94d8425 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:41:58 -0500 Subject: [PATCH 05/10] Make tt_tenfun a tensor method to more closely align to elemfun and MATLAB --- docs/source/matlab/common.rst | 2 +- docs/source/tutorial/algorithm_gcp_opt.ipynb | 1 - docs/source/tutorial/class_tensor.ipynb | 13 +- pyttb/pyttb_utils.py | 163 --------------- pyttb/tensor.py | 197 +++++++++++++++++-- tests/test_pyttb_utils.py | 53 ----- tests/test_tensor.py | 59 ++++++ 7 files changed, 246 insertions(+), 242 deletions(-) diff --git a/docs/source/matlab/common.rst b/docs/source/matlab/common.rst index 0caad94d..1b48f091 100644 --- a/docs/source/matlab/common.rst +++ b/docs/source/matlab/common.rst @@ -56,7 +56,7 @@ Methods +-----------------+----------------------+------------------------------------------------------------------------+ | ``subsref`` | ``__getitem__`` | ``X[index]`` | +-----------------+----------------------+------------------------------------------------------------------------+ -| ``tenfun`` | ``tt_tenfun`` | e.g., ``pyttb.tt_tenfun(lambda x: x + 1, A)`` | +| ``tenfun`` | ``tenfun`` | ``X.tenfun(lambda x: x + 1)`` | +-----------------+----------------------+------------------------------------------------------------------------+ | ``times`` | ``__mul__`` | ``X * Y`` | +-----------------+----------------------+------------------------------------------------------------------------+ diff --git a/docs/source/tutorial/algorithm_gcp_opt.ipynb b/docs/source/tutorial/algorithm_gcp_opt.ipynb index a372fbd2..ee5b5d04 100644 --- a/docs/source/tutorial/algorithm_gcp_opt.ipynb +++ b/docs/source/tutorial/algorithm_gcp_opt.ipynb @@ -158,7 +158,6 @@ "import sys\n", "import pyttb as ttb\n", "import numpy as np\n", - "from pyttb.pyttb_utils import tt_tenfun\n", "\n", "from pyttb.gcp.fg_setup import function_type, setup\n", "from pyttb.gcp.handles import Objectives\n", diff --git a/docs/source/tutorial/class_tensor.ipynb b/docs/source/tutorial/class_tensor.ipynb index 99c003b2..bc1b1fb6 100644 --- a/docs/source/tutorial/class_tensor.ipynb +++ b/docs/source/tutorial/class_tensor.ipynb @@ -27,8 +27,7 @@ "source": [ "import pyttb as ttb\n", "import numpy as np\n", - "import sys\n", - "from pyttb.pyttb_utils import tt_tenfun" + "import sys" ] }, { @@ -847,8 +846,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Using `tt_tenfun` for elementwise operations on one or more `tensor`s\n", - "The function `tt_tenfun` applies a specified function to a number of `tensor`s. This can be used for any function that is not predefined for `tensor`s." + "## Using `tenfun` for elementwise operations on one or more `tensor`s\n", + "The method `tenfun` applies a specified function to a number of `tensor`s. This can be used for any function that is not predefined for `tensor`s." ] }, { @@ -859,7 +858,7 @@ "source": [ "np.random.seed(0)\n", "A = ttb.tensor(np.floor(3 * np.random.rand(2, 2, 3))) # Generate some data.\n", - "tt_tenfun(lambda x: x + 1, A) # Increment every element of A by one." + "A.tenfun(lambda x: x + 1) # Increment every element of A by one." ] }, { @@ -873,7 +872,7 @@ " return np.maximum(a, b)\n", "\n", "\n", - "tt_tenfun(max_elements, A, B) # Max of A and B, elementwise." + "A.tenfun(max_elements, B) # Max of A and B, elementwise." ] }, { @@ -891,7 +890,7 @@ " return np.floor(np.mean(X, axis=0))\n", "\n", "\n", - "tt_tenfun(elementwise_mean, A, B, C) # Elementwise means for A, B, and C." + "A.tenfun(elementwise_mean, B, C) # Elementwise means for A, B, and C." ] }, { diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index a7def9fe..ec234a69 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -7,10 +7,8 @@ from __future__ import annotations from enum import Enum -from inspect import signature from typing import ( Any, - Callable, Iterable, List, Literal, @@ -18,7 +16,6 @@ Sequence, Tuple, Union, - cast, get_args, overload, ) @@ -196,166 +193,6 @@ def tt_dimscheck( return sdims, vidx -def tt_tenfun( - function_handle: Union[ - Callable[[np.ndarray, np.ndarray], np.ndarray], - Callable[[np.ndarray], np.ndarray], - ], - *inputs: Union[ - float, - int, - np.ndarray, - ttb.tensor, - ttb.ktensor, - ttb.ttensor, - ttb.sptensor, - ttb.sumtensor, - ], -) -> ttb.tensor: - """ - Apply a function to each element in a tensor or tensors - - See :meth:`tt_tenfun_binary` and :meth:`tt_tenfun_binary_unary` for supported - options. - """ - if len(inputs) == 0: - assert False, "Must provide element(s) to perform operation on" - - assert callable(function_handle), "function_handle must be callable" - - # Number of inputs for function handle - nfunin = len(signature(function_handle).parameters) - - # Case I: Binary function - if len(inputs) == 2 and nfunin == 2: - # We manually inspected the function handle for the parameters - # maybe there is a more clever way to convince mypy - binary_function_handle = cast( - Callable[[np.ndarray, np.ndarray], np.ndarray], function_handle - ) - X = inputs[0] - if not isinstance(X, (int, float)): - X = _tt_to_tensor(X) - Y = inputs[1] - if not isinstance(Y, (int, float)): - Y = _tt_to_tensor(Y) - return tt_tenfun_binary(binary_function_handle, X, Y) - - # Convert inputs to tensors if they aren't already - # Allow inputs to be mutable in case of type conversion - input_tensors: list[Union[ttb.tensor]] = [] - for an_input in inputs: - if not isinstance( - an_input, - ( - np.ndarray, - ttb.tensor, - ttb.ktensor, - ttb.ttensor, - ttb.sptensor, - ttb.sumtensor, - ), - ): - assert ( - False - ), f"Invalid input to ten fun: {an_input} of type {type(an_input)}" - input_tensors.append(_tt_to_tensor(an_input)) - - # Case II: Expects input to be matrix and applies operation on each columns - if nfunin != 1: - raise ValueError( - "Tenfun only supports binary and unary function handles but provided " - "function handle takes {nfunin} arguments." - ) - unary_function_handle = cast(Callable[[np.ndarray], np.ndarray], function_handle) - return tt_tenfun_unary(unary_function_handle, *input_tensors) - - -def tt_tenfun_binary( - function_handle: Callable[[np.ndarray, np.ndarray], np.ndarray], - first: Union[ttb.tensor, int, float], - second: Union[ttb.tensor, int, float], -) -> ttb.tensor: - """Apply a binary operation to two tensors or a tensor and a scalar. - - Example - ------- - >>> add = lambda x, y: x + y - >>> t0 = ttb.tenones((2, 2)) - >>> t1 = tt_tenfun_binary(add, t0, t0) - >>> t1.isequal(t0 * 2) - True - >>> t2 = tt_tenfun_binary(add, t0, 1) - >>> t2.isequal(t1) - True - """ - if not isinstance(first, (float, int)): - X = first.data - else: - X = np.array(first) - if not isinstance(second, (float, int)): - Y = second.data - else: - Y = np.array(second) - - data = function_handle(X, Y) - Z = ttb.tensor(data, copy=False) - return Z - - -def tt_tenfun_unary( - function_handle: Callable[[np.ndarray], np.ndarray], *inputs: ttb.tensor -) -> ttb.tensor: - """Apply a unary operation to multiple tensors columnwise. - - Example - ------- - >>> tensor_max = lambda x: np.max(x, axis=0) - >>> data = np.array([[1, 2, 3], [4, 5, 6]]) - >>> t0 = ttb.tensor(data) - >>> t1 = ttb.tensor(data) - >>> t2 = tt_tenfun_unary(tensor_max, t0, t1) - >>> t2.isequal(t1) - True - """ - for i, an_input in enumerate(inputs): - if isinstance(an_input, (float, int)): - assert False, f"Argument {i} is a scalar but expected a tensor" - elif i == 0: - sz = an_input.shape - elif sz != an_input.shape: - assert False, f"Tensor {i} is not the same size as the first tensor input" - if len(inputs) == 1: - X = inputs[0].data - X = np.reshape(X, (1, -1)) - else: - X = np.zeros((len(inputs), np.prod(sz))) - for i, an_input in enumerate(inputs): - X[i, :] = np.reshape(an_input.data, (np.prod(sz))) - data = function_handle(X) - data = np.reshape(data, sz) - Z = ttb.tensor(data, copy=False) - return Z - - -def _tt_to_tensor( - some_tensor: Union[ - np.ndarray, - ttb.tensor, - ttb.ktensor, - ttb.ttensor, - ttb.sptensor, - ttb.sumtensor, - ], -) -> ttb.tensor: - """Convert a variety of data structures to a dense tensor.""" - if isinstance(some_tensor, np.ndarray): - return ttb.tensor(some_tensor) - elif isinstance(some_tensor, ttb.tensor): - return some_tensor - return some_tensor.to_tensor() - - def tt_setdiff_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: """ Helper function to reproduce functionality of MATLABS setdiff(a,b,'rows') diff --git a/pyttb/tensor.py b/pyttb/tensor.py index fe729fb7..230b8d1e 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -8,9 +8,10 @@ import logging from collections.abc import Iterable +from inspect import signature from itertools import combinations_with_replacement, permutations from math import factorial -from typing import Any, Callable, List, Literal, Optional, Tuple, Union, overload +from typing import Any, Callable, List, Literal, Optional, Tuple, Union, cast, overload import numpy as np import scipy.sparse.linalg @@ -28,7 +29,6 @@ tt_ind2sub, tt_sub2ind, tt_subsubsref, - tt_tenfun, ) @@ -782,7 +782,7 @@ def logical_and(self, other: Union[float, tensor]) -> tensor: def logical_and(x, y): return np.logical_and(x, y).astype(dtype=x.dtype) - return tt_tenfun(logical_and, self, other) + return self.tenfun(logical_and, other) def logical_not(self) -> tensor: """ @@ -816,7 +816,7 @@ def logical_or(self, other: Union[float, tensor]) -> tensor: def tensor_or(x, y): return np.logical_or(x, y).astype(x.dtype) - return tt_tenfun(tensor_or, self, other) + return self.tenfun(tensor_or, other) def logical_xor(self, other: Union[float, tensor]) -> tensor: """ @@ -837,7 +837,7 @@ def logical_xor(self, other: Union[float, tensor]) -> tensor: def tensor_xor(x, y): return np.logical_xor(x, y).astype(dtype=x.dtype) - return tt_tenfun(tensor_xor, self, other) + return self.tenfun(tensor_xor, other) def mask(self, W: tensor) -> np.ndarray: """ @@ -1756,6 +1756,169 @@ def ttsv( return y assert False, "Invalid value for version; should be None, 1, or 2" + def tenfun( + self, + function_handle: Union[ + Callable[[np.ndarray, np.ndarray], np.ndarray], + Callable[[np.ndarray], np.ndarray], + ], + *inputs: Union[ + float, + int, + np.ndarray, + ttb.tensor, + ttb.ktensor, + ttb.ttensor, + ttb.sptensor, + ttb.sumtensor, + ], + ) -> ttb.tensor: + """ + Apply a function to each element in a tensor or tensors + + See :meth:`pyttb.tensor.tenfun_binary` and + :meth:`pyttb.tensor.tenfun_binary_unary` for supported + options. + """ + assert callable(function_handle), "function_handle must be callable" + + # Number of inputs for function handle + nfunin = len(signature(function_handle).parameters) + + # Case I: Binary function + if len(inputs) == 1 and nfunin == 2: + # We manually inspected the function handle for the parameters + # maybe there is a more clever way to convince mypy + binary_function_handle = cast( + Callable[[np.ndarray, np.ndarray], np.ndarray], function_handle + ) + Y = inputs[0] + if not isinstance(Y, (int, float)): + Y = self._tt_to_tensor(Y) + return self.tenfun_binary(binary_function_handle, Y) + + # Convert inputs to tensors if they aren't already + # Allow inputs to be mutable in case of type conversion + input_tensors: list[Union[ttb.tensor]] = [] + for an_input in inputs: + if not isinstance( + an_input, + ( + np.ndarray, + ttb.tensor, + ttb.ktensor, + ttb.ttensor, + ttb.sptensor, + ttb.sumtensor, + ), + ): + assert ( + False + ), f"Invalid input to ten fun: {an_input} of type {type(an_input)}" + input_tensors.append(self._tt_to_tensor(an_input)) + + # Case II: Expects input to be matrix and applies operation on each columns + if nfunin != 1: + raise ValueError( + "Tenfun only supports binary and unary function handles but provided " + "function handle takes {nfunin} arguments." + ) + unary_function_handle = cast( + Callable[[np.ndarray], np.ndarray], function_handle + ) + return self.tenfun_unary(unary_function_handle, *input_tensors) + + def tenfun_binary( + self, + function_handle: Callable[[np.ndarray, np.ndarray], np.ndarray], + other: Union[ttb.tensor, int, float], + first: bool = True, + ) -> ttb.tensor: + """Apply a binary operation to two tensors or a tensor and a scalar. + + Parameters + ---------- + function_handle: Function to apply. + other: Other input to the binary function. + first: Whether the tensor comes first in the method call (if ordering matters). + + Example + ------- + >>> add = lambda x, y: x + y + >>> t0 = ttb.tenones((2, 2)) + >>> t1 = t0.tenfun_binary(add, t0) + >>> t1.isequal(t0 * 2) + True + >>> t2 = t0.tenfun_binary(add, 1) + >>> t2.isequal(t1) + True + """ + X = self.data + if not isinstance(other, (float, int)): + Y = other.data + else: + Y = np.array(other) + + if not first: + Y, X = X, Y + data = function_handle(X, Y) + Z = ttb.tensor(data, copy=False) + return Z + + def tenfun_unary( + self, function_handle: Callable[[np.ndarray], np.ndarray], *inputs: ttb.tensor + ) -> ttb.tensor: + """Apply a unary operation to multiple tensors columnwise. + + Example + ------- + >>> tensor_max = lambda x: np.max(x, axis=0) + >>> data = np.array([[1, 2, 3], [4, 5, 6]]) + >>> t0 = ttb.tensor(data) + >>> t1 = ttb.tensor(data) + >>> t2 = t0.tenfun_unary(tensor_max, t1) + >>> t2.isequal(t1) + True + """ + sz = self.shape + for i, an_input in enumerate(inputs): + if isinstance(an_input, (float, int)): + assert False, f"Argument {i} is a scalar but expected a tensor" + elif sz != an_input.shape: + assert ( + False + ), f"Tensor {i} is not the same size as the first tensor input" + if len(inputs) == 0: + X = self.data + X = np.reshape(X, (1, -1)) + else: + X = np.zeros((len(inputs) + 1, np.prod(sz))) + X[0, :] = np.reshape(self.data, (np.prod(sz))) + for i, an_input in enumerate(inputs): + X[i + 1, :] = np.reshape(an_input.data, (np.prod(sz))) + data = function_handle(X) + data = np.reshape(data, sz) + Z = ttb.tensor(data, copy=False) + return Z + + def _tt_to_tensor( + self, + some_tensor: Union[ + np.ndarray, + ttb.tensor, + ttb.ktensor, + ttb.ttensor, + ttb.sptensor, + ttb.sumtensor, + ], + ) -> ttb.tensor: + """Convert a variety of data structures to a dense tensor.""" + if isinstance(some_tensor, np.ndarray): + return ttb.tensor(some_tensor) + elif isinstance(some_tensor, ttb.tensor): + return some_tensor + return some_tensor.to_tensor() + def __setitem__(self, key, value): """ Subscripted assignment for a tensor. @@ -2048,7 +2211,7 @@ def __eq__(self, other): def tensor_equality(x, y): return x == y - return tt_tenfun(tensor_equality, self, other) + return self.tenfun(tensor_equality, other) def __ne__(self, other): """ @@ -2080,7 +2243,7 @@ def __ne__(self, other): def tensor_not_equal(x, y): return x != y - return tt_tenfun(tensor_not_equal, self, other) + return self.tenfun(tensor_not_equal, other) def __ge__(self, other): """ @@ -2112,7 +2275,7 @@ def __ge__(self, other): def greater_or_equal(x, y): return x >= y - return tt_tenfun(greater_or_equal, self, other) + return self.tenfun(greater_or_equal, other) def __le__(self, other): """ @@ -2144,7 +2307,7 @@ def __le__(self, other): def less_or_equal(x, y): return x <= y - return tt_tenfun(less_or_equal, self, other) + return self.tenfun(less_or_equal, other) def __gt__(self, other): """ @@ -2176,7 +2339,7 @@ def __gt__(self, other): def greater(x, y): return x > y - return tt_tenfun(greater, self, other) + return self.tenfun(greater, other) def __lt__(self, other): """ @@ -2208,7 +2371,7 @@ def __lt__(self, other): def less(x, y): return x < y - return tt_tenfun(less, self, other) + return self.tenfun(less, other) def __sub__(self, other): """ @@ -2240,7 +2403,7 @@ def __sub__(self, other): def minus(x, y): return x - y - return tt_tenfun(minus, self, other) + return self.tenfun(minus, other) def __add__(self, other): """ @@ -2275,7 +2438,7 @@ def __add__(self, other): def tensor_add(x, y): return x + y - return tt_tenfun(tensor_add, self, other) + return self.tenfun(tensor_add, other) def __radd__(self, other): """ @@ -2325,7 +2488,7 @@ def __pow__(self, power): def tensor_pow(x, y): return x**y - return tt_tenfun(tensor_pow, self, power) + return self.tenfun(tensor_pow, power) def __mul__(self, other): """ @@ -2360,7 +2523,7 @@ def mul(x, y): if isinstance(other, (ttb.ktensor, ttb.sptensor, ttb.ttensor)): other = other.full() - return tt_tenfun(mul, self, other) + return self.tenfun(mul, other) def __rmul__(self, other): """ @@ -2418,7 +2581,7 @@ def div(x, y): with np.errstate(divide="ignore", invalid="ignore"): return x / y - return tt_tenfun(div, self, other) + return self.tenfun(div, other) def __rtruediv__(self, other): """ @@ -2449,7 +2612,7 @@ def div(x, y): with np.errstate(divide="ignore", invalid="ignore"): return x / y - return tt_tenfun(div, other, self) + return self.tenfun_binary(div, other, first=False) def __pos__(self): """ diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py index fa1a3f13..c07999c3 100644 --- a/tests/test_pyttb_utils.py +++ b/tests/test_pyttb_utils.py @@ -75,59 +75,6 @@ def test_tt_dimscheck(): assert "Negative dims" in str(excinfo), f"{str(excinfo)}" -def test_tt_tenfun(): - data = np.array([[1, 2, 3], [4, 5, 6]]) - t1 = ttb.tensor(data) - t2 = ttb.tensor(data) - - # Binary case - def add(x, y): - return x + y - - assert np.array_equal(ttb_utils.tt_tenfun(add, t1, t2).data, 2 * data) - - # Single argument case - def add1(x): - return x + 1 - - assert np.array_equal(ttb_utils.tt_tenfun(add1, t1).data, (data + 1)) - - # Multi argument case - def tensor_max(x): - return np.max(x, axis=0) - - assert np.array_equal(ttb_utils.tt_tenfun(tensor_max, t1, t1, t1).data, data) - # TODO: sptensor arguments, depends on fixing the indexing ordering - - # No np array case - assert np.array_equal(ttb_utils.tt_tenfun(tensor_max, data, data, data).data, data) - - # No argument case - with pytest.raises(AssertionError) as excinfo: - ttb_utils.tt_tenfun(tensor_max) - assert "Must provide element(s) to perform operation on" in str(excinfo) - - # No list case - with pytest.raises(AssertionError) as excinfo: - ttb_utils.tt_tenfun(tensor_max, [1, 2, 3]) - assert "Invalid input to ten fun" in str(excinfo) - - # Scalar argument not in first two positions - with pytest.raises(AssertionError) as excinfo: - ttb_utils.tt_tenfun(tensor_max, t1, t1, 1) - assert "Invalid input to ten fun" in str(excinfo) - - # Tensors of different sizes - with pytest.raises(AssertionError) as excinfo: - ttb_utils.tt_tenfun( - tensor_max, - t1, - t1, - ttb.tensor(np.concatenate((data, np.array([[7, 8, 9]])))), - ) - assert "Tensor 2 is not the same size as the first tensor input" in str(excinfo) - - def test_tt_setdiff_rows(): a = np.array([[4, 6], [1, 9], [2, 6], [2, 6], [99, 0]]) b = np.array( diff --git a/tests/test_tensor.py b/tests/test_tensor.py index 933afdb6..77f581a8 100644 --- a/tests/test_tensor.py +++ b/tests/test_tensor.py @@ -1785,3 +1785,62 @@ def test_mttkrps(): np.allclose(a_direct, an_optimized) for a_direct, an_optimized in zip(direct, optimized) ) + + +def test_tenfun(): + data = np.array([[1, 2, 3], [4, 5, 6]]) + t1 = ttb.tensor(data) + t2 = ttb.tensor(data) + + # Binary case + def add(x, y): + return x + y + + assert np.array_equal(t1.tenfun(add, t2).data, 2 * data) + + # Single argument case + def add1(x): + return x + 1 + + assert np.array_equal(t1.tenfun(add1).data, (data + 1)) + + # Multi argument case + def tensor_max(x): + return np.max(x, axis=0) + + assert np.array_equal(t1.tenfun(tensor_max, t1, t1).data, data) + # TODO: sptensor arguments, depends on fixing the indexing ordering + + # No np array case + assert np.array_equal(t1.tenfun(tensor_max, data, data).data, data) + + # No list case + with pytest.raises(AssertionError) as excinfo: + t1.tenfun(tensor_max, [1, 2, 3]) + assert "Invalid input to ten fun" in str(excinfo) + + # Scalar argument not in first two positions + with pytest.raises(AssertionError) as excinfo: + t1.tenfun(tensor_max, t1, 1) + assert "Invalid input to ten fun" in str(excinfo) + + # Tensors of different sizes + with pytest.raises(AssertionError) as excinfo: + t1.tenfun( + tensor_max, + t1, + ttb.tensor(np.concatenate((data, np.array([[7, 8, 9]])))), + ) + assert "Tensor 1 is not the same size as the first tensor input" in str(excinfo) + + with pytest.raises(ValueError) as excinfo: + + def three_arg_function(x, y, z): + pass + + t1.tenfun(three_arg_function) + assert "only supports binary and unary function handles" in str(excinfo) + + with pytest.raises(AssertionError) as excinfo: + _ = t1.tenfun_unary(add1, 1) + assert "scalar but expected a tensor" in str(excinfo) From 66cbd85246ee70b7248a52bee5386857f8f7e09d Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:10:16 -0500 Subject: [PATCH 06/10] RUFF: Enable Pydocstyle * Mostly adding periods, newlines, and imperitive phrasing --- conftest.py | 7 ++- pyproject.toml | 13 +++-- pyttb/__init__.py | 4 +- pyttb/cp_als.py | 6 +- pyttb/cp_apr.py | 46 +++++----------- pyttb/export_data.py | 20 +++---- pyttb/gcp/__init__.py | 1 + pyttb/gcp/fg.py | 4 +- pyttb/gcp/fg_est.py | 6 +- pyttb/gcp/fg_setup.py | 10 ++-- pyttb/gcp/handles.py | 44 +++++++-------- pyttb/gcp/optimizers.py | 43 ++++++++------- pyttb/gcp/samplers.py | 22 ++++---- pyttb/gcp_opt.py | 4 +- pyttb/hosvd.py | 2 +- pyttb/import_data.py | 17 +++--- pyttb/khatrirao.py | 2 +- pyttb/ktensor.py | 118 ++++++++++++++++++---------------------- pyttb/pyttb_utils.py | 46 ++++++---------- pyttb/sptenmat.py | 29 +++++----- pyttb/sptensor.py | 98 ++++++++++++++++----------------- pyttb/sptensor3.py | 7 +-- pyttb/sumtensor.py | 30 +++++----- pyttb/symktensor.py | 7 +-- pyttb/symtensor.py | 7 +-- pyttb/tenmat.py | 22 ++------ pyttb/tensor.py | 82 +++++++++++----------------- pyttb/ttensor.py | 41 ++++++-------- pyttb/tucker_als.py | 5 +- 29 files changed, 330 insertions(+), 413 deletions(-) diff --git a/conftest.py b/conftest.py index 257cc0c7..8eac1e31 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,4 @@ +"""Pyttb pytest configuration.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the # U.S. Government retains certain rights in this software. @@ -11,12 +12,12 @@ @pytest.fixture(autouse=True) -def add_packages(doctest_namespace): +def add_packages(doctest_namespace): #noqa: D103 doctest_namespace["np"] = numpy doctest_namespace["ttb"] = pyttb -def pytest_addoption(parser): +def pytest_addoption(parser): #noqa: D103 parser.addoption( "--packaging", action="store_true", @@ -26,6 +27,6 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config): #noqa: D103 if not config.option.packaging: config.option.markexpr = "not packaging" diff --git a/pyproject.toml b/pyproject.toml index 590c0a9a..bf5eb0b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ requires = ["setuptools>=61.0", "numpy", "numpy_groupies", "scipy", "wheel"] build-backend = "setuptools.build_meta" [tool.ruff.lint] -select = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B"] +select = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B", "D"] ignore = [ # Ignored in conversion to ruff since not previously enforced "PLR2004", @@ -84,15 +84,18 @@ ignore = [ # There is ongoing discussion about logging/warning etc "B028", ] +[tool.ruff.lint.pydocstyle] +convention = "numpy" + [tool.ruff.lint.per-file-ignores] # See see https://github.com/astral-sh/ruff/issues/3172 for details on this becoming simpler # Everything but I, F (to catch import mess and potential logic errors) -"tests/**.py" = ["E", "PL", "W", "N", "NPY", "RUF", "B"] +"tests/**.py" = ["E", "PL", "W", "N", "NPY", "RUF", "B", "D"] # Ignore everything for now -"docs/**.py" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B"] -"docs/**.ipynb" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B"] -"profiling/**.ipynb" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B"] +"docs/**.py" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B", "D"] +"docs/**.ipynb" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B", "D"] +"profiling/**.ipynb" = ["E", "F", "PL", "W", "I", "N", "NPY", "RUF", "B", "D"] [tool.ruff.format] docstring-code-format = true diff --git a/pyttb/__init__.py b/pyttb/__init__.py index 2a726d6e..6df27a82 100644 --- a/pyttb/__init__.py +++ b/pyttb/__init__.py @@ -1,4 +1,4 @@ -"""pyttb: Python Tensor Toolbox""" +"""pyttb: Python Tensor Toolbox.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -32,7 +32,7 @@ def ignore_warnings(ignore=True): - """Helper to disable warnings""" + """Disable warnings.""" if ignore: warnings.simplefilter("ignore") else: diff --git a/pyttb/cp_als.py b/pyttb/cp_als.py index 45541374..af9af7a1 100644 --- a/pyttb/cp_als.py +++ b/pyttb/cp_als.py @@ -1,4 +1,4 @@ -"""CP Decomposition via Alternating Least Squares""" +"""CP Decomposition via Alternating Least Squares.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -24,8 +24,7 @@ def cp_als( # noqa: PLR0912,PLR0913,PLR0915 printitn: int = 1, fixsigns: bool = True, ) -> Tuple[ttb.ktensor, ttb.ktensor, Dict]: - """ - Compute CP decomposition with alternating least squares + """Compute CP decomposition with alternating least squares. Parameters ---------- @@ -128,7 +127,6 @@ def cp_als( # noqa: PLR0912,PLR0913,PLR0915 Iter 1: f = ... f-delta = ... Final f = ... """ - # Extract number of dimensions and norm of tensor N = input_tensor.ndims normX = input_tensor.norm() diff --git a/pyttb/cp_apr.py b/pyttb/cp_apr.py index 293b60a7..6a445c5a 100644 --- a/pyttb/cp_apr.py +++ b/pyttb/cp_apr.py @@ -1,4 +1,4 @@ -"""Non-negative CP decomposition with alternating Poisson regression""" +"""Non-negative CP decomposition with alternating Poisson regression.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -230,9 +230,6 @@ def tt_cp_apr_mu( # noqa: PLR0912,PLR0913,PLR0915 kappatol: MU ALGORITHM PARAMETER: Tolerance on complementary slackness - Returns - ------- - Notes ----- REFERENCE: E. C. Chi and T. G. Kolda. On Tensors, Sparsity, and @@ -405,9 +402,9 @@ def tt_cp_apr_pdnr( # noqa: PLR0912,PLR0913,PLR0915 precompinds: bool, inexact: bool, ) -> Tuple[ttb.ktensor, Dict]: - """ - Compute nonnegative CP with alternating Poisson regression - computes an estimate of the best rank-R + """Compute nonnegative CP with alternating Poisson regression. + + Computes an estimate of the best rank-R CP model of a tensor X using an alternating Poisson regression. The algorithm solves "row subproblems" in each alternating subproblem, using a Hessian of size R^2. @@ -1272,9 +1269,7 @@ def get_search_dir_pdnr( # noqa: PLR0913 mu: float, epsActSet: float, ) -> Tuple[np.ndarray, np.ndarray]: - """ - Compute the search direction for PDNR using a two-metric projection with - damped Hessian + """Compute the search direction using a two-metric projection with damped Hessian. Parameters ---------- @@ -1372,8 +1367,7 @@ def tt_linesearch_prowsubprob( # noqa: PLR0913 phi_row: np.ndarray, display_warning: bool, ) -> Tuple[np.ndarray, float, float, float, int]: - """ - Perform a line search on a row subproblem + """Perform a line search on a row subproblem. Parameters ---------- @@ -1488,9 +1482,9 @@ def tt_linesearch_prowsubprob( # noqa: PLR0913 def get_hessian( upsilon: np.ndarray, Pi: np.ndarray, free_indices: np.ndarray ) -> np.ndarray: - """ - Return the Hessian for one PDNR row subproblem of Model[n], for just the rows and - columns corresponding to the free variables + """Return the Hessian for one PDNR row subproblem of Model[n]. + + Only for just the rows and columns corresponding to the free variables. Parameters ---------- @@ -1505,7 +1499,6 @@ def get_hessian( Sub-block of full Hessian identified by free-indices """ - num_free = len(free_indices) H = np.zeros((num_free, num_free)) for i in range(num_free): @@ -1523,8 +1516,7 @@ def tt_loglikelihood_row( model_row: np.ndarray, Pi: np.ndarray, ) -> float: - """ - Compute log-likelihood of one row subproblem + """Compute log-likelihood of one row subproblem. Parameters ---------- @@ -1618,7 +1610,6 @@ def get_search_dir_pqnr( # noqa: PLR0913 URL: http://arxiv.org/abs/1304.4964. Submitted for publication. """ - lbfgsSize = delta_model.shape[1] # Determine active and free variables. @@ -1679,8 +1670,7 @@ def calc_grad( data_row: np.ndarray, model_row: np.ndarray, ) -> Tuple[np.ndarray, np.ndarray]: - """ - Compute the gradient for a PQNR row subproblem + """Compute the gradient for a PQNR row subproblem. Parameters ---------- @@ -1709,7 +1699,7 @@ def calc_grad( grad_row = (np.ones(phi_row.shape) - phi_row).transpose() return grad_row, phi_row - +# TODO verify what pi is # Mu helper functions def calculate_pi( Data: Union[ttb.sptensor, ttb.tensor], @@ -1718,9 +1708,7 @@ def calculate_pi( factorIndex: int, ndims: int, ) -> np.ndarray: - """ - Helper function to calculate Pi matrix - # TODO verify what pi is + """Calculate Pi matrix. Parameters ---------- @@ -1758,7 +1746,7 @@ def calculate_phi( # noqa: PLR0913 Pi: np.ndarray, epsilon: float, ) -> np.ndarray: - """ + """Calcualte Phi. Parameters ---------- @@ -1769,9 +1757,6 @@ def calculate_phi( # noqa: PLR0913 Pi: epsilon: - Returns - ------- - """ if isinstance(Data, ttb.sptensor): Phi = -np.ones((Data.shape[factorIndex], rank)) @@ -1846,8 +1831,7 @@ def tt_loglikelihood( def vectorize_for_mu(matrix: np.ndarray) -> np.ndarray: - """ - Helper Function to unravel matrix into vector + """Unravel matrix into vector. Parameters ---------- diff --git a/pyttb/export_data.py b/pyttb/export_data.py index 0f54d61c..56a180d2 100644 --- a/pyttb/export_data.py +++ b/pyttb/export_data.py @@ -1,4 +1,4 @@ -"""Utilities for saving tensor data""" +"""Utilities for saving tensor data.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -19,9 +19,7 @@ def export_data( fmt_data: Optional[str] = None, fmt_weights: Optional[str] = None, ): - """ - Export tensor-related data to a file. - """ + """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)}" @@ -57,19 +55,19 @@ def export_data( def export_size(fp: TextIO, shape: Tuple[int, ...]): - """Export the size of something to a file""" + """Export the size of something to a file.""" print(f"{len(shape)}", file=fp) # # of dimensions on one line shape_str = " ".join([str(d) for d in shape]) print(f"{shape_str}", file=fp) # size of each dimensions on the next line def export_rank(fp: TextIO, data: ttb.ktensor): - """Export the rank of a ktensor to a file""" + """Export the rank of a ktensor to a file.""" print(f"{len(data.weights)}", file=fp) # ktensor rank on one line def export_weights(fp: TextIO, data: ttb.ktensor, fmt_weights: Optional[str]): - """Export KTensor weights""" + """Export KTensor weights.""" if not fmt_weights: fmt_weights = "%.16e" data.weights.tofile(fp, sep=" ", format=fmt_weights) @@ -77,7 +75,7 @@ def export_weights(fp: TextIO, data: ttb.ktensor, fmt_weights: Optional[str]): def export_array(fp: TextIO, data: np.ndarray, fmt_data: Optional[str]): - """Export dense data""" + """Export dense data.""" if not fmt_data: fmt_data = "%.16e" data.tofile(fp, sep="\n", format=fmt_data) @@ -85,7 +83,7 @@ def export_array(fp: TextIO, data: np.ndarray, fmt_data: Optional[str]): def export_factor(fp: TextIO, data: np.ndarray, fmt_data: Optional[str]): - """Export KTensor factor""" + """Export KTensor factor.""" if not fmt_data: fmt_data = "%.16e" for i in range(data.shape[0]): @@ -95,7 +93,7 @@ def export_factor(fp: TextIO, data: np.ndarray, fmt_data: Optional[str]): def export_sparse_size(fp: TextIO, A: ttb.sptensor): - """Export the size of something to a file""" + """Export the size of something to a file.""" print(f"{len(A.shape)}", file=fp) # # of dimensions on one line shape_str = " ".join([str(d) for d in A.shape]) print(f"{shape_str}", file=fp) # size of each dimensions on the next line @@ -103,7 +101,7 @@ def export_sparse_size(fp: TextIO, A: ttb.sptensor): def export_sparse_array(fp: TextIO, A: ttb.sptensor, fmt_data: Optional[str]): - """Export sparse array data in coordinate format""" + """Export sparse array data in coordinate format.""" if not fmt_data: fmt_data = "%.16e" # TODO: looping through all values may take a long time, can this be more efficient? diff --git a/pyttb/gcp/__init__.py b/pyttb/gcp/__init__.py index e69de29b..00d2f2c7 100644 --- a/pyttb/gcp/__init__.py +++ b/pyttb/gcp/__init__.py @@ -0,0 +1 @@ +"""Generalized CP Decomposition Support Code.""" diff --git a/pyttb/gcp/fg.py b/pyttb/gcp/fg.py index a303b3aa..f525837e 100644 --- a/pyttb/gcp/fg.py +++ b/pyttb/gcp/fg.py @@ -1,4 +1,4 @@ -"""Evaluate Function And Gradient Handles""" +"""Evaluate Function And Gradient Handles.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -51,7 +51,7 @@ def evaluate( function_handle: Optional[function_type] = None, gradient_handle: Optional[function_type] = None, ) -> Union[float, List[np.ndarray], Tuple[float, List[np.ndarray]]]: - """Evaluate an objective function and/or gradient function + """Evaluate an objective function and/or gradient function. Parameters ---------- diff --git a/pyttb/gcp/fg_est.py b/pyttb/gcp/fg_est.py index 32ccd008..8a754adc 100644 --- a/pyttb/gcp/fg_est.py +++ b/pyttb/gcp/fg_est.py @@ -1,4 +1,4 @@ -"""Evaluate Functions And Gradients based on Subsamples""" +"""Evaluate Functions And Gradients based on Subsamples.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -65,7 +65,7 @@ def estimate( # noqa: PLR0913 lambda_check: bool = True, crng: Optional[np.ndarray] = None, ) -> Union[float, List[np.ndarray], Tuple[float, List[np.ndarray]]]: - """Estimate the GCP function and gradient with a subsample + """Estimate the GCP function and gradient with a subsample. Parameters ---------- @@ -142,7 +142,7 @@ def estimate( # noqa: PLR0913 def estimate_helper( factors: List[np.ndarray], subs: np.ndarray ) -> Tuple[np.ndarray, List[np.ndarray]]: - """Extract model values at sample locations and exploded Zk's + """Extract model values at sample locations and exploded Zk's. Parameters ---------- diff --git a/pyttb/gcp/fg_setup.py b/pyttb/gcp/fg_setup.py index 62a33247..333fad8e 100644 --- a/pyttb/gcp/fg_setup.py +++ b/pyttb/gcp/fg_setup.py @@ -1,4 +1,4 @@ -"""Prepare Function and Gradient Handles for GCP OPT""" +"""Prepare Function and Gradient Handles for GCP OPT.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -24,7 +24,7 @@ def setup( # noqa: PLR0912,PLR0915 data: Optional[Union[ttb.tensor, ttb.sptensor]] = None, additional_parameter: Optional[float] = None, ) -> fg_return: - """Collects the function and gradient handles for GCP + """Collect the function and gradient handles for GCP. Parameters ---------- @@ -116,21 +116,21 @@ def setup( # noqa: PLR0912,PLR0915 def valid_nonneg(data: Union[ttb.tensor, ttb.sptensor]) -> bool: - """Check if provided data is valid non-negative tensor""" + """Check if provided data is valid non-negative tensor.""" if isinstance(data, ttb.sptensor): return bool(np.all(data.vals > 0)) return bool(np.all(data.data > 0)) def valid_binary(data: Union[ttb.tensor, ttb.sptensor]) -> bool: - """Check if provided data is valid binary tensor""" + """Check if provided data is valid binary tensor.""" if isinstance(data, ttb.sptensor): return bool(np.all(data.vals == 1)) return bool(np.all(np.isin(np.unique(data.data), [0, 1]))) def valid_natural(data: Union[ttb.tensor, ttb.sptensor]) -> bool: - """Check if provided data is valid natural number tensor""" + """Check if provided data is valid natural number tensor.""" if isinstance(data, ttb.sptensor): vals = data.vals else: diff --git a/pyttb/gcp/handles.py b/pyttb/gcp/handles.py index 33270552..dc55cc9a 100644 --- a/pyttb/gcp/handles.py +++ b/pyttb/gcp/handles.py @@ -1,4 +1,4 @@ -"""Implementation of the different function and gradient handles for GCP OPT""" +"""Implementation of the different function and gradient handles for GCP OPT.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -17,7 +17,7 @@ class Objectives(Enum): - """Valid objective functions for GCP""" + """Valid objective functions for GCP.""" GAUSSIAN = 0 BERNOULLI_ODDS = 1 @@ -32,77 +32,77 @@ class Objectives(Enum): def gaussian(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for gaussian distributions""" + """Return objective function for gaussian distributions.""" return (model - data) ** 2 def gaussian_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for gaussian distributions""" + """Return gradient function for gaussian distributions.""" return 2 * (model - data) def bernoulli_odds(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for bernoulli distributions""" + """Return objective function for bernoulli distributions.""" return np.log(model + 1) - data * np.log(model + EPS) def bernoulli_odds_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for bernoulli distributions""" + """Return gradient function for bernoulli distributions.""" return 1.0 / (model + 1) - data / (model + EPS) def bernoulli_logit(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for bernoulli logit distributions""" + """Return objective function for bernoulli logit distributions.""" return np.log(np.exp(model) + 1) - data * model def bernoulli_logit_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for bernoulli logit distributions""" + """Return gradient function for bernoulli logit distributions.""" return np.exp(model) / (np.exp(model) + 1) - data def poisson(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for poisson distributions""" + """Return objective function for poisson distributions.""" return model - data * np.log(model + EPS) def poisson_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for poisson distributions""" + """Return gradient function for poisson distributions.""" return 1 - data / (model + EPS) def poisson_log(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for log poisson distributions""" + """Return objective function for log poisson distributions.""" return np.exp(model) - data * model def poisson_log_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for log poisson distributions""" + """Return gradient function for log poisson distributions.""" return np.exp(model) - data def rayleigh(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for rayleigh distributions""" + """Return objective function for rayleigh distributions.""" return 2 * np.log(model + EPS) + (np.pi / 4) * (data / (model + EPS)) ** 2 def rayleigh_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for rayleigh distributions""" + """Return gradient function for rayleigh distributions.""" return 2 / (model + EPS) - (np.pi / 2) * data**2 / (model + EPS) ** 3 def gamma(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return objective function for gamma distributions""" + """Return objective function for gamma distributions.""" return data / (model + EPS) + np.log(model + EPS) def gamma_grad(data: np.ndarray, model: np.ndarray) -> np.ndarray: - """Return gradient function for gamma distributions""" + """Return gradient function for gamma distributions.""" return -data / (model + EPS) ** 2 + 1 / (model + EPS) def huber(data: ttb.tensor, model: ttb.tensor, threshold: float) -> np.ndarray: - """Return objective function for huber loss""" + """Return objective function for huber loss.""" abs_diff = np.abs(data - model) below_threshold = abs_diff < threshold return abs_diff**2 * below_threshold + ( @@ -111,7 +111,7 @@ def huber(data: ttb.tensor, model: ttb.tensor, threshold: float) -> np.ndarray: def huber_grad(data: ttb.tensor, model: ttb.tensor, threshold: float) -> np.ndarray: - """Return gradient function for huber loss""" + """Return gradient function for huber loss.""" abs_diff = np.abs(data - model) below_threshold = abs_diff < threshold return -2 * (data - model) * below_threshold - ( @@ -124,24 +124,24 @@ def huber_grad(data: ttb.tensor, model: ttb.tensor, threshold: float) -> np.ndar def negative_binomial( data: np.ndarray, model: np.ndarray, num_trials: float ) -> np.ndarray: - """Return objective function for negative binomial distributions""" + """Return objective function for negative binomial distributions.""" return (num_trials + data) * np.log(model + 1) - data * np.log(model + EPS) def negative_binomial_grad( data: np.ndarray, model: np.ndarray, num_trials: float ) -> np.ndarray: - """Return gradient function for negative binomial distributions""" + """Return gradient function for negative binomial distributions.""" return (num_trials + 1) / (1 + model) - data / (model + EPS) def beta(data: np.ndarray, model: np.ndarray, b: float) -> np.ndarray: - """Return objective function for beta distributions""" + """Return objective function for beta distributions.""" return (1 / b) * (model + EPS) ** b - (1 / (b - 1)) * data * (model + EPS) ** ( b - 1 ) def beta_grad(data: np.ndarray, model: np.ndarray, b: float) -> np.ndarray: - """Return gradient function for beta distributions""" + """Return gradient function for beta distributions.""" return (model + EPS) ** (b - 1) - data * (model + EPS) ** (b - 2) diff --git a/pyttb/gcp/optimizers.py b/pyttb/gcp/optimizers.py index ce5acac2..d2be3351 100644 --- a/pyttb/gcp/optimizers.py +++ b/pyttb/gcp/optimizers.py @@ -1,4 +1,4 @@ -"""Optimizer Implementations for GCP""" +"""Optimizer Implementations for GCP.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -23,7 +23,7 @@ class StochasticSolver(ABC): - """Interface for Stochastic GCP Solvers""" + """Interface for Stochastic GCP Solvers.""" def __init__( # noqa: PLR0913 self, @@ -35,7 +35,7 @@ def __init__( # noqa: PLR0913 max_iters: int = 1000, printitn: int = 1, ): - """General Setup for Stochastic Solvers + """General Setup for Stochastic Solvers. Parameters ---------- @@ -70,7 +70,7 @@ def update_step( gradient: List[np.ndarray], lower_bound: float, ) -> Tuple[List[np.ndarray], float]: - """Calculates the update step for the solver + """Calculate the update step for the solver. Parameters ---------- @@ -89,7 +89,7 @@ def update_step( @abstractmethod def set_failed_epoch(self): - """Set internal state on failed epoch""" + """Set internal state on failed epoch.""" def solve( # noqa: PLR0913 self, @@ -100,7 +100,7 @@ def solve( # noqa: PLR0913 lower_bound: float = -np.inf, sampler: Optional[GCPSampler] = None, ) -> Tuple[ttb.ktensor, Dict]: - """Run solver until completion + """Run solver until completion. Parameters ---------- @@ -242,9 +242,9 @@ def solve( # noqa: PLR0913 class SGD(StochasticSolver): - """General Stochastic Gradient Descent""" + """General Stochastic Gradient Descent.""" - def update_step( + def update_step( #noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: step = self._decay**self._nfails * self._rate @@ -254,13 +254,13 @@ def update_step( ] return factor_matrices, step - def set_failed_epoch(self): + def set_failed_epoch(self): #noqa: D102 # No additional internal state for SGD pass class Adam(StochasticSolver): - """Adam Optimizer""" + """Adam Optimizer.""" def __init__( # noqa: PLR0913 self, @@ -275,7 +275,7 @@ def __init__( # noqa: PLR0913 beta_2: float = 0.999, epsilon: float = 1e-8, ): - """General Setup for Adam Solver + """General Setup for Adam Solver. Parameters ---------- @@ -318,14 +318,14 @@ def __init__( # noqa: PLR0913 self._v: List[np.ndarray] = [] self._v_prev: List[np.ndarray] = [] - def set_failed_epoch( + def set_failed_epoch( #noqa: D102 self, ): self._total_iterations -= self._epoch_iters self._m = self._m_prev.copy() self._v = self._v_prev.copy() - def update_step( + def update_step( #noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: if self._total_iterations == 0: @@ -364,7 +364,7 @@ def update_step( class Adagrad(StochasticSolver): - """Adagrad Optimizer""" + """Adagrad Optimizer.""" def __init__( # noqa: PLR0913 self, @@ -387,12 +387,12 @@ def __init__( # noqa: PLR0913 ) self._gnormsum = 0.0 - def set_failed_epoch( + def set_failed_epoch( #noqa: D102 self, ): self._gnormsum = 0.0 - def update_step( + def update_step( #noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: self._gnormsum += np.sum([np.sum(gk**2) for gk in gradient]) @@ -406,7 +406,7 @@ def update_step( # If we use more scipy optimizers in the future we should generalize this class LBFGSB: - """Simple wrapper around scipy lbfgsb + """Simple wrapper around scipy lbfgsb. NOTE: If used for publications please see scipy documentation for adding citation for the implementation. @@ -425,7 +425,7 @@ def __init__( # noqa: PLR0913 callback: Optional[Callable[[np.ndarray], None]] = None, maxls: Optional[int] = None, ): - """Setup all hyper-parameters for solver. + """Prepare all hyper-parameters for solver. See scipy for details and standard defaults. A variety of defaults are set specifically for gcp opt. @@ -475,7 +475,7 @@ def solve( # noqa: PLR0913 lower_bound: float = -np.inf, mask: Optional[np.ndarray] = None, ) -> Tuple[ttb.ktensor, Dict]: - """Solves the defined optimization problem""" + """Solves the defined optimization problem.""" model = initial_model.copy() def lbfgsb_func_grad(vector: np.ndarray): @@ -519,6 +519,8 @@ def lbfgsb_func_grad(vector: np.ndarray): return model, lbfgsb_info class Monitor(dict): + """Monitor LBFGSB Timings.""" + def __init__( self, maxiter: int, @@ -530,6 +532,7 @@ def __init__( self._callback = callback def __call__(self, xk: np.ndarray) -> None: + """Update monitor.""" if self._callback is not None: self._callback(xk) self.time_trace[self.iter] = time.perf_counter() - self.startTime @@ -537,10 +540,12 @@ def __call__(self, xk: np.ndarray) -> None: @property def callback(self): + """Return stored callback.""" return self._callback @property def __dict__(self): + """Monitor Entries.""" if not self._callback: return {"time_trace": self.time_trace} else: diff --git a/pyttb/gcp/samplers.py b/pyttb/gcp/samplers.py index 8af8e058..97ec27cc 100644 --- a/pyttb/gcp/samplers.py +++ b/pyttb/gcp/samplers.py @@ -1,4 +1,4 @@ -"""Implementation of various sampling approaches for GCP OPT""" +"""Implementation of various sampling approaches for GCP OPT.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -26,14 +26,14 @@ @dataclass class StratifiedCount: - """Contains stratified sampling counts""" + """Contains stratified sampling counts.""" num_zeros: int num_nonzeros: int class Samplers(Enum): - """Implemented Samplers""" + """Implemented Samplers.""" UNIFORM = 0 SEMISTRATIFIED = 1 @@ -41,7 +41,7 @@ class Samplers(Enum): class GCPSampler: - """Contains Gradient and Function Sampling Details""" + """Contains Gradient and Function Sampling Details.""" def __init__( # noqa: PLR0913 self, @@ -237,16 +237,16 @@ def _prepare_gradient_sampler( # noqa: PLR0912,PLR0913 raise ValueError("Invalid choice for function_sampler") def function_sample(self, data: Union[ttb.tensor, ttb.sptensor]) -> sample_type: - """Draw a sample from the objective function""" + """Draw a sample from the objective function.""" return self._fsampler(data) def gradient_sample(self, data: Union[ttb.tensor, ttb.sptensor]) -> sample_type: - """Draw a sample from the gradient function""" + """Draw a sample from the gradient function.""" return self._gsampler(data) @property def crng(self) -> np.ndarray: - """Correction Range for possibly miss-sampled zeros""" + """Correction Range for possibly miss-sampled zeros.""" return self._crng @@ -289,7 +289,7 @@ def zeros( over_sample_rate: float = 1.1, with_replacement=True, ) -> np.ndarray: - """Samples zeros from a sparse tensor + """Sample zeros from a sparse tensor. Parameters ---------- @@ -372,7 +372,7 @@ def zeros( def uniform(data: ttb.tensor, samples: int) -> sample_type: - """Uniformly samples indices from a tensor + """Uniformly samples indices from a tensor. Parameters ---------- @@ -397,7 +397,7 @@ def uniform(data: ttb.tensor, samples: int) -> sample_type: def semistrat(data: ttb.sptensor, num_nonzeros: int, num_zeros: int) -> sample_type: - """Sample nonzero and zero entries from a sparse tensor + """Sample nonzero and zero entries from a sparse tensor. Parameters ---------- @@ -435,7 +435,7 @@ def stratified( num_zeros: int, over_sample_rate: float = 1.1, ) -> sample_type: - """Sample nonzero and zero entries from a sparse tensor + """Sample nonzero and zero entries from a sparse tensor. Parameters ---------- diff --git a/pyttb/gcp_opt.py b/pyttb/gcp_opt.py index 1ac112a9..7666ff1b 100644 --- a/pyttb/gcp_opt.py +++ b/pyttb/gcp_opt.py @@ -1,4 +1,4 @@ -"""Generalized CP Decomposition""" +"""Generalized CP Decomposition.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -136,7 +136,7 @@ def _get_initial_guess( rank: int, init: Union[Literal["random"], ttb.ktensor, List[np.ndarray]], ) -> ttb.ktensor: - """Get initial guess for gcp_opt + """Get initial guess for gcp_opt. Returns ------- diff --git a/pyttb/hosvd.py b/pyttb/hosvd.py index c26bca06..5a40921c 100644 --- a/pyttb/hosvd.py +++ b/pyttb/hosvd.py @@ -1,4 +1,4 @@ -"""Higher Order SVD Implementation""" +"""Higher Order SVD Implementation.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the diff --git a/pyttb/import_data.py b/pyttb/import_data.py index d19c2e57..6008c2f0 100644 --- a/pyttb/import_data.py +++ b/pyttb/import_data.py @@ -1,4 +1,4 @@ -"""Utilities for importing tensor data""" +"""Utilities for importing tensor data.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -17,8 +17,7 @@ def import_data( filename: str, index_base: int = 1 ) -> Union[ttb.sptensor, ttb.ktensor, ttb.tensor, np.ndarray]: - """ - Import tensor data + """Import tensor data. Parameters ---------- @@ -73,12 +72,12 @@ def import_data( def import_type(fp: TextIO) -> str: - """Extract IO data type""" + """Extract IO data type.""" return fp.readline().strip().split(" ")[0] def import_shape(fp: TextIO) -> Tuple[int, ...]: - """Extract the shape of something from a file""" + """Extract the shape of something from a file.""" n = int(fp.readline().strip().split(" ")[0]) shape = [int(d) for d in fp.readline().strip().split(" ")] if len(shape) != n: @@ -87,19 +86,19 @@ def import_shape(fp: TextIO) -> Tuple[int, ...]: def import_nnz(fp: TextIO) -> int: - """Extract the number of non-zeros of something from a file""" + """Extract the number of non-zeros of something from a file.""" return int(fp.readline().strip().split(" ")[0]) def import_rank(fp: TextIO) -> int: - """Extract the rank of something from a file""" + """Extract the rank of something from a file.""" return int(fp.readline().strip().split(" ")[0]) def import_sparse_array( fp: TextIO, n: int, nz: int, index_base: int = 1 ) -> Tuple[np.ndarray, np.ndarray]: - """Extract sparse data subs and vals from coordinate format data""" + """Extract sparse data subs and vals from coordinate format data.""" subs = np.zeros((nz, n), dtype="int64") vals = np.zeros((nz, 1)) for k in range(nz): @@ -110,5 +109,5 @@ def import_sparse_array( def import_array(fp: TextIO, n: Union[int, np.integer]) -> np.ndarray: - """Extract numpy array from file""" + """Extract numpy array from file.""" return np.fromfile(fp, count=n, sep=" ") diff --git a/pyttb/khatrirao.py b/pyttb/khatrirao.py index e5f718e0..2d55e56c 100644 --- a/pyttb/khatrirao.py +++ b/pyttb/khatrirao.py @@ -1,4 +1,4 @@ -"""Khatri-Rao Product Implementation""" +"""Khatri-Rao Product Implementation.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the diff --git a/pyttb/ktensor.py b/pyttb/ktensor.py index 5ae1f05c..bc5ed895 100644 --- a/pyttb/ktensor.py +++ b/pyttb/ktensor.py @@ -75,8 +75,9 @@ def __init__( weights: Optional[np.ndarray] = None, copy: bool = True, ): - """ - Create a :class:`pyttb.ktensor` in one of the following ways: + """Create a :class:`pyttb.ktensor`. + + Created in one of the following ways: - With no inputs (or `weights` and `factor_matrices` both None), return an empty :class:`pyttb.ktensor`. - Otherwise, return a :class:`pyttb.ktensor` with `weights` and @@ -135,7 +136,6 @@ def __init__( [[5. 6.] [7. 8.]] """ - # Cannot specify weights and not factor_matrices if factor_matrices is None and weights is not None: assert False, "factor_matrices cannot be None if weights are provided." @@ -198,8 +198,9 @@ def from_function( shape: Tuple[int, ...], num_components: int, ): - """ - Construct a :class:`pyttb.ktensor` whose factor matrix entries are + """Construct a :class:`pyttb.ktensor`. + + Factor matrix entries are set using a function. The weights of the returned :class:`pyttb.ktensor` will all be equal to 1. @@ -297,8 +298,9 @@ def from_function( def from_vector( cls, data: np.ndarray, shape: Tuple[int, ...], contains_weights: bool ): - """ - Construct a :class:`pyttb.ktensor` from a vector and shape. The rank of the + """Construct a :class:`pyttb.ktensor` from a vector and shape. + + The rank of the :class:`pyttb.ktensor` is inferred from the shape and length of the vector. Parameters @@ -411,8 +413,8 @@ def arrange( weight_factor: Optional[int] = None, permutation: Optional[Union[Tuple, List, np.ndarray]] = None, ): - """ - Arrange the rank-1 components of a :class:`pyttb.ktensor` in place. + """Arrange the rank-1 components of a :class:`pyttb.ktensor` in place. + If `permutation` is passed, the columns of `self.factor_matrices` are arranged using the provided permutation, so you must make a copy before calling this method if you want to store the original @@ -601,6 +603,7 @@ def copy(self) -> ktensor: return ttb.ktensor(self.factor_matrices, self.weights, copy=True) def __deepcopy__(self, memo): + """Return deep copy of ktensor.""" return self.copy() def double(self) -> np.ndarray: @@ -629,9 +632,7 @@ def double(self) -> np.ndarray: def extract( self, idx: Optional[Union[int, tuple, list, np.ndarray]] = None ) -> ktensor: - """ - Creates a new :class:`pyttb.ktensor` with only the specified - components. + """Create a new :class:`pyttb.ktensor` with only the specified components. Parameters ---------- @@ -711,8 +712,9 @@ def extract( assert False, "Input parameter must be an int, tuple, list or numpy.ndarray" def fixsigns(self, other: Optional[ktensor] = None) -> ktensor: # noqa: PLR0912 - """ - Change the elements of a :class:`pyttb.ktensor` in place so that the + """Change the elements of a :class:`pyttb.ktensor` in place. + + Update so that the largest magnitude entries for each column vector in each factor matrix are positive, provided that the sign on pairs of vectors in a rank-1 component can be flipped. @@ -857,8 +859,9 @@ def fixsigns(self, other: Optional[ktensor] = None) -> ktensor: # noqa: PLR0912 return self def to_tensor(self) -> ttb.tensor: - """Convenience method to convert to tensor. - Same as :meth:`pyttb.ktensor.full` + """Convert to tensor. + + Same as :meth:`pyttb.ktensor.full`. """ return self.full() @@ -894,10 +897,11 @@ def full(self) -> ttb.tensor: """ def min_split_dims(dims): - """ - solve + """Return Minimum split dimensions. + + Solve min_{i in range(1,d)} product(dims[:i]) + product(dims[i:]) - to minimize the memory footprint of the intermediate matrix + to minimize the memory footprint of the intermediate matrix. """ sum_of_prods = [ np.prod(dims[:i]) + np.prod(dims[i:]) for i in range(1, len(dims)) @@ -920,9 +924,7 @@ def to_tenmat( ] = None, copy: bool = True, ) -> ttb.tenmat: - """ - Construct a :class:`pyttb.tenmat` from a :class:`pyttb.ktensor` and - unwrapping details. + """Construct a :class:`pyttb.tenmat` from a :class:`pyttb.ktensor`. Parameters ---------- @@ -1074,9 +1076,7 @@ def issymmetric( def issymmetric( self, return_diffs: bool = False ) -> Union[bool, Tuple[bool, np.ndarray]]: - """ - Returns True if the :class:`pyttb.ktensor` is exactly symmetric for - every permutation. + """Return True if :class:`pyttb.ktensor` is symmetric for every permutation. Parameters ---------- @@ -1128,8 +1128,9 @@ def issymmetric( return issym def mask(self, W: Union[ttb.tensor, ttb.sptensor]) -> np.ndarray: - """ - Extract :class:`pyttb.ktensor` values as specified by `W`, a + """Extract :class:`pyttb.ktensor` values as specified by `W`. + + `W` is a :class:`pyttb.tensor` or :class:`pyttb.sptensor` containing only values of zeros (0) and ones (1). The values in the :class:`pyttb.ktensor` corresponding to the indices for the @@ -1227,9 +1228,7 @@ def mttkrp(self, U: Union[ktensor, List[np.ndarray]], n: int) -> np.ndarray: @property def ncomponents(self) -> int: - """ - Number of components in the :class:`pyttb.ktensor` (i.e., number of - columns in each factor matrix) of the :class:`pyttb.ktensor`. + """Number of columns in each factor matrix for the :class:`pyttb.ktensor`. Examples -------- @@ -1241,9 +1240,7 @@ def ncomponents(self) -> int: @property def ndims(self) -> int: - """ - Number of dimensions (i.e., number of factor matrices) of the - :class:`pyttb.ktensor`. + """Number of dimensions of the :class:`pyttb.ktensor`. Examples -------- @@ -1254,9 +1251,10 @@ def ndims(self) -> int: return len(self.factor_matrices) def norm(self) -> float: - """ - Compute the norm (i.e., square root of the sum of squares of entries) - of a :class:`pyttb.ktensor`. + """Compute the norm of a :class:`pyttb.ktensor`. + + Frobenius norm, or square root of the sum of + squares of entries. Examples -------- @@ -1277,10 +1275,9 @@ def normalize( normtype: float = 2, mode: Optional[int] = None, ) -> ktensor: - """ - Normalize the columns of the factor matrices of a - :class:`pyttb.ktensor` in place, then optionally - absorb the weights into desired normalized factors. + """Normalize the columns of the factor matrices in place. + + Optionally absorb the weights into desired normalized factors. Parameters ---------- @@ -1498,8 +1495,8 @@ def permute(self, order: np.ndarray) -> ktensor: return ttb.ktensor([self.factor_matrices[i] for i in order], self.weights) def redistribute(self, mode: int) -> ktensor: - """ - Distribute weights of a :class:`pyttb.ktensor` to the specified mode. + """Distribute weights of a :class:`pyttb.ktensor` to the specified mode. + The redistribution is performed in place. Parameters @@ -1565,10 +1562,7 @@ def score( threshold: Optional[float] = None, greedy: bool = True, ) -> Tuple[float, ktensor, bool, np.ndarray]: - """ - Checks if two :class:`pyttb.ktensor` instances with the same shapes - but potentially different number of components match except for - permutation. + """Check if two :class:`pyttb.ktensor` with the same shape match. Matching is defined as follows. If `self` and `other` are single- component :class:`pyttb.ktensor` instances that have been normalized @@ -1641,7 +1635,6 @@ def score( >>> print(perm) [0 1 2] """ - assert ( greedy ), "Not yet implemented. Only greedy method is implemented currently." @@ -1797,9 +1790,10 @@ def symmetrize(self) -> ktensor: return ttb.ktensor([V.copy() for i in range(K.ndims)], weights) def tolist(self, mode: Optional[int] = None) -> List[np.ndarray]: - """ - Convert :class:`pyttb.ktensor` to a list of factor matrices, evenly - distributing the weights across factors. Optionally absorb the + """Convert :class:`pyttb.ktensor` to a list of factor matrices. + + Eevenly + distributes the weights across factors. Optionally absorb the weights into a single mode. Parameters @@ -1871,8 +1865,9 @@ def tolist(self, mode: Optional[int] = None) -> List[np.ndarray]: return factor_matrices def tovec(self, include_weights: bool = True) -> np.ndarray: - """ - Convert :class:`pyttb.ktensor` to column vector. Optionally include + """Convert :class:`pyttb.ktensor` to column vector. + + Optionally include or exclude the weights. The output of this method can be consumed by :meth:`from_vector`. @@ -2000,7 +1995,7 @@ def ttv( input. If k == n, a scalar is returned. Examples - ------- + -------- Compute the product of a :class:`pyttb.ktensor` and a single vector (results in a :class:`pyttb.ktensor`): @@ -2044,7 +2039,6 @@ def ttv( [[1. 3.] [2. 4.]] """ - if dims is None and exclude_dims is None: dims = np.array([]) elif isinstance(dims, (float, int)): @@ -2094,8 +2088,9 @@ def ttv( return ttb.ktensor(factor_matrices, new_weights, copy=False) def update(self, modes: Union[int, Iterable[int]], data: np.ndarray) -> ktensor: - """ - Updates a :class:`pyttb.ktensor` in the specific dimensions with the + """Update a :class:`pyttb.ktensor` in the specific dimensions. + + Updates with the values in `data` (in vector or matrix form). The value of `modes` must be a value in [-1,...,self.ndims]. If the Further, the number of elements in `data` must equal self.shape[modes] * self.ncomponents. The update is @@ -2492,9 +2487,7 @@ def __sub__(self, other): return ttb.ktensor(factor_matrices, weights) def __mul__(self, other): - """ - Elementwise (including scalar) multiplication for - :class:`pyttb.ktensor` instances. + """Elementwise (including scalar) multiplication for :class:`pyttb.ktensor`. Parameters ---------- @@ -2515,9 +2508,7 @@ def __mul__(self, other): ), "Multiplication by ktensors only allowed for scalars, tensors, or sptensors" def __rmul__(self, other): - """ - Elementwise (including scalar) multiplication for - :class:`pyttb.ktensor` instances. + """Elementwise (including scalar) multiplication for :class:`pyttb.ktensor`. Parameters ---------- @@ -2530,8 +2521,7 @@ def __rmul__(self, other): return self.__mul__(other) def __repr__(self): - """ - String representation of a :class:`pyttb.ktensor`. + """Return string representation of a :class:`pyttb.ktensor`. Returns ------- diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index ec234a69..465c031e 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -1,4 +1,4 @@ -"""PYTTB shared utilities across tensor types""" +"""PYTTB shared utilities across tensor types.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -26,8 +26,7 @@ def tt_union_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: - """ - Helper function to reproduce functionality of MATLABS intersect(a,b,'rows') + """Reproduce functionality of MATLABS intersect(a,b,'rows'). Parameters ---------- @@ -94,8 +93,7 @@ def tt_dimscheck( dims: Optional[np.ndarray] = None, exclude_dims: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, Optional[np.ndarray]]: - """ - Used to preprocess dimensions for tensor dimensions + """Preprocess dimensions for tensor operations. Parameters ---------- @@ -194,8 +192,7 @@ def tt_dimscheck( def tt_setdiff_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: - """ - Helper function to reproduce functionality of MATLABS setdiff(a,b,'rows') + """Reproduce functionality of MATLABS setdiff(a,b,'rows'). Parameters ---------- @@ -224,8 +221,7 @@ def tt_setdiff_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: def tt_intersect_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: - """ - Helper function to reproduce functionality of MATLABS intersect(a,b,'rows') + """Reproduce functionality of MATLABS intersect(a,b,'rows'). Parameters ---------- @@ -266,8 +262,7 @@ def tt_intersect_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray: def tt_irenumber( t: ttb.sptensor, shape: Tuple[int, ...], number_range: Sequence[IndexType] ) -> np.ndarray: - """ - RENUMBER indices for sptensor __setitem__ + """Renumber indices for sptensor __setitem__. Parameters ---------- @@ -306,8 +301,7 @@ def tt_irenumber( def tt_renumber( subs: np.ndarray, shape: Tuple[int, ...], number_range: Sequence[IndexType] ) -> Tuple[np.ndarray, Tuple[int, ...]]: - """ - RENUMBER indices for sptensor __getitem__ + """Renumber indices for sptensor __getitem__. [NEWSUBS,NEWSZ] = RENUMBER(SUBS,SZ,RANGE) takes a set of original subscripts SUBS with entries from a tensor of size @@ -363,8 +357,9 @@ def tt_renumber( def tt_renumberdim( idx: np.ndarray, shape: int, number_range: IndexType ) -> Tuple[int, int]: - """ - RENUMBERDIM helper function for RENUMBER + """Renumber a single dimension. + + Helper function for RENUMBER. Parameters ---------- @@ -405,8 +400,7 @@ def tt_renumberdim( def tt_ismember_rows( search: np.ndarray, source: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: - """ - Find location of search rows in source array + """Find location of search rows in source array. Parameters ---------- @@ -475,8 +469,7 @@ def tt_ind2sub(shape: Tuple[int, ...], idx: np.ndarray) -> np.ndarray: def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: - """ - Helper function for tensor toolbox subsref. + """Helper function for tensor toolbox subsref. Parameters ---------- @@ -488,7 +481,7 @@ def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: Returns ------- Still uncertain to this functionality - """ + """ # noqa: D401 # TODO figure out when subsref yields key of length>1 for now ignore this logic and # just return # if len(s) == 1: @@ -501,8 +494,7 @@ def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: def tt_sub2ind(shape: Tuple[int, ...], subs: np.ndarray) -> np.ndarray: - """ - Converts multidimensional subscripts to linear indices. + """Convert multidimensional subscripts to linear indices. Parameters ---------- @@ -520,7 +512,6 @@ def tt_sub2ind(shape: Tuple[int, ...], subs: np.ndarray) -> np.ndarray: See Also -------- - :func:`tt_ind2sub`: """ if subs.size == 0: @@ -557,7 +548,6 @@ def tt_sizecheck(shape: Tuple[int, ...], nargout: bool = True) -> bool: See Also -------- - :func:`tt_subscheck`: """ siz = np.array(shape) @@ -606,7 +596,6 @@ def tt_subscheck(subs: np.ndarray, nargout: bool = True) -> bool: See Also -------- - :func:`tt_sizecheck`: :func:`tt_valscheck`: """ @@ -654,7 +643,6 @@ def tt_valscheck(vals: np.ndarray, nargout: bool = True) -> bool: See Also -------- - :func:`tt_sizecheck`: :func:`tt_subscheck`: """ @@ -731,7 +719,7 @@ def islogical(a: np.ndarray) -> bool: class IndexVariant(Enum): - """Methods for indexing entries of tensors""" + """Methods for indexing entries of tensors.""" UNKNOWN = 0 LINEAR = 1 @@ -773,7 +761,7 @@ def get_index_variant(indices: IndexType) -> IndexVariant: def get_mttkrp_factors( U: Union[ttb.ktensor, List[np.ndarray]], n: int, ndims: int ) -> List[np.ndarray]: - """Apply standard checks and type conversions for mttkrp factors""" + """Apply standard checks and type conversions for mttkrp factors.""" if isinstance(U, ttb.ktensor): U = U.copy() # Absorb lambda into one of the factors but not the one that is skipped @@ -800,7 +788,7 @@ def gather_wrap_dims( cdims: Optional[np.ndarray] = None, cdims_cyclic: Optional[Union[Literal["fc"], Literal["bc"], Literal["t"]]] = None, ) -> Tuple[np.ndarray, np.ndarray]: - """Utility to extract tensor modes mapped to rows and columns for matricized tensor. + """Extract tensor modes mapped to rows and columns for matricized tensors. Parameters ---------- diff --git a/pyttb/sptenmat.py b/pyttb/sptenmat.py index 196f06b3..31f6b644 100644 --- a/pyttb/sptenmat.py +++ b/pyttb/sptenmat.py @@ -17,10 +17,7 @@ class sptenmat: - """ - SPTENMAT Store sparse tensor as a sparse matrix. - - """ + """Store sparse tensor as a sparse matrix.""" __slots__ = ("tshape", "rdims", "cdims", "subs", "vals") @@ -33,8 +30,9 @@ def __init__( # noqa: PLR0913 tshape: Tuple[int, ...] = (), copy: bool = True, ): - """ - Construct a :class:`pyttb.sptenmat` from a set of 2D subscripts (subs) + """Construct a :class:`pyttb.sptenmat`. + + Constructed from a set of 2D subscripts (subs) and values (vals) along with the mappings of the row (rdims) and column indices (cdims) and the shape of the original tensor (tshape). @@ -170,8 +168,9 @@ def from_array( cdims: Optional[np.ndarray] = None, tshape: Tuple[int, ...] = (), ): - """ - Construct a :class:`pyttb.sptenmat` from a coo_matrix + """Construct a :class:`pyttb.sptenmat`. + + Constructed from a coo_matrix along with the mappings of the row (rdims) and column indices (cdims) and the shape of the original tensor (tshape). @@ -250,11 +249,11 @@ def copy(self) -> sptenmat: ) def __deepcopy__(self, memo): + """Return deepcopy of this sptenmat.""" return self.copy() def to_sptensor(self) -> ttb.sptensor: - """ - Contruct a :class:`pyttb.sptensor` from `:class:pyttb.sptenmat` + """Contruct a :class:`pyttb.sptensor` from `:class:pyttb.sptenmat`. Examples -------- @@ -372,9 +371,10 @@ def nnz(self) -> int: return len(self.vals) def norm(self) -> float: - """ - Compute the norm (i.e., Frobenius norm, or square root of the sum of - squares of entries) of the :class:`pyttb.sptenmat`. + """Compute the norm of the :class:`pyttb.sptenmat`. + + Frobenius norm, or square root of the sum of + squares of entries. Examples -------- @@ -542,8 +542,7 @@ def __setitem__(self, key, value): # noqa: PLR0912 self.vals = self.vals[sort_idx] def __repr__(self): - """ - String representation of a :class:`pyttb.sptenmat`. + """Return string representation of a :class:`pyttb.sptenmat`. Examples -------- diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index b65eed41..f0152862 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -87,8 +87,9 @@ def __init__( shape: Optional[Tuple[int, ...]] = None, copy: bool = True, ): - """ - Construct a :class:`pyttb.sptensor` from a set of `subs` (subscripts), + """Construct a :class:`pyttb.sptensor`. + + Constructed from a set of `subs` (subscripts), `vals` (values), and `shape`. No validation is performed. For initializer with error checking see :meth:`from_aggregator`. @@ -171,8 +172,9 @@ def from_function( shape: Tuple[int, ...], nonzeros: float, ) -> sptensor: - """ - Construct a :class:`pyttb.sptensor` whose nonzeros are set using a + """Construct a :class:`pyttb.sptensor`. + + Constructed with nonzeros set using a function. The subscripts of the nonzero elements of the sparse tensor are generated randomly using `numpy`, so calling `numpy.random.seed()` before using this method will provide reproducible results. @@ -254,8 +256,9 @@ def from_aggregator( shape: Optional[Tuple[int, ...]] = None, function_handle: Union[str, Callable[[Any], Union[float, np.ndarray]]] = "sum", ) -> sptensor: - """ - Construct a :class:`pyttb.sptensor` from a set of `subs` (subscripts), + """Construct a :class:`pyttb.sptensor`. + + Constructed from a set of `subs` (subscripts), `vals` (values), and `shape` after an aggregation function is applied to the values. @@ -374,6 +377,7 @@ def copy(self) -> sptensor: return ttb.sptensor(self.subs, self.vals, self.shape, copy=True) def __deepcopy__(self, memo): + """Return deep copy of this sptensor.""" return self.copy() def allsubs(self) -> np.ndarray: @@ -488,8 +492,9 @@ def collapse( return ttb.sptensor(np.array([]), np.array([]), tuple(newsize), copy=False) def contract(self, i_0: int, i_1: int) -> Union[np.ndarray, sptensor, ttb.tensor]: - """ - Contract the :class:`pyttb.sptensor` along two dimensions. If the + """Contract the :class:`pyttb.sptensor` along two dimensions. + + If the result is sufficiently dense, it is returned as a :class:`pyttb.tensor`. @@ -585,9 +590,9 @@ def double(self) -> np.ndarray: return a def elemfun(self, function_handle: Callable[[np.ndarray], np.ndarray]) -> sptensor: - """ - Apply a function to the nonzero elements of the - :class:`pyttb.sptensor`. Returns a copy of the sparse tensor, with the + """Apply a function to the nonzero elements of the :class:`pyttb.sptensor`. + + Returns a copy of the sparse tensor, with the updated values. Parameters @@ -668,7 +673,8 @@ def find(self) -> Tuple[np.ndarray, np.ndarray]: return self.subs, self.vals def to_tensor(self) -> ttb.tensor: - """ + """Convert to dense tensor. + Same as :meth:`pyttb.sptensor.full`. """ return self.full() @@ -715,9 +721,7 @@ def to_sptenmat( Union[Literal["fc"], Literal["bc"], Literal["t"]] ] = None, ) -> ttb.sptenmat: - """ - Construct a :class:`pyttb.sptenmat` from a :class:`pyttb.sptensor` and - unwrapping details. + """Construct a :class:`pyttb.sptenmat` from a :class:`pyttb.sptensor`. Parameters ---------- @@ -825,9 +829,7 @@ def to_sptenmat( def innerprod( self, other: Union[sptensor, ttb.tensor, ttb.ktensor, ttb.ttensor] ) -> float: - """ - Compute inner product of the :class:`pyttb.sptensor` with another - tensor. + """Compute inner product of the :class:`pyttb.sptensor` with another tensor. Parameters ---------- @@ -894,9 +896,9 @@ def innerprod( assert False, f"Inner product between sptensor and {type(other)} not supported" def isequal(self, other: Union[sptensor, ttb.tensor]) -> bool: - """ - Determine if the :class:`pyttb.sptensor` is equal to another tensor, - where all elements are exactly the same in both tensors. + """Determine if the :class:`pyttb.sptensor` is equal to another tensor. + + Equal when all elements are exactly the same in both tensors. Parameters ---------- @@ -1174,8 +1176,8 @@ def logical_xor( assert False, "The argument must be an sptensor, tensor or scalar" def mask(self, W: sptensor) -> np.ndarray: - """ - Extract values of the :class:`pyttb.sptensor` as specified by `W`. + """Extract values of the :class:`pyttb.sptensor` as specified by `W`. + The values in the sparse tensor corresponding to ones (1) in `W` will be returned as a column vector. @@ -1236,9 +1238,9 @@ def mask(self, W: sptensor) -> np.ndarray: return vals def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray: - """ - Matricized tensor times Khatri-Rao product using the - :class:`pyttb.sptensor`. This is an efficient form of the matrix + """Matricized tensor times Khatri-Rao product using :class:`pyttb.sptensor`. + + This is an efficient form of the matrix product that avoids explicitly computing the matricized sparse tensor and the large intermediate Khatri-Rao product arrays. @@ -1350,9 +1352,10 @@ def nnz(self) -> int: return self.subs.shape[0] def norm(self) -> float: - """ - Compute the norm (i.e., Frobenius norm, or square root of the sum of - squares of entries) of the :class:`pyttb.sptensor`. + """Compute the norm of the :class:`pyttb.sptensor`. + + Frobenius norm, or square root of the sum of + squares of entries. Examples -------- @@ -1477,8 +1480,9 @@ def ones(self) -> sptensor: return ttb.sptensor(self.subs, oneVals, self.shape) def permute(self, order: np.ndarray) -> sptensor: - """ - Permute the :class:`pyttb.sptensor` dimensions. The result is a new + """Permute the :class:`pyttb.sptensor` dimensions. + + The result is a new sparse tensor that has the same values, but the order of the subscripts needed to access any particular element are rearranged as specified by `order`. @@ -1529,9 +1533,9 @@ def reshape( new_shape: Tuple[int, ...], old_modes: Optional[Union[np.ndarray, int]] = None, ) -> sptensor: - """ - Reshape the :class:`pyttb.sptensor` to the have shape specified in - `new_shape`. If `old_modes` is specified, reshape only those modes of + """Reshape the :class:`pyttb.sptensor` to the `new_shape`. + + If `old_modes` is specified, reshape only those modes of the sparse tensor, moving newly reshaped modes to the end of the subscripts; otherwise use all modes. The product of the new shape must equal the product of the old shape. @@ -1702,9 +1706,7 @@ def scale( assert False, "Invalid scaling factor" def spmatrix(self) -> sparse.coo_matrix: - """ - Converts a 2-way :class:`pyttb.sptensor` to a - :class:`scipy.sparse.coo_matrix`. + """Convert 2-way :class:`pyttb.sptensor` to :class:`scipy.sparse.coo_matrix`. Examples -------- @@ -1735,8 +1737,7 @@ def spmatrix(self) -> sparse.coo_matrix: ) def squeeze(self) -> Union[sptensor, float]: - """ - Removes singleton dimensions from the :class:`pyttb.sptensor`. + """Remove singleton dimensions from the :class:`pyttb.sptensor`. Examples -------- @@ -2991,7 +2992,7 @@ def __rmul__(self, other): assert False, "This object cannot be multiplied by sptensor" def _compare(self, other, operator, opposite_operator, include_zero=False): # noqa: PLR0912 - """Generalized Comparison operation + """Generalized Comparison operation. Parameters ---------- @@ -3220,8 +3221,8 @@ def __gt__(self, other): return self._compare(other, gt, lt) def __truediv__(self, other): # noqa: PLR0912, PLR0915 - """ - Element-wise left division operator (/). + """Element-wise left division operator (/). + Comparisons with empty tensors raise an exception. Parameters @@ -3250,7 +3251,6 @@ def __truediv__(self, other): # noqa: PLR0912, PLR0915 sparse tensor of shape (2, 2) with 1 nonzeros [1, 1] = 0.66666... """ - # Divide by a scalar -> result is sparse if isinstance(other, (float, int)): # Inline mrdivide @@ -3374,8 +3374,7 @@ def __rtruediv__(self, other): assert False, "Dividing that object by an sptensor is not supported" def __repr__(self): # pragma: no cover - """ - String representation of a :class:`pyttb.sptensor`. + """Return string representation of a :class:`pyttb.sptensor`. Examples -------- @@ -3611,8 +3610,9 @@ def sptenrand( density: Optional[float] = None, nonzeros: Optional[float] = None, ) -> sptensor: - """ - Create a :class:`pyttb.sptensor` with entries drawn from a uniform + """Create a :class:`pyttb.sptensor` with random entries and indices. + + Entries drawn from a uniform distribution on the unit interval and indices selected using a uniform distribution. You can specify the density or number of nonzeros in the resulting sparse tensor but not both. @@ -3665,8 +3665,8 @@ def unit_uniform(pass_through_shape: Tuple[int, ...]) -> np.ndarray: def sptendiag( elements: np.ndarray, shape: Optional[Tuple[int, ...]] = None ) -> sptensor: - """ - Creates a :class:`pyttb.sptensor` with elements along the super diagonal. + """Create a :class:`pyttb.sptensor` with elements along the super diagonal. + If provided shape is too small the sparse tensor will be enlarged to accommodate. diff --git a/pyttb/sptensor3.py b/pyttb/sptensor3.py index 64a83cd2..97420f70 100644 --- a/pyttb/sptensor3.py +++ b/pyttb/sptensor3.py @@ -1,4 +1,4 @@ -"""Sparse Tensor 3 Class Placeholder""" +"""Sparse Tensor 3 Class Placeholder.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -6,10 +6,7 @@ class sptensor3: - """ - SPTENSOR3 a sparse tensor variant. - - """ + """A sparse tensor variant.""" def __init__(self): assert False, "SPTENSOR3 class not yet implemented" diff --git a/pyttb/sumtensor.py b/pyttb/sumtensor.py index 50c4055e..0eebe79e 100644 --- a/pyttb/sumtensor.py +++ b/pyttb/sumtensor.py @@ -18,10 +18,7 @@ class sumtensor: - """ - SUMTENSOR Class for implicit sum of other tensors. - - """ + """Class for implicit sum of other tensors.""" def __init__( self, @@ -30,8 +27,8 @@ def __init__( ] = None, copy: bool = True, ): - """ - Creates a :class:`pyttb.sumtensor` from a collection of tensors. + """Create a :class:`pyttb.sumtensor` from a collection of tensors. + Each provided tensor is explicitly retained. All provided tensors must have the same shape but can be combinations of types. @@ -43,7 +40,7 @@ def __init__( Whether to make a copy of provided data or just reference it. Examples - ------- + -------- Create an empty :class:`pyttb.tensor`: >>> T1 = ttb.tenones((3, 4, 5)) @@ -85,17 +82,18 @@ def copy(self) -> sumtensor: return ttb.sumtensor(self.parts, copy=True) def __deepcopy__(self, memo): + """Return deepcopy of this sumtensor.""" return self.copy() @property def shape(self) -> Tuple[int, ...]: + """Shape of a :class:`pyttb.sumtensor`.""" if len(self.parts) == 0: return () return self.parts[0].shape def __repr__(self): - """ - String representation of the sumtensor. + """Return string representation of the sumtensor. Returns ------- @@ -248,7 +246,8 @@ def __radd__(self, other): return self.__add__(other) def to_tensor(self) -> ttb.tensor: - """ + """Return sumtensor converted to dense tensor. + Same as :meth:`pyttb.sumtensor.full`. """ return self.full() @@ -298,9 +297,7 @@ def double(self) -> np.ndarray: def innerprod( self, other: Union[ttb.tensor, ttb.sptensor, ttb.ktensor, ttb.ttensor] ) -> float: - """ - Efficient inner product between a sumtensor and other `pyttb` tensors - (`tensor`, `sptensor`, `ktensor`, or `ttensor`). + """Efficient inner product between a sumtensor and other `pyttb` tensors. Parameters ---------- @@ -325,8 +322,9 @@ def innerprod( return result def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray: - """ - Matricized tensor times Khatri-Rao product. The matrices used in the + """Matricized tensor times Khatri-Rao product. + + The matrices used in the Khatri-Rao product are passed as a :class:`pyttb.ktensor` (where the factor matrices are used) or as a list of :class:`numpy.ndarray` objects. @@ -434,7 +432,7 @@ def ttv( return ttb.sumtensor(new_parts, copy=False) def norm(self) -> float: - """Compatibility Interface. Just returns 0""" + """Compatibility Interface. Just returns 0.""" warnings.warn( "Sumtensor doesn't actually support norm. " "Returning 0 for compatibility." ) diff --git a/pyttb/symktensor.py b/pyttb/symktensor.py index 398e77a5..a9a11591 100644 --- a/pyttb/symktensor.py +++ b/pyttb/symktensor.py @@ -1,4 +1,4 @@ -"""Symmetric Kruskal Tensor Class Placeholder""" +"""Symmetric Kruskal Tensor Class Placeholder.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -6,10 +6,7 @@ class symktensor: - """ - SYMKTENSOR Class for symmetric Kruskal tensors (decomposed). - - """ + """Class for symmetric Kruskal tensors (decomposed).""" def __init__(self): assert False, "SYMKTENSOR class not yet implemented" diff --git a/pyttb/symtensor.py b/pyttb/symtensor.py index d1eb548a..a95d8007 100644 --- a/pyttb/symtensor.py +++ b/pyttb/symtensor.py @@ -1,4 +1,4 @@ -"""Symmetric Tensor Class Placeholder""" +"""Symmetric Tensor Class Placeholder.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -6,10 +6,7 @@ class symtensor: - """ - SYMTENSOR Class for storing only unique entries of symmetric tensor. - - """ + """Class for storing only unique entries of symmetric tensor.""" def __init__(self): assert False, "SYMTENSOR class not yet implemented" diff --git a/pyttb/tenmat.py b/pyttb/tenmat.py index 683f0b6e..b4554cf9 100644 --- a/pyttb/tenmat.py +++ b/pyttb/tenmat.py @@ -15,10 +15,7 @@ class tenmat: - """ - TENMAT Store tensor as a matrix. - - """ + """Store tensor as a matrix.""" __slots__ = ("tshape", "rindices", "cindices", "data") @@ -30,8 +27,8 @@ def __init__( # noqa: PLR0912 tshape: Optional[Tuple[int, ...]] = None, copy: bool = True, ): - """ - Construct a :class:`pyttb.tenmat` from explicit components. + """Construct a :class:`pyttb.tenmat` from explicit components. + If you already have a tensor see :meth:`pyttb.tensor.to_tenmat`. Parameters @@ -86,7 +83,6 @@ def __init__( # noqa: PLR0912 [[4., 5.], [6., 7.]]]) """ - # Case 0a: Empty Contructor # data is empty, return empty tenmat unless rdims, cdims, or tshape are # not empty @@ -197,6 +193,7 @@ def copy(self) -> tenmat: ) def __deepcopy__(self, memo): + """Return deep copy of this tenmat.""" return self.copy() def to_tensor(self, copy: bool = True) -> ttb.tensor: @@ -333,8 +330,7 @@ def ndims(self) -> int: return len(self.shape) def norm(self) -> float: - """ - Frobenius norm of a :class:`pyttb.tenmat`. + """Frobenius norm of a :class:`pyttb.tenmat`. Examples -------- @@ -551,7 +547,6 @@ def __add__(self, other): ------- :class:`pyttb.tenmat` """ - # One argument is a scalar if np.isscalar(other): Z = self.copy() @@ -622,7 +617,6 @@ def __sub__(self, other): ------- :class:`pyttb.tenmat` """ - # One argument is a scalar if np.isscalar(other): Z = self.copy() @@ -661,7 +655,6 @@ def __rsub__(self, other): ------- :class:`pyttb.tenmat` """ - # One argument is a scalar if np.isscalar(other): Z = self.copy() @@ -697,7 +690,6 @@ def __pos__(self): :class:`pyttb.tenmat` copy of tenmat """ - T = self.copy() return T @@ -722,15 +714,13 @@ def __neg__(self): :class:`pyttb.tenmat` Copy of original tenmat with negated data. """ - T = self.copy() T.data = -1 * T.data return T def __repr__(self): - """ - String representation of a :class:`pyttb.tenmat`. + """Return string representation of a :class:`pyttb.tenmat`. Examples -------- diff --git a/pyttb/tensor.py b/pyttb/tensor.py index 230b8d1e..56385b0c 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -62,8 +62,7 @@ def __init__( shape: Optional[Tuple[int, ...]] = None, copy: bool = True, ): - """ - Creates a :class:`pyttb.tensor` from a :class:`numpy.ndarray` + """Create a :class:`pyttb.tensor` from a :class:`numpy.ndarray`. Note that 1D tensors (i.e., when len(shape)==1) contains a data array that follow the Numpy convention of being a row vector. @@ -78,7 +77,7 @@ def __init__( Whether to make a copy of provided data or just reference it. Examples - ------- + -------- Create an empty :class:`pyttb.tensor`: >>> T = ttb.tensor() @@ -142,9 +141,7 @@ def from_function( function_handle: Callable[[Tuple[int, ...]], np.ndarray], shape: Tuple[int, ...], ) -> tensor: - """ - Construct a :class:`pyttb.tensor` whose data entries are set using - a function. + """Construct a :class:`pyttb.tensor` with data from a function. Parameters ---------- @@ -206,6 +203,7 @@ def copy(self) -> tensor: return ttb.tensor(self.data, self.shape, copy=True) def __deepcopy__(self, memo): + """Return deep copy of this tensor.""" return self.copy() def collapse( @@ -422,8 +420,7 @@ def find(self) -> Tuple[np.ndarray, np.ndarray]: return subs, vals def to_sptensor(self) -> ttb.sptensor: - """ - Contruct a :class:`pyttb.sptensor` from `:class:pyttb.tensor` + """Contruct a :class:`pyttb.sptensor` from `:class:pyttb.tensor`. Returns ------- @@ -466,9 +463,7 @@ def to_tenmat( ] = None, copy: bool = True, ) -> ttb.tenmat: - """ - Construct a :class:`pyttb.tenmat` from a :class:`pyttb.tensor` and - unwrapping details. + """Construct a :class:`pyttb.tenmat` from a :class:`pyttb.tensor`. Parameters ---------- @@ -583,9 +578,7 @@ def to_tenmat( def innerprod( self, other: Union[tensor, ttb.sptensor, ttb.ktensor, ttb.ttensor] ) -> float: - """ - Efficient inner product between a tensor and other `pyttb` tensors - (`tensor`, `sptensor`, `ktensor`, or `ttensor`). + """Efficient inner product between a tensor and other `pyttb` tensors. Parameters ---------- @@ -870,8 +863,9 @@ def mask(self, W: tensor) -> np.ndarray: return self.data[tuple(wsubs.transpose())] def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray: - """ - Matricized tensor times Khatri-Rao product. The matrices used in the + """Matricized tensor times Khatri-Rao product. + + The matrices used in the Khatri-Rao product are passed as a :class:`pyttb.ktensor` (where the factor matrices are used) or as a list of :class:`numpy.ndarray` objects. @@ -894,7 +888,6 @@ def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray: array([[4., 4.], [4., 4.]]) """ - # check that we have a tensor that can perform mttkrp if self.ndims < 2: assert False, "MTTKRP is invalid for tensors with fewer than 2 dimensions" @@ -1012,8 +1005,9 @@ def nnz(self) -> int: return np.count_nonzero(self.data) def norm(self) -> float: - """ - Frobenius norm of the tensor, defined as the square root of the sum of the + """Frobenius norm of the tensor. + + Defined as the square root of the sum of the squares of the elements of the tensor. Examples @@ -1086,8 +1080,9 @@ def nvecs(self, n: int, r: int, flipsign: bool = True) -> np.ndarray: return v def permute(self, order: np.ndarray) -> tensor: - """ - Permute tensor dimensions. The result is a tensor that has the + """Permute tensor dimensions. + + The result is a tensor that has the same values, but the order of the subscripts needed to access any particular element are rearranged as specified by `order`. @@ -1214,8 +1209,7 @@ def scale( return ttb.tenmat(result, dims, remdims, self.shape, copy=False).to_tensor() def squeeze(self) -> Union[tensor, float]: - """ - Removes singleton dimensions from the tensor. + """Remove singleton dimensions from the tensor. Returns ------- @@ -1773,8 +1767,7 @@ def tenfun( ttb.sumtensor, ], ) -> ttb.tensor: - """ - Apply a function to each element in a tensor or tensors + """Apply a function to each element in a tensor or tensors. See :meth:`pyttb.tensor.tenfun_binary` and :meth:`pyttb.tensor.tenfun_binary_unary` for supported @@ -2441,8 +2434,7 @@ def tensor_add(x, y): return self.tenfun(tensor_add, other) def __radd__(self, other): - """ - Right binary addition (+) for tensors + """Right binary addition (+) for tensors. Parameters ---------- @@ -2491,8 +2483,7 @@ def tensor_pow(x, y): return self.tenfun(tensor_pow, power) def __mul__(self, other): - """ - Element-wise multiplication (*) for tensors, self*other + """Element-wise multiplication (*) for tensors, self*other. Parameters ---------- @@ -2526,8 +2517,7 @@ def mul(x, y): return self.tenfun(mul, other) def __rmul__(self, other): - """ - Element wise right multiplication (*) for tensors, other*self + """Element wise right multiplication (*) for tensors, other*self. Parameters ---------- @@ -2549,8 +2539,7 @@ def __rmul__(self, other): return self.__mul__(other) def __truediv__(self, other): - """ - Element-wise left division (/) for tensors, self/other + """Element-wise left division (/) for tensors, self/other. Parameters ---------- @@ -2584,8 +2573,7 @@ def div(x, y): return self.tenfun(div, other) def __rtruediv__(self, other): - """ - Element wise right division (/) for tensors, other/self + """Element wise right division (/) for tensors, other/self. Parameters ---------- @@ -2650,12 +2638,10 @@ def __neg__(self): [[-1 -2] [-3 -4]] """ - return ttb.tensor(-1 * self.data) def __repr__(self): - """ - String representation of the tensor. + """Return string representation of the tensor. Returns ------- @@ -2714,8 +2700,7 @@ def __repr__(self): def tenones(shape: Tuple[int, ...]) -> tensor: - """ - Creates a tensor of all ones. + """Create a tensor of all ones. Parameters ---------- @@ -2745,8 +2730,7 @@ def tenones(shape: Tuple[int, ...]) -> tensor: def tenzeros(shape: Tuple[int, ...]) -> tensor: - """ - Creates a tensor of all zeros. + """Create a tensor of all zeros. Parameters ---------- @@ -2776,9 +2760,7 @@ def tenzeros(shape: Tuple[int, ...]) -> tensor: def tenrand(shape: Tuple[int, ...]) -> tensor: - """ - Creates a tensor with entries drawn from a uniform - distribution on the unit interval. + """Create a tensor with entries drawn from a uniform distribution on [0, 1]. Parameters ---------- @@ -2808,9 +2790,9 @@ def unit_uniform(pass_through_shape: Tuple[int, ...]) -> np.ndarray: def tendiag(elements: np.ndarray, shape: Optional[Tuple[int, ...]] = None) -> tensor: - """ - Creates a tensor with elements along super diagonal. If provided shape is too - small the tensor will be enlarged to accomodate. + """Create a tensor with elements along super diagonal. + + If provided shape is too small the tensor will be enlarged to accomodate. Parameters ---------- @@ -2893,8 +2875,8 @@ def teneye(order: int, size: int) -> tensor: def mttv_left(W_in: np.ndarray, U1: np.ndarray) -> np.ndarray: - """ - Contract leading mode in partial MTTKRP W_in using factor matrix U1. + """Contract leading mode in partial MTTKRP W_in using factor matrix U1. + The leading mode is the mode for which consecutive increases in index address elements at consecutive increases in the memory offset. diff --git a/pyttb/ttensor.py b/pyttb/ttensor.py index cef017e9..e95b5532 100644 --- a/pyttb/ttensor.py +++ b/pyttb/ttensor.py @@ -1,4 +1,4 @@ -"""Tucker Tensor Implementation""" +"""Tucker Tensor Implementation.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -22,9 +22,7 @@ class ttensor: - """ - TTENSOR Class for Tucker tensors (decomposed). - """ + """Class for Tucker tensors (decomposed).""" __slots__ = ("core", "factor_matrices") @@ -117,10 +115,11 @@ def copy(self) -> ttensor: return ttb.ttensor(self.core, self.factor_matrices, copy=True) def __deepcopy__(self, memo): + """Return deepcopy of class.""" return self.copy() def _validate_ttensor(self): - """Verifies the validity of constructed ttensor""" + """Verify constructed ttensor.""" # Confirm all factors are matrices for factor_idx, factor in enumerate(self.factor_matrices): if not isinstance(factor, (np.ndarray, sparse.coo_matrix)): @@ -154,8 +153,7 @@ def shape(self) -> Tuple[int, ...]: return tuple(factor.shape[0] for factor in self.factor_matrices) def __repr__(self): # pragma: no cover - """ - String representation of a tucker tensor. + """Return string representation of a tucker tensor. Returns ------- @@ -174,7 +172,8 @@ def __repr__(self): # pragma: no cover __str__ = __repr__ def to_tensor(self) -> ttb.tensor: - """Convenience method to convert to tensor. + """Convert to tensor. + Same as :meth:`pyttb.ttensor.full` """ return self.full() @@ -189,8 +188,7 @@ def full(self) -> ttb.tensor: return recomposed_tensor def double(self) -> np.ndarray: - """ - Convert ttensor to an array of doubles + """Convert ttensor to an array of doubles. Returns ------- @@ -210,8 +208,7 @@ def ndims(self) -> int: return len(self.factor_matrices) def isequal(self, other: ttensor) -> bool: - """ - Component equality for ttensors + """Component equality for ttensors. Parameters ---------- @@ -241,12 +238,10 @@ def __pos__(self): ------- :class:`pyttb.ttensor`, copy of tensor """ - return self.copy() def __neg__(self): - """ - Unary minus (-) for ttensors + """Unary minus (-) for ttensors. Returns ------- @@ -257,8 +252,7 @@ def __neg__(self): def innerprod( self, other: Union[ttb.tensor, ttb.sptensor, ttb.ktensor, ttb.ttensor] ) -> float: - """ - Efficient inner product with a ttensor + """Efficient inner product with a ttensor. Parameters ---------- @@ -306,8 +300,7 @@ def innerprod( ) def __mul__(self, other): - """ - Element wise multiplication (*) for ttensors (only scalars supported) + """Element wise multiplication (*) for ttensors (only scalars supported). Parameters ---------- @@ -325,8 +318,7 @@ def __mul__(self, other): ) def __rmul__(self, other): - """ - Element wise right multiplication (*) for ttensors (only scalars supported) + """Element wise right multiplication (*) for ttensors (only scalars supported). Parameters ---------- @@ -346,8 +338,7 @@ def ttv( dims: Optional[Union[int, np.ndarray]] = None, exclude_dims: Optional[Union[int, np.ndarray]] = None, ) -> Union[float, ttensor]: - """ - TTensor times vector + """TTensor times vector. Parameters ---------- @@ -436,6 +427,7 @@ def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray: def norm(self) -> float: """ Compute the norm of a ttensor. + Returns ------- Frobenius norm of Tensor. @@ -480,8 +472,7 @@ def ttm( exclude_dims: Optional[Union[int, np.ndarray]] = None, transpose: bool = False, ) -> ttensor: - """ - Tensor times matrix for ttensor + """Tensor times matrix for ttensor. Parameters ---------- diff --git a/pyttb/tucker_als.py b/pyttb/tucker_als.py index 452e1bd8..f32322b8 100644 --- a/pyttb/tucker_als.py +++ b/pyttb/tucker_als.py @@ -1,4 +1,4 @@ -"""Tucker decomposition via Alternating Least Squares""" +"""Tucker decomposition via Alternating Least Squares.""" # Copyright 2024 National Technology & Engineering Solutions of Sandia, # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the @@ -24,8 +24,7 @@ def tucker_als( # noqa: PLR0912,PLR0913,PLR0915 init: Union[Literal["random"], Literal["nvecs"], ttb.ktensor] = "random", printitn: int = 1, ) -> Tuple[ttensor, ttensor, Dict]: - """ - Compute Tucker decomposition with alternating least squares + """Compute Tucker decomposition with alternating least squares. Parameters ---------- From 990b8c532ff48a6ba8229f02e6535f6f4958aed1 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:14:14 -0500 Subject: [PATCH 07/10] RUFF: forgot to run format after doc updates --- conftest.py | 6 +++--- pyttb/cp_apr.py | 1 + pyttb/gcp/optimizers.py | 12 ++++++------ pyttb/pyttb_utils.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/conftest.py b/conftest.py index 8eac1e31..0811cdd9 100644 --- a/conftest.py +++ b/conftest.py @@ -12,12 +12,12 @@ @pytest.fixture(autouse=True) -def add_packages(doctest_namespace): #noqa: D103 +def add_packages(doctest_namespace): # noqa: D103 doctest_namespace["np"] = numpy doctest_namespace["ttb"] = pyttb -def pytest_addoption(parser): #noqa: D103 +def pytest_addoption(parser): # noqa: D103 parser.addoption( "--packaging", action="store_true", @@ -27,6 +27,6 @@ def pytest_addoption(parser): #noqa: D103 ) -def pytest_configure(config): #noqa: D103 +def pytest_configure(config): # noqa: D103 if not config.option.packaging: config.option.markexpr = "not packaging" diff --git a/pyttb/cp_apr.py b/pyttb/cp_apr.py index 6a445c5a..5d0b9fe2 100644 --- a/pyttb/cp_apr.py +++ b/pyttb/cp_apr.py @@ -1699,6 +1699,7 @@ def calc_grad( grad_row = (np.ones(phi_row.shape) - phi_row).transpose() return grad_row, phi_row + # TODO verify what pi is # Mu helper functions def calculate_pi( diff --git a/pyttb/gcp/optimizers.py b/pyttb/gcp/optimizers.py index d2be3351..300a7fcf 100644 --- a/pyttb/gcp/optimizers.py +++ b/pyttb/gcp/optimizers.py @@ -244,7 +244,7 @@ def solve( # noqa: PLR0913 class SGD(StochasticSolver): """General Stochastic Gradient Descent.""" - def update_step( #noqa: D102 + def update_step( # noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: step = self._decay**self._nfails * self._rate @@ -254,7 +254,7 @@ def update_step( #noqa: D102 ] return factor_matrices, step - def set_failed_epoch(self): #noqa: D102 + def set_failed_epoch(self): # noqa: D102 # No additional internal state for SGD pass @@ -318,14 +318,14 @@ def __init__( # noqa: PLR0913 self._v: List[np.ndarray] = [] self._v_prev: List[np.ndarray] = [] - def set_failed_epoch( #noqa: D102 + def set_failed_epoch( # noqa: D102 self, ): self._total_iterations -= self._epoch_iters self._m = self._m_prev.copy() self._v = self._v_prev.copy() - def update_step( #noqa: D102 + def update_step( # noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: if self._total_iterations == 0: @@ -387,12 +387,12 @@ def __init__( # noqa: PLR0913 ) self._gnormsum = 0.0 - def set_failed_epoch( #noqa: D102 + def set_failed_epoch( # noqa: D102 self, ): self._gnormsum = 0.0 - def update_step( #noqa: D102 + def update_step( # noqa: D102 self, model: ttb.ktensor, gradient: List[np.ndarray], lower_bound: float ) -> Tuple[List[np.ndarray], float]: self._gnormsum += np.sum([np.sum(gk**2) for gk in gradient]) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index 465c031e..b0deaaec 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -481,7 +481,7 @@ def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: Returns ------- Still uncertain to this functionality - """ # noqa: D401 + """ # noqa: D401 # TODO figure out when subsref yields key of length>1 for now ignore this logic and # just return # if len(s) == 1: From 279f7eaba133c30474fa296e2ff7507cdde82547 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:29:48 -0500 Subject: [PATCH 08/10] Fix merge errors and capture stricter requirements on main. --- pyttb/ktensor.py | 4 +--- pyttb/pyttb_utils.py | 26 +++++++++++++++----------- pyttb/sptensor.py | 3 +-- pyttb/sumtensor.py | 2 +- pyttb/tensor.py | 9 ++++++--- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pyttb/ktensor.py b/pyttb/ktensor.py index dfcb6ea3..5c5f5571 100644 --- a/pyttb/ktensor.py +++ b/pyttb/ktensor.py @@ -303,9 +303,7 @@ def from_function( return cls(factor_matrices, weights, copy=False) @classmethod - def from_vector( - cls, data: np.ndarray, shape: Shape, contains_weights: bool - ): + def from_vector(cls, data: np.ndarray, shape: Shape, contains_weights: bool): """Construct a :class:`pyttb.ktensor` from a vector and shape. The rank of the diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index d58d7fce..2718dd93 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -7,7 +7,6 @@ from __future__ import annotations from enum import Enum -from inspect import signature from math import prod from typing import ( Any, @@ -17,6 +16,7 @@ Sequence, Tuple, Union, + cast, get_args, overload, ) @@ -340,12 +340,12 @@ def tt_renumber( if not number_range[i] == slice(None, None, None): if subs.size == 0: if not isinstance(number_range[i], slice): - if isinstance(number_range[i], (int, float, np.integer)): - newshape[i] = number_range[i] + # This should be statically determinable but mypy unhappy + # without intermediate + number_range_i = number_range[i] + if isinstance(number_range_i, (int, float, np.integer)): + newshape[i] = number_range_i else: - # This should be statically determinable but mypy unhappy - # without assert - number_range_i = number_range[i] assert not isinstance(number_range_i, (int, slice, np.integer)) newshape[i] = len(number_range_i) else: @@ -502,11 +502,16 @@ def tt_subsubsref(obj: np.ndarray, s: Any) -> Union[float, np.ndarray]: # else: # return obj[s[1:]] if isinstance(obj, np.ndarray) and obj.size == 1: - return obj.item() + # TODO: Globally figure out why typing thinks item is a string + return cast(float, obj.item()) return obj -def tt_sub2ind(shape: Tuple[int, ...], subs: np.ndarray) -> np.ndarray: +def tt_sub2ind( + shape: Tuple[int, ...], + subs: np.ndarray, + order: Union[Literal["F"], Literal["C"]] = "F", +) -> np.ndarray: """Convert multidimensional subscripts to linear indices. Parameters @@ -889,8 +894,7 @@ def np_to_python( def parse_shape(shape: Shape) -> Tuple[int, ...]: - """Provides more flexible shape support - + """Parse flexible type into shape tuple. Examples -------- @@ -930,7 +934,7 @@ def parse_shape(shape: Shape) -> Tuple[int, ...]: def parse_one_d(maybe_vector: OneDArray) -> np.ndarray: - """Provides more flexible vector support + """Parse flexible type into numpy array. Examples -------- diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index df556a12..509f2795 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -1249,8 +1249,7 @@ def mask(self, W: sptensor) -> np.ndarray: return vals def mttkrp( - self, - U: Union[ttb.ktensor, Sequence[np.ndarray]], n: Union[int, np.integer] + self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: Union[int, np.integer] ) -> np.ndarray: """Matricized tensor times Khatri-Rao product using :class:`pyttb.sptensor`. diff --git a/pyttb/sumtensor.py b/pyttb/sumtensor.py index 1be04f07..f6be9bf9 100644 --- a/pyttb/sumtensor.py +++ b/pyttb/sumtensor.py @@ -322,7 +322,7 @@ def innerprod( return result def mttkrp( - self, U: Union[ttb.ktensor, List[np.ndarray]], n: Union[int, np.integer] + self, U: Union[ttb.ktensor, List[np.ndarray]], n: Union[int, np.integer] ) -> np.ndarray: """Matricized tensor times Khatri-Rao product. diff --git a/pyttb/tensor.py b/pyttb/tensor.py index 06d04a56..ba342c38 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -145,7 +145,7 @@ def __init__( if copy: self.data = data.copy(self.order) else: - if not self.matches_order(data): + if not self._matches_order(data): logging.warning( f"Selected no copy, but input data isn't {self.order} ordered " "so must copy." @@ -159,7 +159,8 @@ def order(self) -> Literal["F"]: """Return the data layout of the underlying storage.""" return "F" - def matches_order(self, array: np.ndarray) -> bool: + def _matches_order(self, array: np.ndarray) -> bool: + """Check if provided array matches tensor memory layout.""" if array.flags["C_CONTIGUOUS"] and self.order == "C": return True if array.flags["F_CONTIGUOUS"] and self.order == "F": @@ -896,7 +897,7 @@ def mask(self, W: tensor) -> np.ndarray: return self.data[tuple(wsubs.transpose())] def mttkrp( - self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: Union[int, np.integer] + self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: Union[int, np.integer] ) -> np.ndarray: """Matricized tensor times Khatri-Rao product. @@ -2757,6 +2758,7 @@ def ones(shape: Tuple[int, ...]) -> np.ndarray: return tensor.from_function(ones, shape) + def tenzeros(shape: Shape, order: Union[Literal["F"], Literal["C"]] = "F") -> tensor: """Create a tensor of all zeros. @@ -2792,6 +2794,7 @@ def zeros(shape: Tuple[int, ...]) -> np.ndarray: return tensor.from_function(zeros, shape) + def tenrand(shape: Shape, order: Union[Literal["F"], Literal["C"]] = "F") -> tensor: """Create a tensor with entries drawn from a uniform distribution on [0, 1]. From 2ab3b7326448a96554aefeb2a8cc1d51553d01cf Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:33:22 -0500 Subject: [PATCH 09/10] Fix type hint for more flexible tensor collapse. --- pyttb/tensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyttb/tensor.py b/pyttb/tensor.py index ba342c38..465522d9 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -243,7 +243,7 @@ def __deepcopy__(self, memo): def collapse( self, - dims: Optional[np.ndarray] = None, + dims: Optional[OneDArray] = None, fun: Callable[[np.ndarray], Union[float, np.ndarray]] = np.sum, ) -> Union[float, np.ndarray, tensor]: """ @@ -280,10 +280,11 @@ def collapse( if dims is None: dims = np.arange(0, self.ndims) + dims, _ = tt_dimscheck(self.ndims, dims=dims) + if dims.size == 0: return self.copy() - dims, _ = tt_dimscheck(self.ndims, dims=dims) remdims = np.setdiff1d(np.arange(0, self.ndims), dims) # Check for the case where we accumulate over *all* dimensions From c321d24fea8cea10c16daa41367c852451a50bad Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:36:36 -0500 Subject: [PATCH 10/10] TTENSOR: Fix missing newline and indentation. --- pyttb/ttensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyttb/ttensor.py b/pyttb/ttensor.py index 53d0cd08..deb0eeac 100644 --- a/pyttb/ttensor.py +++ b/pyttb/ttensor.py @@ -168,7 +168,8 @@ def __repr__(self): # pragma: no cover Contains the core, and factor matrices as strings on different lines. """ display_string = f"Tensor of shape: {self.shape}\n" f"\tCore is a\n" - display_string += textwrap.indent(str(self.core), "\t") + display_string += textwrap.indent(str(self.core), "\t\t") + display_string += "\n" for factor_idx, factor in enumerate(self.factor_matrices): display_string += f"\tU[{factor_idx}] = \n"