From ebd5069e63326d21c169b9b0254599de58f121d2 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:52:56 -0400 Subject: [PATCH] Nick/tenmat constructors (#293) * Add copy constructor for tenmat. * SPTENMAT: Remove from_data. Merge with __init__ * SPTENMAT: Move from_tensor_type to to_sptenmat * Add isequal * Pull out some common code ahead of tenmat constructor refactor. * TENMAT: Decouple from_data and from_tensor * From_data should only accept vector or matrix * TENMAT: Merge from_data into default constructor * TENMAT: Move from_tensor to to_tenmat * Add isequal utility method to tenmat * Update tutorials to match new constructors. * Improve some documentation for updated constructors/methods. * BUILD: Temporarily pin ruff to unblock --- docs/source/tutorial/class_sptenmat.ipynb | 40 +-- docs/source/tutorial/class_tenmat.ipynb | 70 ++-- pyttb/hosvd.py | 2 +- pyttb/pyttb_utils.py | 44 ++- pyttb/sptenmat.py | 275 +++++++-------- pyttb/sptensor.py | 120 ++++++- pyttb/tenmat.py | 397 +++++++++++++--------- pyttb/tensor.py | 140 +++++++- pyttb/ttensor.py | 12 +- tests/test_sptenmat.py | 98 +++--- tests/test_tenmat.py | 204 +++++------ tests/test_tensor.py | 2 +- 12 files changed, 829 insertions(+), 575 deletions(-) diff --git a/docs/source/tutorial/class_sptenmat.ipynb b/docs/source/tutorial/class_sptenmat.ipynb index 7cc27566..314fe6a7 100644 --- a/docs/source/tutorial/class_sptenmat.ipynb +++ b/docs/source/tutorial/class_sptenmat.ipynb @@ -70,7 +70,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(X, np.array([0])) # Mode-0 matricization\n", + "A = X.to_sptenmat(np.array([0])) # Mode-0 matricization\n", "A" ] }, @@ -81,7 +81,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(X, np.array([1, 2])) # Multiple modes mapped to rows.\n", + "A = X.to_sptenmat(np.array([1, 2])) # Multiple modes mapped to rows.\n", "A" ] }, @@ -92,9 +92,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, cdims=np.array([1, 2])\n", - ") # Specify column dimensions.\n", + "A = X.to_sptenmat(cdims=np.array([1, 2])) # Specify column dimensions.\n", "A" ] }, @@ -105,9 +103,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, np.arange(4)\n", - ") # All modes mapped to rows, i.e., vectorize.\n", + "A = X.to_sptenmat(np.arange(4)) # All modes mapped to rows, i.e., vectorize.\n", "A" ] }, @@ -118,9 +114,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, np.array([1])\n", - ") # By default, columns are ordered as [0, 2, 3]\n", + "A = X.to_sptenmat(np.array([1])) # By default, columns are ordered as [0, 2, 3]\n", "A" ] }, @@ -131,9 +125,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, np.array([1]), np.array([3, 0, 2])\n", - ") # Specify explicit ordering\n", + "A = X.to_sptenmat(np.array([1]), np.array([3, 0, 2])) # Specify explicit ordering\n", "A" ] }, @@ -144,9 +136,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"fc\"\n", - ") # Forward cyclic column ordering\n", + "A = X.to_sptenmat(np.array([1]), cdims_cyclic=\"fc\") # Forward cyclic column ordering\n", "A" ] }, @@ -157,9 +147,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic column ordering\n", + "A = X.to_sptenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic column ordering\n", "A" ] }, @@ -236,9 +224,7 @@ "metadata": {}, "outputs": [], "source": [ - "B = ttb.sptenmat.from_data(\n", - " A.subs, A.vals, A.rdims, A.cdims, A.tshape\n", - ") # Effectively copies A\n", + "B = ttb.sptenmat(A.subs, A.vals, A.rdims, A.cdims, A.tshape) # Effectively copies A\n", "B" ] }, @@ -257,9 +243,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.sptenmat.from_data(\n", - " rdims=A.rdims, cdims=A.cdims, tshape=A.tshape\n", - ") # An empty sptenmat\n", + "A = ttb.sptenmat(rdims=A.rdims, cdims=A.cdims, tshape=A.tshape) # An empty sptenmat\n", "A" ] }, @@ -298,7 +282,7 @@ "outputs": [], "source": [ "X = ttb.sptenrand((10, 10, 10, 10), nonzeros=10) # Create sptensor\n", - "A = ttb.sptenmat.from_tensor_type(X, np.array([0])) # Convert to an sptenmat\n", + "A = X.to_sptenmat(np.array([0])) # Convert to an sptenmat\n", "A" ] }, @@ -328,7 +312,7 @@ "metadata": {}, "outputs": [], "source": [ - "B = ttb.sptenmat.from_tensor_type(ttb.sptenrand((3, 3, 3), nonzeros=3), np.array([0]))\n", + "B = ttb.sptenrand((3, 3, 3), nonzeros=3).to_sptenmat(np.array([0]))\n", "B" ] }, diff --git a/docs/source/tutorial/class_tenmat.ipynb b/docs/source/tutorial/class_tenmat.ipynb index fdedd0ec..034c0ce9 100644 --- a/docs/source/tutorial/class_tenmat.ipynb +++ b/docs/source/tutorial/class_tenmat.ipynb @@ -53,7 +53,7 @@ "outputs": [], "source": [ "# Dims [0,1] map to rows, [2,3] to columns.\n", - "A = ttb.tenmat.from_tensor_type(X, np.array([0, 1]), np.array([2, 3]))\n", + "A = X.to_tenmat(np.array([0, 1]), np.array([2, 3]))\n", "A" ] }, @@ -63,7 +63,7 @@ "metadata": {}, "outputs": [], "source": [ - "B = ttb.tenmat.from_tensor_type(X, np.array([1, 0]), np.array([2, 3])) # Order matters!\n", + "B = X.to_tenmat(np.array([1, 0]), np.array([2, 3])) # Order matters!\n", "B" ] }, @@ -73,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "C = ttb.tenmat.from_tensor_type(X, np.array([0, 1]), np.array([3, 2]))\n", + "C = X.to_tenmat(np.array([0, 1]), np.array([3, 2]))\n", "C" ] }, @@ -92,9 +92,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1])\n", - ") # np.array([1]) passed to the `rdims` parameter\n", + "A = X.to_tenmat(np.array([1])) # np.array([1]) passed to the `rdims` parameter\n", "A" ] }, @@ -114,7 +112,7 @@ "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", "# Same as A = ttb.tenmat.from_tensor_type(X, np.array([0,3]), np.array([1,2]))\n", - "A = ttb.tenmat.from_tensor_type(X, cdims=np.array([1, 2]))\n", + "A = X.to_tenmat(cdims=np.array([1, 2]))\n", "A" ] }, @@ -132,9 +130,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, cdims=np.arange(0, 4)\n", - ") # Map all the dimensions to the columns\n", + "A = X.to_tenmat(cdims=np.arange(0, 4)) # Map all the dimensions to the columns\n", "A" ] }, @@ -153,9 +149,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([2])\n", - ") # By default, columns are ordered as [0, 1, 3].\n", + "A = X.to_tenmat(np.array([2])) # By default, columns are ordered as [0, 1, 3].\n", "A" ] }, @@ -166,9 +160,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), np.array([2, 0, 3])\n", - ") # Explicit specification.\n", + "A = X.to_tenmat(np.array([1]), np.array([2, 0, 3])) # Explicit specification.\n", "A" ] }, @@ -178,9 +170,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"fc\"\n", - ") # Forward cyclic, [2,3,0].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"fc\") # Forward cyclic, [2,3,0].\n", "A" ] }, @@ -190,9 +180,7 @@ "metadata": {}, "outputs": [], "source": [ - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A" ] }, @@ -210,9 +198,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A.data # The 2D numpy array itself." ] }, @@ -257,10 +243,8 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", - "B = ttb.tenmat.from_data(A.data, A.rindices, A.cindices, A.tshape)\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", + "B = ttb.tenmat(A.data, A.rindices, A.cindices, A.tshape)\n", "B # Recreates A." ] }, @@ -295,9 +279,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A.double() # Converts A to a standard 2D numpy array." ] }, @@ -315,9 +297,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "Y = A.to_tensor()\n", "Y" ] @@ -336,9 +316,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A.shape # 2D numpy array shape." ] }, @@ -365,9 +343,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A[1, 0] # Returns the (1,0) element of the 2D numpy array" ] }, @@ -385,9 +361,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A[0:2, 0:2] = np.ones((2, 2))\n", "A" ] @@ -422,9 +396,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "A.norm() # Norm of the 2D numpy array." ] }, @@ -488,9 +460,7 @@ "outputs": [], "source": [ "X = ttb.tensor(np.arange(1, 25), shape=(3, 2, 2, 2)) # Create a tensor.\n", - "A = ttb.tenmat.from_tensor_type(\n", - " X, np.array([1]), cdims_cyclic=\"bc\"\n", - ") # Backward cyclic, [0,3,2].\n", + "A = X.to_tenmat(np.array([1]), cdims_cyclic=\"bc\") # Backward cyclic, [0,3,2].\n", "B = A * A.ctranspose() # Tenmat that is the product of two tenmats.\n", "B" ] diff --git a/pyttb/hosvd.py b/pyttb/hosvd.py index e3b04871..55ddc942 100644 --- a/pyttb/hosvd.py +++ b/pyttb/hosvd.py @@ -99,7 +99,7 @@ def hosvd( # noqa: PLR0912,PLR0913,PLR0915 for k in dimorder: # Compute Gram matrix - Yk = ttb.tenmat.from_tensor_type(Y, np.array([k])).double() + Yk = Y.to_tenmat(np.array([k])).double() Z = np.dot(Yk, Yk.transpose()) # Compute eigenvalue decomposition diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index 396aeb27..2a467b67 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -7,7 +7,7 @@ from enum import Enum from inspect import signature -from typing import List, Optional, Tuple, Union, get_args, overload +from typing import List, Literal, Optional, Tuple, Union, get_args, overload import numpy as np @@ -879,3 +879,45 @@ def get_mttkrp_factors( assert len(U) == ndims, "List of factor matrices is the wrong length" return U + + +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]: + alldims = np.array([range(ndims)]) + + if rdims is not None and cdims is None: + # Single row mapping + if len(rdims) == 1 and cdims_cyclic is not None: + # TODO we should be able to remove this since we can just specify + # cdims alone + if cdims_cyclic == "t": + cdims = rdims + rdims = np.setdiff1d(alldims, rdims) + elif cdims_cyclic == "fc": + cdims = np.array( + [i for i in range(rdims[0] + 1, ndims)] + + [i for i in range(rdims[0])] + ) + elif cdims_cyclic == "bc": + cdims = np.array( + [i for i in range(rdims[0] - 1, -1, -1)] + + [i for i in range(ndims - 1, rdims[0], -1)] + ) + else: + assert False, ( + "Unrecognized value for cdims_cyclic pattern, " + 'must be "fc" or "bc".' + ) + else: + # Multiple row mapping + cdims = np.setdiff1d(alldims, rdims) + + elif rdims is None and cdims is not None: + rdims = np.setdiff1d(alldims, cdims) + + assert rdims is not None and cdims is not None + return rdims.astype(int), cdims.astype(int) diff --git a/pyttb/sptenmat.py b/pyttb/sptenmat.py index 69f3c014..8e8f6471 100644 --- a/pyttb/sptenmat.py +++ b/pyttb/sptenmat.py @@ -5,17 +5,17 @@ """Classes and functions for working with Kruskal tensors.""" from __future__ import annotations -from typing import Literal, Optional, Tuple, Union +from typing import Optional, Tuple, Union import numpy as np from numpy_groupies import aggregate as accumarray from scipy import sparse import pyttb as ttb -from pyttb.pyttb_utils import tt_ind2sub, tt_sub2ind +from pyttb.pyttb_utils import gather_wrap_dims, tt_ind2sub -class sptenmat(object): +class sptenmat: """ SPTENMAT Store sparse tensor as a sparse matrix. @@ -23,19 +23,8 @@ class sptenmat(object): __slots__ = ("tshape", "rdims", "cdims", "subs", "vals") - def __init__(self): - """ - Construct an empty :class:`pyttb.sptenmat` - """ - self.tshape = () - self.rdims = np.array([]) - self.cdims = np.array([]) - self.subs = np.array([], ndmin=2, dtype=int) - self.vals = np.array([], ndmin=2) - - @classmethod - def from_data( # noqa: PLR0913 - cls, + def __init__( # noqa: PLR0913 + self, subs: Optional[np.ndarray] = None, vals: Optional[np.ndarray] = None, rdims: Optional[np.ndarray] = None, @@ -50,29 +39,74 @@ def from_data( # noqa: PLR0913 Parameters ---------- subs: - Location of non-zero entries + Location of non-zero entries, in sptenmat. vals: - Values for non-zero entries + Values for non-zero entries, in sptenmat. rdims: - Mapping of row indices + Mapping of row indices. cdims: - Mapping of column indices + Mapping of column indices. tshape: - Shape of the original tensor + Shape of the original tensor. + + Examples + -------- + Create an empty :class:`pyttb.sptenmat`: + + >>> S = ttb.sptenmat() + >>> S # doctest: +NORMALIZE_WHITESPACE + sptenmat corresponding to a sptensor of shape () with 0 nonzeros + rdims = [ ] (modes of sptensor corresponding to rows) + cdims = [ ] (modes of sptensor corresponding to columns) + + Create a :class:`pyttb.sptenmat` from subscripts, values, and unwrapping + dimensions: + + >>> subs = np.array([[1, 6], [1, 7]]) + >>> vals = np.array([[6], [7]]) + >>> tshape = (4, 4, 4) + >>> S = ttb.sptenmat(\ + subs,\ + vals,\ + rdims=np.array([0]),\ + cdims=np.array([1,2]),\ + tshape=tshape\ + ) + >>> S # doctest: +NORMALIZE_WHITESPACE + sptenmat corresponding to a sptensor of shape (4, 4, 4) with 2 nonzeros + rdims = [ 0 ] (modes of sptensor corresponding to rows) + cdims = [ 1, 2 ] (modes of sptensor corresponding to columns) + [1, 6] = 6 + [1, 7] = 7 """ + # Empty case + if rdims is None and cdims is None: + assert ( + subs is None and vals is None + ), "Must provide rdims or cdims with values" + self.subs = np.array([], ndmin=2, dtype=int) + self.vals = np.array([], ndmin=2) + self.rdims = np.array([], dtype=int) + self.cdims = np.array([], dtype=int) + self.tshape: Union[Tuple[()], Tuple[int, ...]] = () + return + if subs is None: subs = np.array([], ndmin=2, dtype=int) if vals is None: vals = np.array([], ndmin=2) - if rdims is None: - rdims = np.array([], dtype=int) - if cdims is None: - cdims = np.array([], dtype=int) n = len(tshape) alldims = np.array([range(n)]) # Error check - dims = np.hstack([rdims, cdims], dtype=int) + rdims, cdims = gather_wrap_dims(n, rdims, cdims) + # if rdims or cdims is empty, hstack will output an array of float not int + if rdims.size == 0: + dims = cdims.copy() + elif cdims.size == 0: + dims = rdims.copy() + else: + dims = np.hstack([rdims, cdims], dtype=int) assert len(dims) == n and (alldims == np.sort(dims)).all(), ( "Incorrect specification of dimensions, the sorted concatenation of " "rdims and cdims must be range(len(tshape))." @@ -94,7 +128,9 @@ def from_data( # noqa: PLR0913 newsubs, loc = np.unique(subs, axis=0, return_inverse=True) # Sum the corresponding values # Squeeze to convert from column vector to row vector - newvals = accumarray(loc, np.squeeze(vals), size=newsubs.shape[0], func=sum) + newvals = accumarray( + loc, np.squeeze(vals, axis=1), size=newsubs.shape[0], func=sum + ) # Find the nonzero indices of the new values nzidx = np.nonzero(newvals) @@ -104,107 +140,11 @@ def from_data( # noqa: PLR0913 if newvals.size > 0: newvals = newvals[:, None] - sptenmatInstance = cls() - sptenmatInstance.tshape = tshape - sptenmatInstance.rdims = rdims.copy().astype(int) - sptenmatInstance.cdims = cdims.copy().astype(int) - sptenmatInstance.subs = newsubs - sptenmatInstance.vals = newvals - return sptenmatInstance - - @classmethod - def from_tensor_type( # noqa: PLR0912 - cls, - source: Union[ttb.sptensor, ttb.sptenmat], - rdims: Optional[np.ndarray] = None, - cdims: Optional[np.ndarray] = None, - cdims_cyclic: Optional[ - Union[Literal["fc"], Literal["bc"], Literal["t"]] - ] = None, - ): - valid_sources = (sptenmat, ttb.sptensor) - assert isinstance(source, valid_sources), ( - "Can only generate sptenmat from " - f"{[src.__name__ for src in valid_sources]} but received {type(source)}." - ) - # Copy Constructor - if isinstance(source, sptenmat): - return cls().from_data( - source.subs.copy(), - source.vals.copy(), - source.rdims.copy(), - source.cdims.copy(), - source.tshape, - ) - - if isinstance(source, ttb.sptensor): - n = source.ndims - alldims = np.array([range(n)]) - - if rdims is not None and cdims is None: - # Single row mapping - if len(rdims) == 1 and cdims_cyclic is not None: - if cdims_cyclic == "t": - cdims = rdims - rdims = np.setdiff1d(alldims, rdims) - elif cdims_cyclic == "fc": - # cdims = [rdims+1:n, 1:rdims-1]; - cdims = np.array( - [i for i in range(rdims[0] + 1, n)] - + [i for i in range(rdims[0])] - ) - elif cdims_cyclic == "bc": - # cdims = [rdims-1:-1:1, n:-1:rdims+1]; - cdims = np.array( - [i for i in range(rdims[0] - 1, -1, -1)] - + [i for i in range(n - 1, rdims[0], -1)] - ) - else: - assert False, ( - "Unrecognized value for cdims_cyclic pattern, " - 'must be "fc" or "bc".' - ) - else: - # Multiple row mapping - cdims = np.setdiff1d(alldims, rdims) - - elif rdims is None and cdims is not None: - rdims = np.setdiff1d(alldims, cdims) - - assert rdims is not None and cdims is not None - dims = np.hstack([rdims, cdims], dtype=int) - if not len(dims) == n or not (alldims == np.sort(dims)).all(): - assert False, ( - "Incorrect specification of dimensions, the sorted " - "concatenation of rdims and cdims must be range(source.ndims)." - ) - - rsize = np.array(source.shape)[rdims] - csize = np.array(source.shape)[cdims] - - if rsize.size == 0: - ridx = np.zeros((source.nnz, 1)) - elif source.subs.size == 0: - ridx = np.array([], dtype=int) - else: - ridx = tt_sub2ind(rsize, source.subs[:, rdims]) - ridx = ridx.reshape((ridx.size, 1)).astype(int) - - if csize.size == 0: - cidx = np.zeros((source.nnz, 1)) - elif source.subs.size == 0: - cidx = np.array([], dtype=int) - else: - cidx = tt_sub2ind(csize, source.subs[:, cdims]) - cidx = cidx.reshape((cidx.size, 1)).astype(int) - - return cls().from_data( - np.hstack([ridx, cidx], dtype=int), - source.vals.copy(), - rdims.astype(int), - cdims.astype(int), - source.shape, - ) + self.tshape = tshape + self.rdims = rdims.copy().astype(int) + self.cdims = cdims.copy().astype(int) + self.subs = newsubs + self.vals = newvals @classmethod def from_array( @@ -229,6 +169,28 @@ def from_array( Mapping of column indices. tshape: Shape of the original tensor. + + Examples + -------- + Create a :class:`pyttb.sptenmat` from a sparse matrix and unwrapping + dimensions. Infer column dimensions from row dimensions specification. + + >>> data = np.array([6, 7]) + >>> rows = np.array([1, 1]) + >>> cols = np.array([6, 7]) + >>> sparse_matrix = sparse.coo_matrix((data, (rows, cols))) + >>> tshape = (4, 4, 4) + >>> S = ttb.sptenmat.from_array(\ + sparse_matrix,\ + rdims=np.array([0]),\ + tshape=tshape\ + ) + >>> S # doctest: +NORMALIZE_WHITESPACE + sptenmat corresponding to a sptensor of shape (4, 4, 4) with 2 nonzeros + rdims = [ 0 ] (modes of sptensor corresponding to rows) + cdims = [ 1, 2 ] (modes of sptensor corresponding to columns) + [1, 6] = 6 + [1, 7] = 7 """ vals = None if isinstance(array, np.ndarray): @@ -240,7 +202,38 @@ def from_array( f"Expected sparse matrix or array but received: {type(array)}" ) subs = np.vstack(array.nonzero()).transpose() - return ttb.sptenmat.from_data(subs, vals, rdims, cdims, tshape) + return ttb.sptenmat(subs, vals, rdims, cdims, tshape) + + def copy(self) -> sptenmat: + """ + Return a deep copy of the :class:`pyttb.sptenmat`. + + Examples + -------- + Create a :class:`pyttb.sptenmat` (ST1) and make a deep copy. Verify + the deep copy (ST3) is not just a reference (like ST2) to the original. + + >>> S1 = ttb.sptensor(shape=(2,2)) + >>> S1[0,0] = 1 + >>> ST1 = S1.to_sptenmat(np.array([0])) + >>> ST2 = ST1 + >>> ST3 = ST1.copy() + >>> ST1[0,0] = 3 + >>> ST1.to_sptensor().isequal(ST2.to_sptensor()) + True + >>> ST1.to_sptensor().isequal(ST3.to_sptensor()) + False + """ + return sptenmat( + self.subs.copy(), + self.vals.copy(), + self.rdims.copy(), + self.cdims.copy(), + self.tshape, + ) + + def __deepcopy__(self, memo): + return self.copy() def to_sptensor(self) -> ttb.sptensor: """ @@ -287,9 +280,7 @@ def full(self) -> ttb.tenmat: Convert a :class:`pyttb.sptenmat` to a (dense) :class:`pyttb.tenmat`. """ # Create empty dense tenmat - result = ttb.tenmat.from_data( - np.zeros(self.shape), self.rdims, self.cdims, self.tshape - ) + result = ttb.tenmat(np.zeros(self.shape), self.rdims, self.cdims, self.tshape) # Assign nonzero values result[tuple(self.subs.transpose())] = np.squeeze(self.vals) return result @@ -308,17 +299,33 @@ def norm(self) -> np.floating: """ return np.linalg.norm(self.vals) + def isequal(self, other: sptenmat) -> bool: + """ + Exact equality for :class:`pyttb.sptenmat` + """ + if not isinstance(other, ttb.sptenmat): + raise ValueError( + f"Can only compares against other sptenmat but received: {type(other)}" + ) + return ( + np.array_equal(self.vals, other.vals) + and np.array_equal(self.subs, other.subs) + and self.tshape == other.tshape + and np.array_equal(self.cdims, other.cdims) + and np.array_equal(self.rdims, other.rdims) + ) + def __pos__(self): """ Unary plus operator (+). """ - return self.from_tensor_type(self) + return self.copy() def __neg__(self): """ Unary minus operator (-). """ - result = self.from_tensor_type(self) + result = self.copy() result.vals *= -1 return result diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index fd4fb1b3..de1f4d23 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -30,6 +30,7 @@ import pyttb as ttb from pyttb.pyttb_utils import ( IndexVariant, + gather_wrap_dims, get_index_variant, get_mttkrp_factors, tt_dimscheck, @@ -687,6 +688,121 @@ def full(self) -> ttb.tensor: B[idx.astype(int)] = self.vals.transpose()[0] return B + def to_sptenmat( + self, + rdims: Optional[np.ndarray] = None, + cdims: Optional[np.ndarray] = None, + cdims_cyclic: Optional[ + Union[Literal["fc"], Literal["bc"], Literal["t"]] + ] = None, + ) -> ttb.sptenmat: + """ + Construct a :class:`pyttb.sptenmat` from a :class:`pyttb.sptensor` and + unwrapping details. + + Parameters + ---------- + 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. + + Examples + -------- + Create a :class:`pyttb.sptensor`. + + >>> subs = np.array([[1, 2, 1], [1, 3, 1]]) + >>> vals = np.array([[6], [7]]) + >>> tshape = (4, 4, 4) + >>> S = ttb.sptensor(subs, vals, tshape) + + Convert to a :class:`pyttb.sptenmat` unwrapping around the first dimension. + Either allow for implicit column or explicit column dimension + specification. + + >>> ST1 = S.to_sptenmat(rdims=np.array([0])) + >>> ST2 = S.to_sptenmat(rdims=np.array([0]), cdims=np.array([1, 2])) + >>> ST1.isequal(ST2) + True + + Convert using cyclic column ordering. For the three mode case _fc_ is the same + result. + + >>> ST3 = S.to_sptenmat(rdims=np.array([0]), cdims_cyclic="fc") + >>> ST3 # doctest: +NORMALIZE_WHITESPACE + sptenmat corresponding to a sptensor of shape (4, 4, 4) with 2 nonzeros + rdims = [ 0 ] (modes of sptensor corresponding to rows) + cdims = [ 1, 2 ] (modes of sptensor corresponding to columns) + [1, 6] = 6 + [1, 7] = 7 + + Backwards cyclic reverses the order. + + >>> ST4 = S.to_sptenmat(rdims=np.array([0]), cdims_cyclic="bc") + >>> ST4 # doctest: +NORMALIZE_WHITESPACE + sptenmat corresponding to a sptensor of shape (4, 4, 4) with 2 nonzeros + rdims = [ 0 ] (modes of sptensor corresponding to rows) + cdims = [ 2, 1 ] (modes of sptensor corresponding to columns) + [1, 9] = 6 + [1, 13] = 7 + """ + n = self.ndims + alldims = np.array([range(n)]) + + rdims, cdims = gather_wrap_dims(self.ndims, rdims, cdims, cdims_cyclic) + dims = np.hstack([rdims, cdims], dtype=int) + if not len(dims) == n or not (alldims == np.sort(dims)).all(): + assert False, ( + "Incorrect specification of dimensions, the sorted " + "concatenation of rdims and cdims must be range(source.ndims)." + ) + + rsize = np.array(self.shape)[rdims] + csize = np.array(self.shape)[cdims] + + if rsize.size == 0: + ridx = np.zeros((self.nnz, 1)) + elif self.subs.size == 0: + ridx = np.array([], dtype=int) + else: + ridx = tt_sub2ind(rsize, self.subs[:, rdims]) + ridx = ridx.reshape((ridx.size, 1)).astype(int) + + if csize.size == 0: + cidx = np.zeros((self.nnz, 1)) + elif self.subs.size == 0: + cidx = np.array([], dtype=int) + else: + cidx = tt_sub2ind(csize, self.subs[:, cdims]) + cidx = cidx.reshape((cidx.size, 1)).astype(int) + + return ttb.sptenmat( + np.hstack([ridx, cidx], dtype=int), + self.vals.copy(), + rdims.astype(int), + cdims.astype(int), + self.shape, + ) + def innerprod( self, other: Union[sptensor, ttb.tensor, ttb.ktensor, ttb.ttensor] ) -> float: @@ -3460,9 +3576,7 @@ def ttm( siz[final_dim] = matrices.shape[0] # Compute self[mode]' - Xnt = ttb.sptenmat.from_tensor_type( - self, np.array([final_dim]), cdims_cyclic="t" - ) + Xnt = self.to_sptenmat(np.array([final_dim]), cdims_cyclic="t") # Convert to sparse matrix and do multiplication; generally result is sparse Z = Xnt.double().dot(matrices.transpose()) diff --git a/pyttb/tenmat.py b/pyttb/tenmat.py index 4d6da1bc..24d3cab7 100644 --- a/pyttb/tenmat.py +++ b/pyttb/tenmat.py @@ -5,11 +5,12 @@ # U.S. Government retains certain rights in this software. from __future__ import annotations -from typing import Literal, Optional, Tuple, Union +from typing import Optional, Tuple, Union import numpy as np import pyttb as ttb +from pyttb.pyttb_utils import gather_wrap_dims class tenmat: @@ -20,59 +21,89 @@ class tenmat: __slots__ = ("tshape", "rindices", "cindices", "data") - def __init__(self): - """ - Create empty tenmat. - """ - - # Case 0a: Empty Contructor - self.tshape = () - self.rindices = np.array([]) - self.cindices = np.array([]) - self.data = np.array([]) - - @classmethod - def from_data( - cls, - data: np.ndarray, - rdims: np.ndarray, + def __init__( # noqa: PLR0912 + self, + data: Optional[np.ndarray] = None, + rdims: Optional[np.ndarray] = None, cdims: Optional[np.ndarray] = None, tshape: Optional[Tuple[int, ...]] = None, - ) -> tenmat: + ): """ - Creates a tenmat from explicit description. + Construct a :class:`pyttb.tenmat` from explicit components. + If you already have a tensor see :method:`pyttb.tensor.to_tenmat`. Parameters ---------- data: - Tensor source data + Flattened tensor data. rdims: + Which dimensions of original tensor map to rows. cdims: + Which dimensions of original tensor map to columns. tshape: + Original tensor shape. - Returns - ------- - Constructed tenmat - """ - # CONVERT A MULTIDIMENSIONAL ARRAY + Examples + -------- + Create an empty :class:`pyttb.tenmat`. - # Verify that data is a numeric numpy.ndarray - if not isinstance(data, np.ndarray) or not issubclass( - data.dtype.type, np.number - ): - assert False, "First argument must be a numeric numpy.ndarray." + >>> ttb.tenmat() # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape () + rindices = [ ] (modes of tensor corresponding to rows) + cindices = [ ] (modes of tensor corresponding to columns) + data = [] + + Create tensor shaped data. + + >>> tshape = (2, 2, 2) + >>> data = np.reshape(np.arange(np.prod(tshape), dtype=np.double), tshape) + >>> data # doctest: +NORMALIZE_WHITESPACE + array([[[0., 1.], + [2., 3.]], + [[4., 5.], + [6., 7.]]]) + + Manually matrize the tensor. + + >>> flat_data = np.reshape(data, (2,4), order="F") + >>> flat_data # doctest: +NORMALIZE_WHITESPACE + array([[0., 2., 1., 3.], + [4., 6., 5., 7.]]) + + Encode matrication into :class:`pyttb.tenmat`. + + >>> tm = ttb.tenmat(flat_data, rdims=np.array([0]), tshape=tshape) + + Extract original tensor shaped data. + + >>> tm.to_tensor().double() # doctest: +NORMALIZE_WHITESPACE + array([[[0., 1.], + [2., 3.]], + [[4., 5.], + [6., 7.]]]) + """ + # Case 0a: Empty Contructor # data is empty, return empty tenmat unless rdims, cdims, or tshape are # not empty - if data.size == 0: - cdims_empty = cdims is None or not cdims.size == 0 + if data is None or (isinstance(data, np.ndarray) and data.size == 0): + cdims_empty = cdims is None or cdims.size == 0 + rdims_empty = rdims is None or rdims.size == 0 tshape_empty = tshape is None or tshape == () - if not rdims.size == 0 or cdims_empty or not tshape_empty: - assert ( - False - ), "When data is empty, rdims, cdims, and tshape must also be empty." - else: - return cls() + assert ( + rdims_empty and cdims_empty and tshape_empty + ), "When data is empty, rdims, cdims, and tshape must also be empty." + + self.tshape: Union[Tuple[()], Tuple[int, ...]] = () + self.rindices = np.array([]) + self.cindices = np.array([]) + self.data = np.array([], ndmin=2, order="F") + return + + # Verify that data is a numeric numpy.ndarray + assert isinstance(data, np.ndarray) and issubclass( + data.dtype.type, np.number + ), "First argument must be a numeric numpy.ndarray." # data is 1d array, must convert to 2d array for tenmat if len(data.shape) == 1: @@ -82,9 +113,10 @@ def from_data( # make data a 2d array with shape (1, data.shape[0]), i.e., a row vector data = np.reshape(data.copy(), (1, data.shape[0]), order="F") - # data is ndarray and only rdims is specified - if cdims is None: - return ttb.tenmat.from_tensor_type(ttb.tensor(data), rdims) + if len(data.shape) != 2: + raise ValueError( + f"Data must be a matrix or vector but had {len(data.shape)} dimensions" + ) # use data.shape for tshape if not provided if tshape is None: @@ -99,6 +131,10 @@ def from_data( "not match" ) + n = len(tshape) + alldims = np.array([range(n)]) + rdims, cdims = gather_wrap_dims(n, rdims, cdims) + # check that data.shape and product of dimensions agree if not np.prod(np.array(tshape)[rdims]) * np.prod( np.array(tshape)[cdims] @@ -107,115 +143,93 @@ def from_data( False ), "data.shape does not match shape specified by rdims, cdims, and tshape." - return ttb.tenmat.from_tensor_type(ttb.tensor(data, tshape), rdims, cdims) + # if rdims or cdims is empty, hstack will output an array of float not int + if rdims.size == 0: + dims = cdims.copy() + elif cdims.size == 0: + dims = rdims.copy() + else: + dims = np.hstack([rdims, cdims]) + if not len(dims) == n or not (alldims == np.sort(dims)).all(): + assert False, ( + "Incorrect specification of dimensions, the sorted concatenation " + "of rdims and cdims must be range(source.ndims)." + ) + + self.tshape = tshape + self.rindices = rdims.copy() + self.cindices = cdims.copy() + self.data = np.asfortranarray(data.copy()) + return - @classmethod - def from_tensor_type( # noqa: PLR0912 - cls, - source: Union[ttb.tensor, tenmat], - rdims: Optional[np.ndarray] = None, - cdims: Optional[np.ndarray] = None, - cdims_cyclic: Optional[Union[Literal["fc"], Literal["bc"]]] = None, - ): + def copy(self) -> tenmat: """ - Converts other tensor types into a tenmat + Return a deep copy of the :class:`pyttb.tenmat`. - Parameters - ---------- - source: - Tensor type to create dense tensor from - rdims: - cdims: - cdims_cyclic: + Examples + -------- + Create a :class:`pyttb.tenmat` (TM1) and make a deep copy. Verify + the deep copy (TM3) is not just a reference (like TM2) to the original. - Returns - ------- - Constructed tenmat - """ - # Case 0b: Copy Constructor - if isinstance(source, tenmat): - # Create tenmat - tenmatInstance = cls() - tenmatInstance.tshape = source.tshape - tenmatInstance.rindices = source.rindices.copy() - tenmatInstance.cindices = source.cindices.copy() - tenmatInstance.data = source.data.copy() - return tenmatInstance + >>> T1 = ttb.tensor(np.ones((3,2))) + >>> TM1 = T1.to_tenmat(np.array([0])) + >>> TM2 = TM1 + >>> TM3 = TM1.copy() + >>> TM1[0,0] = 3 + >>> TM1[0,0] == TM2[0,0] + True + >>> TM1[0,0] == TM3[0,0] + False + """ + # Create tenmat + tenmatInstance = tenmat() + tenmatInstance.tshape = self.tshape + tenmatInstance.rindices = self.rindices.copy() + tenmatInstance.cindices = self.cindices.copy() + tenmatInstance.data = self.data.copy() + return tenmatInstance - # Case III: Convert a tensor to a tenmat - if isinstance(source, ttb.tensor): - n = source.ndims - alldims = np.array([range(n)]) - tshape = source.shape - - # Verify inputs - if rdims is None and cdims is None: - assert False, "Either rdims or cdims or both must be specified." - if rdims is not None and not sum(np.in1d(rdims, alldims)) == len(rdims): - assert False, "Values in rdims must be in [0, source.ndims]." - if cdims is not None and not sum(np.in1d(cdims, alldims)) == len(cdims): - assert False, "Values in cdims must be in [0, source.ndims]." - - if rdims is not None and cdims is None: - # Single row mapping - if len(rdims) == 1 and cdims_cyclic is not None: - if cdims_cyclic == "fc": - # cdims = [rdims+1:n, 1:rdims-1]; - cdims = np.array( - list(range(rdims[0] + 1, n)) + list(range(rdims[0])) - ) - elif cdims_cyclic == "bc": - # cdims = [rdims-1:-1:1, n:-1:rdims+1]; - cdims = np.array( - list(range(rdims[0] - 1, -1, -1)) - + list(range(n - 1, rdims[0], -1)) - ) - else: - assert False, ( - "Unrecognized value for cdims_cyclic pattern, must be " - '"fc" or "bc".' - ) - - else: - # Multiple row mapping - cdims = np.setdiff1d(alldims, rdims) - - elif rdims is None and cdims is not None: - rdims = np.setdiff1d(alldims, cdims) - - # Making typing happy - assert rdims is not None - assert cdims is not None - # if rdims or cdims is empty, hstack will output an array of float not int - if rdims.size == 0: - dims = cdims.copy() - elif cdims.size == 0: - dims = rdims.copy() - else: - dims = np.hstack([rdims, cdims]) - if not len(dims) == n or not (alldims == np.sort(dims)).all(): - assert False, ( - "Incorrect specification of dimensions, the sorted concatenation " - "of rdims and cdims must be range(source.ndims)." - ) + def __deepcopy__(self, memo): + return self.copy() - rprod = 1 if rdims.size == 0 else np.prod(np.array(tshape)[rdims]) - cprod = 1 if cdims.size == 0 else np.prod(np.array(tshape)[cdims]) - data = np.reshape(source.permute(dims).data, (rprod, cprod), order="F") + def to_tensor(self) -> ttb.tensor: + """ + Return copy of tenmat data as a tensor. - # Create tenmat - tenmatInstance = cls() - tenmatInstance.tshape = tshape - tenmatInstance.rindices = rdims.copy() - tenmatInstance.cindices = cdims.copy() - tenmatInstance.data = data.copy() - return tenmatInstance - raise ValueError( - f"Can only create tenmat from tensor or tenmat but recieved {type(source)}" - ) + Examples + -------- + Create tensor shaped data. - def to_tensor(self) -> ttb.tensor: - """Return copy of tenmat data as a tensor""" + >>> tshape = (2, 2, 2) + >>> data = np.reshape(np.arange(np.prod(tshape), dtype=np.double), tshape) + >>> data # doctest: +NORMALIZE_WHITESPACE + array([[[0., 1.], + [2., 3.]], + [[4., 5.], + [6., 7.]]]) + + Manually matrize the tensor. + + >>> flat_data = np.reshape(data, (2,4), order="F") + >>> flat_data # doctest: +NORMALIZE_WHITESPACE + array([[0., 2., 1., 3.], + [4., 6., 5., 7.]]) + + Encode matrication into :class:`pyttb.tenmat`. + + >>> tm = ttb.tenmat(flat_data, rdims=np.array([0]), tshape=tshape) + + Extract original tensor shaped data. + + >>> tm.to_tensor() # doctest: +NORMALIZE_WHITESPACE + tensor of shape (2, 2, 2) + data[0, :, :] = + [[0. 1.] + [2. 3.]] + data[1, :, :] = + [[4. 5.] + [6. 7.]] + """ # RESHAPE TENSOR-AS-MATRIX # Here we just reverse what was done in the tenmat constructor. # First we reshape the data to be an MDA, then we un-permute @@ -231,14 +245,29 @@ def ctranspose(self) -> tenmat: """ Complex conjugate transpose for tenmat. - Parameters - ---------- - - Returns - ------- - :class:`pyttb.tenmat` + Examples + -------- + Create :class:`pyttb.tensor` then convert to :class:`pyttb.tenmat`. + + >>> T = ttb.tenones((2,2,2)) + >>> TM = T.to_tenmat(rdims=np.array([0])) + >>> TM # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 0 ] (modes of tensor corresponding to rows) + cindices = [ 1, 2 ] (modes of tensor corresponding to columns) + data[:, :] = + [[1. 1. 1. 1.] + [1. 1. 1. 1.]] + >>> TM.ctranspose() # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 1, 2 ] (modes of tensor corresponding to rows) + cindices = [ 0 ] (modes of tensor corresponding to columns) + data[:, :] = + [[1. 1.] + [1. 1.] + [1. 1.] + [1. 1.]] """ - tenmatInstance = tenmat() tenmatInstance.rindices = self.cindices.copy() tenmatInstance.cindices = self.rindices.copy() @@ -250,6 +279,21 @@ def double(self) -> np.ndarray: """ Convert tenmat to an array of doubles + Examples + -------- + >>> T = ttb.tenones((2,2,2)) + >>> TM = T.to_tenmat(rdims=np.array([0])) + >>> TM # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 0 ] (modes of tensor corresponding to rows) + cindices = [ 1, 2 ] (modes of tensor corresponding to columns) + data[:, :] = + [[1. 1. 1. 1.] + [1. 1. 1. 1.]] + >>> TM.double() # doctest: +NORMALIZE_WHITESPACE + array([[1., 1., 1., 1.], + [1., 1., 1., 1.]]) + Returns ------- Copy of tenmat data. @@ -262,20 +306,51 @@ def ndims(self) -> int: return len(self.shape) def norm(self) -> float: - """Frobenius norm of a tenmat.""" + """ + Frobenius norm of a tenmat. + + Examples + -------- + >>> T = ttb.tenones((2,2,2)) + >>> TM = T.to_tenmat(rdims=np.array([0])) + >>> TM # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 0 ] (modes of tensor corresponding to rows) + cindices = [ 1, 2 ] (modes of tensor corresponding to columns) + data[:, :] = + [[1. 1. 1. 1.] + [1. 1. 1. 1.]] + >>> TM.norm() # doctest: +ELLIPSIS + 2.82... + """ # default of np.linalg.norm is to vectorize the data and compute the vector # norm, which is equivalent to the Frobenius norm for multidimensional arrays. - # However, the argument 'fro' only workks for 1-D and 2-D + # However, the argument 'fro' only works for 1-D and 2-D # arrays currently. return float(np.linalg.norm(self.data)) @property def shape(self) -> Tuple[int, ...]: """Return the shape of a tenmat""" - if self.data.shape == (0,): + if self.data.size == 0: return () return self.data.shape + def isequal(self, other: tenmat) -> bool: + """ + Exact equality for :class:`pyttb.tenmat` + """ + if not isinstance(other, ttb.tenmat): + raise ValueError( + f"Can only compares against other tenmat but received: {type(other)}" + ) + return ( + np.array_equal(self.data, other.data) + and self.tshape == other.tshape + and np.array_equal(self.rindices, other.rindices) + and np.array_equal(self.cindices, other.cindices) + ) + def __setitem__(self, key, value): """ SUBSASGN Subscripted assignment for a tensor. @@ -310,7 +385,7 @@ def __mul__(self, other): """ # One argument is a scalar if np.isscalar(other): - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = Z.data * other return Z if isinstance(other, tenmat): @@ -371,7 +446,7 @@ def __add__(self, other): # One argument is a scalar if np.isscalar(other): - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = Z.data + other return Z if isinstance(other, tenmat): @@ -379,7 +454,7 @@ def __add__(self, other): if not self.shape == other.shape: assert False, "tenmat shape mismatch." - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = Z.data + other.data return Z assert False, "tenmat addition only valid with scalar or tenmat objects." @@ -413,7 +488,7 @@ def __sub__(self, other): # One argument is a scalar if np.isscalar(other): - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = Z.data - other return Z if isinstance(other, tenmat): @@ -421,7 +496,7 @@ def __sub__(self, other): if not self.shape == other.shape: assert False, "tenmat shape mismatch." - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = Z.data - other.data return Z assert False, "tenmat subtraction only valid with scalar or tenmat objects." @@ -441,7 +516,7 @@ def __rsub__(self, other): # One argument is a scalar if np.isscalar(other): - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = other - Z.data return Z if isinstance(other, tenmat): @@ -449,7 +524,7 @@ def __rsub__(self, other): if not self.shape == other.shape: assert False, "tenmat shape mismatch." - Z = ttb.tenmat.from_tensor_type(self) + Z = self.copy() Z.data = other.data - Z.data return Z assert False, "tenmat subtraction only valid with scalar or tenmat objects." @@ -464,7 +539,7 @@ def __pos__(self): copy of tenmat """ - T = ttb.tenmat.from_tensor_type(self) + T = self.copy() return T @@ -478,7 +553,7 @@ def __neg__(self): copy of tenmat """ - T = ttb.tenmat.from_tensor_type(self) + T = self.copy() T.data = -1 * T.data return T diff --git a/pyttb/tensor.py b/pyttb/tensor.py index 6ecc1fc1..b02b40ed 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -20,6 +20,7 @@ import pyttb as ttb from pyttb.pyttb_utils import ( IndexVariant, + gather_wrap_dims, get_index_variant, get_mttkrp_factors, tt_dimscheck, @@ -259,7 +260,7 @@ def collapse( newshape = tuple(np.array(self.shape)[remdims]) ## Convert to a matrix where each row is going to be collapsed - A = ttb.tenmat.from_data(self.data, remdims, dims).double() + A = self.to_tenmat(remdims, dims).double() ## Apply the collapse function B = np.zeros((A.shape[0], 1)) @@ -452,6 +453,126 @@ def full(self) -> tensor: """ return ttb.tensor(self.data) + def to_tenmat( + self, + rdims: Optional[np.ndarray] = None, + cdims: Optional[np.ndarray] = None, + cdims_cyclic: Optional[ + Union[Literal["fc"], Literal["bc"], Literal["t"]] + ] = None, + ) -> ttb.tenmat: + """ + Construct a :class:`pyttb.tenmat` from a :class:`pyttb.tensor` and + unwrapping details. + + Parameters + ---------- + 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) + in the order range(rdims,self.ndims()) followed by range(0, rdims). + _bc_ (backward cyclic) 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. + + Examples + -------- + Create a :class:`pyttb.tensor`. + + >>> tshape = (2, 2, 2) + >>> data = np.reshape(np.arange(np.prod(tshape)), tshape) + >>> T = ttb.tensor(data) + >>> T # doctest: +NORMALIZE_WHITESPACE + tensor of shape (2, 2, 2) + data[0, :, :] = + [[0 1] + [2 3]] + data[1, :, :] = + [[4 5] + [6 7]] + + Convert to a :class:`pyttb.tenmat` unwrapping around the first dimension. + Either allow for implicit column or explicit column dimension + specification. + + >>> TM1 = T.to_tenmat(rdims=np.array([0])) + >>> TM2 = T.to_tenmat(rdims=np.array([0]), cdims=np.array([1, 2])) + >>> TM1.isequal(TM2) + True + + Convert using cyclic column ordering. For the three mode case _fc_ is the same + result. + + >>> TM3 = T.to_tenmat(rdims=np.array([0]), cdims_cyclic="fc") + >>> TM3 # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 0 ] (modes of tensor corresponding to rows) + cindices = [ 1, 2 ] (modes of tensor corresponding to columns) + data[:, :] = + [[0 2 1 3] + [4 6 5 7]] + + Backwards cyclic reverses the order. + + >>> TM4 = T.to_tenmat(rdims=np.array([0]), cdims_cyclic="bc") + >>> TM4 # doctest: +NORMALIZE_WHITESPACE + matrix corresponding to a tensor of shape (2, 2, 2) + rindices = [ 0 ] (modes of tensor corresponding to rows) + cindices = [ 2, 1 ] (modes of tensor corresponding to columns) + data[:, :] = + [[0 1 2 3] + [4 5 6 7]] + """ + n = self.ndims + alldims = np.array([range(n)]) + tshape = self.shape + + # Verify inputs + if rdims is None and cdims is None: + assert False, "Either rdims or cdims or both must be specified." + if rdims is not None and not sum(np.in1d(rdims, alldims)) == len(rdims): + assert False, "Values in rdims must be in [0, source.ndims]." + if cdims is not None and not sum(np.in1d(cdims, alldims)) == len(cdims): + assert False, "Values in cdims must be in [0, source.ndims]." + + rdims, cdims = gather_wrap_dims(n, rdims, cdims, cdims_cyclic) + # if rdims or cdims is empty, hstack will output an array of float not int + if rdims.size == 0: + dims = cdims.copy() + elif cdims.size == 0: + dims = rdims.copy() + else: + dims = np.hstack([rdims, cdims]) + if not len(dims) == n or not (alldims == np.sort(dims)).all(): + assert False, ( + "Incorrect specification of dimensions, the sorted concatenation " + "of rdims and cdims must be range(source.ndims)." + ) + rprod = 1 if rdims.size == 0 else np.prod(np.array(tshape)[rdims]) + cprod = 1 if cdims.size == 0 else np.prod(np.array(tshape)[cdims]) + data = np.reshape( + self.permute(dims).data, + (rprod, cprod), + order="F", + ) + return ttb.tenmat(data, rdims, cdims, tshape=tshape) + def innerprod( self, other: Union[tensor, ttb.sptensor, ttb.ktensor, ttb.ttensor] ) -> float: @@ -934,7 +1055,7 @@ def nvecs(self, n: int, r: int, flipsign: bool = True) -> np.ndarray: array([[ 0.4045..., 0.9145...], [ 0.9145..., -0.4045...]]) """ - Xn = ttb.tenmat.from_tensor_type(self, rdims=np.array([n])).double() + Xn = self.to_tenmat(rdims=np.array([n])).double() y = Xn @ Xn.T if r < y.shape[0] - 1: @@ -1079,16 +1200,11 @@ def scale( factor = ttb.tensor(factor, copy=False) # TODO this should probably be doable directly as a numpy view # where I think this is currently a copy - vector_factor = ttb.tenmat.from_tensor_type( - factor, np.arange(factor.ndims) - ).double() - vector_self = ttb.tenmat.from_tensor_type(self, dims, remdims).double() + vector_factor = factor.to_tenmat(np.arange(factor.ndims)).double() + vector_self = self.to_tenmat(dims, remdims).double() # Numpy broadcasting should be equivalent to bsxfun result = vector_self * vector_factor - # TODO why do we need this transpose for things to work? - if len(dims) == 1: - result = result.transpose() - return ttb.tenmat.from_data(result, dims, remdims, self.shape).to_tensor() + return ttb.tenmat(result, dims, remdims, self.shape).to_tensor() def squeeze(self) -> Union[tensor, float]: """ @@ -1453,8 +1569,8 @@ def ttt( # Compute the product # Avoid transpose by reshaping self and computing result = self * other - amatrix = ttb.tenmat.from_tensor_type(self, cdims=selfdims) - bmatrix = ttb.tenmat.from_tensor_type(other, rdims=otherdims) + amatrix = self.to_tenmat(cdims=selfdims) + bmatrix = other.to_tenmat(rdims=otherdims) cmatrix = amatrix * bmatrix # Check whether or not the result is a scalar diff --git a/pyttb/ttensor.py b/pyttb/ttensor.py index d7696270..8767f02f 100644 --- a/pyttb/ttensor.py +++ b/pyttb/ttensor.py @@ -626,20 +626,16 @@ def nvecs( # noqa: PLR0912 H = self.core.ttm(V) if isinstance(H, ttb.sptensor): - HnT = ttb.sptenmat.from_tensor_type( - H, np.array([n]), cdims_cyclic="t" - ).double() + HnT = H.to_sptenmat(np.array([n]), cdims_cyclic="t").double() else: - HnT = ttb.tenmat.from_tensor_type(H.full(), cdims=np.array([n])).double() + HnT = H.full().to_tenmat(cdims=np.array([n])).double() G = self.core if isinstance(G, ttb.sptensor): - GnT = ttb.sptenmat.from_tensor_type( - G, np.array([n]), cdims_cyclic="t" - ).double() + GnT = G.to_sptenmat(np.array([n]), cdims_cyclic="t").double() else: - GnT = ttb.tenmat.from_tensor_type(G.full(), cdims=np.array([n])).double() + GnT = G.full().to_tenmat(cdims=np.array([n])).double() # Compute Xn * Xn' # Big hack because if RHS is sparse wrong dot product is used diff --git a/tests/test_sptenmat.py b/tests/test_sptenmat.py index e7a796b9..fbfbee07 100644 --- a/tests/test_sptenmat.py +++ b/tests/test_sptenmat.py @@ -2,6 +2,8 @@ # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the # U.S. Government retains certain rights in this software. +from copy import deepcopy + import numpy as np import pytest from scipy import sparse @@ -59,7 +61,7 @@ def sample_sptenmat(): "cdims": cdims, "tshape": tshape, } - sptenmatInstance = ttb.sptenmat.from_data(subs, vals, rdims, cdims, tshape) + sptenmatInstance = ttb.sptenmat(subs, vals, rdims, cdims, tshape) return data, sptenmatInstance @@ -88,7 +90,7 @@ def test_sptenmat_initialization_from_data(sample_sptenmat): shape = (np.prod(np.array(tshape)[rdims]), np.prod(np.array(tshape)[cdims])) # Constructor from data: subs, vals, rdims, cdims, and tshape - S = ttb.sptenmat.from_data(subs, vals, rdims, cdims, tshape) + S = ttb.sptenmat(subs, vals, rdims, cdims, tshape) np.testing.assert_array_equal(S.subs, subs) np.testing.assert_array_equal(S.vals, vals) np.testing.assert_array_equal(S.rdims, rdims) @@ -97,7 +99,7 @@ def test_sptenmat_initialization_from_data(sample_sptenmat): np.testing.assert_array_equal(S.shape, shape) # Constructor from data: rdims, cdims, and tshape - S = ttb.sptenmat.from_data(rdims=rdims, cdims=cdims, tshape=tshape) + S = ttb.sptenmat(rdims=rdims, cdims=cdims, tshape=tshape) np.testing.assert_array_equal(S.subs, np.array([])) np.testing.assert_array_equal(S.vals, np.array([])) np.testing.assert_array_equal(S.rdims, rdims) @@ -108,7 +110,7 @@ def test_sptenmat_initialization_from_data(sample_sptenmat): # Constructor from data: rdims, and tshape all_rdims = np.arange(len(tshape)) rdims_shape = (np.prod(tshape), 1) - S = ttb.sptenmat.from_data(rdims=all_rdims, tshape=tshape) + S = ttb.sptenmat(rdims=all_rdims, tshape=tshape) np.testing.assert_array_equal(S.subs, np.array([])) np.testing.assert_array_equal(S.vals, np.array([])) np.testing.assert_array_equal(S.rdims, all_rdims) @@ -118,7 +120,7 @@ def test_sptenmat_initialization_from_data(sample_sptenmat): # Constructor from data: cdims, and tshape cdims_shape = (1, np.prod(tshape)) - S = ttb.sptenmat.from_data(cdims=all_rdims, tshape=tshape) + S = ttb.sptenmat(cdims=all_rdims, tshape=tshape) np.testing.assert_array_equal(S.subs, np.array([])) np.testing.assert_array_equal(S.vals, np.array([])) np.testing.assert_array_equal(S.rdims, np.array([])) @@ -134,39 +136,35 @@ def test_sptenmat_initialization_from_tensor_type( params, sptenmatInstance = sample_sptenmat params3, sptensorInstance = sample_sptensor_3way # Copy constructor - S = ttb.sptenmat.from_tensor_type(sptenmatInstance) + S = sptenmatInstance.copy() + assert S is not sptenmatInstance + assert S.isequal(sptenmatInstance) + + S = deepcopy(sptenmatInstance) assert S is not sptenmatInstance - np.testing.assert_array_equal(S.subs, sptenmatInstance.subs) - np.testing.assert_array_equal(S.vals, sptenmatInstance.vals) - np.testing.assert_array_equal(S.rdims, sptenmatInstance.rdims) - np.testing.assert_array_equal(S.cdims, sptenmatInstance.cdims) - np.testing.assert_array_equal(S.tshape, sptenmatInstance.tshape) + assert S.isequal(sptenmatInstance) # Multi-row options - S = ttb.sptenmat.from_tensor_type(sptensorInstance, rdims=np.array([0, 1])) + S = sptensorInstance.to_sptenmat(rdims=np.array([0, 1])) np.testing.assert_array_equal(S.vals, sptensorInstance.vals) np.testing.assert_array_equal(S.rdims, np.array([0, 1])) np.testing.assert_array_equal(S.cdims, np.array([2])) np.testing.assert_array_equal(S.tshape, sptensorInstance.shape) - S = ttb.sptenmat.from_tensor_type(sptensorInstance, cdims=np.array([2])) + S = sptensorInstance.to_sptenmat(cdims=np.array([2])) np.testing.assert_array_equal(S.vals, sptensorInstance.vals) np.testing.assert_array_equal(S.rdims, np.array([0, 1])) np.testing.assert_array_equal(S.cdims, np.array([2])) np.testing.assert_array_equal(S.tshape, sptensorInstance.shape) # Single row options - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims_cyclic="fc" - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims_cyclic="fc") np.testing.assert_array_equal(S.vals, sptensorInstance.vals) np.testing.assert_array_equal(S.rdims, np.array([0])) np.testing.assert_array_equal(S.cdims, np.array([1, 2])) np.testing.assert_array_equal(S.tshape, sptensorInstance.shape) - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims_cyclic="bc" - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims_cyclic="bc") np.testing.assert_array_equal(S.vals, sptensorInstance.vals) np.testing.assert_array_equal(S.rdims, np.array([0])) np.testing.assert_array_equal(S.cdims, np.array([2, 1])) @@ -174,18 +172,18 @@ def test_sptenmat_initialization_from_tensor_type( # Some fun edge cases ## Empty sptensor - S = ttb.sptenmat.from_tensor_type( - ttb.sptensor(shape=(4, 4, 4)), rdims=np.array([0]), cdims=np.array([1, 2]) + S = ttb.sptensor(shape=(4, 4, 4)).to_sptenmat( + rdims=np.array([0]), cdims=np.array([1, 2]) ) assert S.subs.size == 0 ## Only rows - S = ttb.sptenmat.from_tensor_type(sptensorInstance, rdims=np.array([0, 1, 2])) + S = sptensorInstance.to_sptenmat(rdims=np.array([0, 1, 2])) np.all(S.subs[:, 1] == 0) np.testing.assert_array_equal(S.rdims, np.array([0, 1, 2])) np.testing.assert_array_equal(S.cdims, np.array([])) np.testing.assert_array_equal(S.tshape, sptensorInstance.shape) ## Only cols - S = ttb.sptenmat.from_tensor_type(sptensorInstance, cdims=np.array([0, 1, 2])) + S = sptensorInstance.to_sptenmat(cdims=np.array([0, 1, 2])) np.all(S.subs[:, 0] == 0) np.testing.assert_array_equal(S.rdims, np.array([])) np.testing.assert_array_equal(S.cdims, np.array([0, 1, 2])) @@ -193,20 +191,16 @@ def test_sptenmat_initialization_from_tensor_type( # Negative tests with pytest.raises(AssertionError): - ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims_cyclic="bag_argument_string" + sptensorInstance.to_sptenmat( + rdims=np.array([0]), cdims_cyclic="bag_argument_string" ) with pytest.raises(AssertionError): - ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) def test_sptenmat_double(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) spmatrix = sptensorInstance.spmatrix() sptenmat_matrix = S.double() differences, _, _ = sparse.find(spmatrix - sptenmat_matrix) @@ -215,9 +209,7 @@ def test_sptenmat_double(sample_sptensor_2way): ) empty_sptensor = ttb.sptensor(shape=(4, 3)) - S = ttb.sptenmat.from_tensor_type( - empty_sptensor, rdims=np.array([0]), cdims=np.array([1]) - ) + S = empty_sptensor.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) spmatrix = empty_sptensor.spmatrix() sptenmat_matrix = S.double() differences, _, _ = sparse.find(spmatrix - sptenmat_matrix) @@ -228,9 +220,7 @@ def test_sptenmat_double(sample_sptensor_2way): def test_sptenmat_full(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) matrix = sptensorInstance.double() tenmat_matrix = S.full().double() np.testing.assert_array_equal(matrix, tenmat_matrix) @@ -238,25 +228,19 @@ def test_sptenmat_full(sample_sptensor_2way): def test_sptenmat_nnz(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) assert S.nnz == sptensorInstance.nnz def test_sptenmat_norm(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) assert S.norm() == sptensorInstance.norm() def test_sptenmat_pos(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = +ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = +sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) spmatrix = sptensorInstance.spmatrix() sptenmat_matrix = S.double() differences, _, _ = sparse.find(spmatrix - sptenmat_matrix) @@ -267,9 +251,7 @@ def test_sptenmat_pos(sample_sptensor_2way): def test_sptenmat_neg(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = -ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = -sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) spmatrix = (-sptensorInstance).spmatrix() sptenmat_matrix = S.double() differences, _, _ = sparse.find(spmatrix - sptenmat_matrix) @@ -279,9 +261,7 @@ def test_sptenmat_neg(sample_sptensor_2way): def test_sptenmat_setitem(sample_sptensor_2way): - S = ttb.sptenmat.from_tensor_type( - ttb.sptensor(shape=(4, 3)), rdims=np.array([0]), cdims=np.array([1]) - ) + S = ttb.sptensor(shape=(4, 3)).to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) with pytest.raises(IndexError): S[[0, 0]] = 1 with pytest.raises(IndexError): @@ -307,9 +287,7 @@ def test_sptenmat_setitem(sample_sptensor_2way): def test_sptenmat_to_sptensor(sample_sptensor_2way): params3, sptensorInstance = sample_sptensor_2way - S = ttb.sptenmat.from_tensor_type( - sptensorInstance, rdims=np.array([0]), cdims=np.array([1]) - ) + S = sptensorInstance.to_sptenmat(rdims=np.array([0]), cdims=np.array([1])) round_trip = S.to_sptensor() assert sptensorInstance.isequal(round_trip), ( f"Original: {sptensorInstance}\n" f"Reconstructed: {round_trip}" @@ -337,9 +315,7 @@ def test_sptenmat__str__(sample_sptensor_3way): assert s == sptenmatInstance.__str__() # Test 3D - sptenmatInstance3 = ttb.sptenmat.from_tensor_type( - sptensorInstance3, rdims, cdims, tshape - ) + sptenmatInstance3 = sptensorInstance3.to_sptenmat(rdims, cdims, tshape) s = "" s += "sptenmat corresponding to a sptensor of shape " s += f"{sptenmatInstance3.tshape!r}" @@ -362,3 +338,9 @@ def test_sptenmat__str__(sample_sptensor_3way): if i < sptenmatInstance3.subs.shape[0] - 1: s += "\n" assert s == sptenmatInstance3.__str__() + + +def test_sptenmat_isequal(): + # Negative test + with pytest.raises(ValueError): + ttb.sptenmat().isequal("Not an sptenmat") diff --git a/tests/test_tenmat.py b/tests/test_tenmat.py index 36888694..ad8db719 100644 --- a/tests/test_tenmat.py +++ b/tests/test_tenmat.py @@ -2,6 +2,8 @@ # LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the # U.S. Government retains certain rights in this software. +from copy import deepcopy + import numpy as np import pytest @@ -99,7 +101,7 @@ def test_tenmat_initialization_from_data( (_, ndarrayInstance4) = sample_ndarray_4way # Constructor from empty array, rdims, cdims, and tshape - tenmatNdarraye = ttb.tenmat.from_data(np.array([]), np.array([]), np.array([]), ()) + tenmatNdarraye = ttb.tenmat(np.array([[]]), np.array([]), np.array([]), ()) assert (tenmatNdarraye.data == np.array([])).all() assert (tenmatNdarraye.rindices == np.array([])).all() assert (tenmatNdarraye.cindices == np.array([])).all() @@ -107,88 +109,70 @@ def test_tenmat_initialization_from_data( assert tenmatNdarraye.tshape == () # Constructor from 1d array - tenmatNdarray1 = ttb.tenmat.from_data(ndarrayInstance1, rdims, cdims, tshape) - assert (tenmatNdarray1.data == tenmatInstance.data).all() + tenmatNdarray1 = ttb.tenmat(ndarrayInstance1, rdims, cdims, tshape) + assert ( + tenmatNdarray1.data + == np.reshape( + tenmatInstance.data, (1, np.prod(tenmatNdarray1.shape)), order="F" + ) + ).all() assert (tenmatNdarray1.rindices == tenmatInstance.rindices).all() assert (tenmatNdarray1.cindices == tenmatInstance.cindices).all() - assert tenmatNdarray1.shape == tenmatInstance.shape + assert np.prod(tenmatNdarray1.shape) == np.prod(tenmatInstance.shape) assert tenmatNdarray1.tshape == tenmatInstance.tshape - # Constructor from 1d array converted to 2d column vector - tenmatNdarray1c = ttb.tenmat.from_data( - np.reshape(ndarrayInstance1, (ndarrayInstance1.shape[0], 1), order="F"), + # Constructor from 1d array converted to 2d row vector + tenmatNdarray1r = ttb.tenmat( + np.reshape(ndarrayInstance1, (1, ndarrayInstance1.shape[0]), order="F"), rdims, cdims, tshape, ) - assert (tenmatNdarray1c.data == tenmatInstance.data).all() - assert (tenmatNdarray1c.rindices == tenmatInstance.rindices).all() - assert (tenmatNdarray1c.cindices == tenmatInstance.cindices).all() - assert tenmatNdarray1c.shape == tenmatInstance.shape - assert tenmatNdarray1c.tshape == tenmatInstance.tshape + assert tenmatNdarray1r.isequal(tenmatNdarray1) # Constructor from 2d array - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, tshape) - assert (tenmatNdarray2.data == tenmatInstance.data).all() - assert (tenmatNdarray2.rindices == tenmatInstance.rindices).all() - assert (tenmatNdarray2.cindices == tenmatInstance.cindices).all() - assert tenmatNdarray2.shape == tenmatInstance.shape - assert tenmatNdarray2.tshape == tenmatInstance.tshape - - ## Constructor from 4d array - tenmatNdarray4 = ttb.tenmat.from_data(ndarrayInstance4, rdims, cdims) - assert (tenmatNdarray4.data == tenmatInstance.data).all() - assert (tenmatNdarray4.rindices == tenmatInstance.rindices).all() - assert (tenmatNdarray4.cindices == tenmatInstance.cindices).all() - assert tenmatNdarray4.shape == tenmatInstance.shape - assert tenmatNdarray4.tshape == tenmatInstance.tshape - - ## Constructor from 4d array just specifying rdims - tenmatNdarray4 = ttb.tenmat.from_data(ndarrayInstance4, np.array([0])) - assert ( - tenmatNdarray4.data - == np.reshape(ndarrayInstance4, tenmatNdarray4.shape, order="F") - ).all() + tenmatNdarray2 = ttb.tenmat(ndarrayInstance2, rdims, cdims, tshape) + assert tenmatNdarray2.isequal(tenmatInstance) # Exceptions ## data is not numpy.ndarray exc = "First argument must be a numeric numpy.ndarray." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data([7], rdims, cdims, tshape) + ttb.tenmat([7], rdims, cdims, tshape) assert exc in str(excinfo) ## data is numpy.ndarray but not numeric exc = "First argument must be a numeric numpy.ndarray." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(ndarrayInstance2 > 0, rdims, cdims, tshape) + ttb.tenmat(ndarrayInstance2 > 0, rdims, cdims, tshape) assert exc in str(excinfo) # data is empty numpy.ndarray, but other params are not exc = "When data is empty, rdims, cdims, and tshape must also be empty." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(np.array([]), rdims, np.array([]), ()) + ttb.tenmat(np.array([]), rdims, np.array([]), ()) assert exc in str(excinfo) with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(np.array([]), np.array([]), cdims, ()) + ttb.tenmat(np.array([]), np.array([]), cdims, ()) assert exc in str(excinfo) with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(np.array([]), np.array([]), np.array([]), tshape) + ttb.tenmat(np.array([]), np.array([]), np.array([]), tshape) assert exc in str(excinfo) ## data is 1D numpy.ndarray exc = "tshape must be specified when data is 1d array." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(ndarrayInstance1, rdims, cdims) + ttb.tenmat(ndarrayInstance1, rdims, cdims) assert exc in str(excinfo) with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(ndarrayInstance1, rdims, cdims, None) + ttb.tenmat(ndarrayInstance1, rdims, cdims, None) assert exc in str(excinfo) # tshape is not a tuple exc = "tshape must be a tuple." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, list(tshape)) + ttb.tenmat(ndarrayInstance2, rdims, cdims, list(tshape)) assert exc in str(excinfo) # products of tshape and data.shape do not match @@ -196,9 +180,7 @@ def test_tenmat_initialization_from_data( "Incorrect dimensions specified: products of data.shape and tuple do not match" ) with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data( - ndarrayInstance2, rdims, cdims, tuple(np.array(tshape) + 1) - ) + ttb.tenmat(ndarrayInstance2, rdims, cdims, tuple(np.array(tshape) + 1)) assert exc in str(excinfo) # products of tshape and data.shape do not match @@ -215,9 +197,13 @@ def test_tenmat_initialization_from_data( # D.append([np.array([0,1,1]), np.array([3])]) for d in D: with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_data(ndarrayInstance2, d[0], d[1], tshape) + ttb.tenmat(ndarrayInstance2, d[0], d[1], tshape) assert exc in str(excinfo) + # Passing tensor shaped data + with pytest.raises(ValueError) as excinfo: + ttb.tenmat(np.ones((4, 4, 4)), np.arange(3)) + def test_tenmat_initialization_from_tensor_type( sample_tenmat_4way, sample_tensor_3way, sample_tensor_4way @@ -231,7 +217,14 @@ def test_tenmat_initialization_from_tensor_type( data = params["data"] # Copy Constructor - tenmatCopy = ttb.tenmat.from_tensor_type(tenmatInstance) + tenmatCopy = tenmatInstance.copy() + assert (tenmatCopy.data == data).all() + assert (tenmatCopy.rindices == rdims).all() + assert (tenmatCopy.cindices == cdims).all() + assert tenmatCopy.shape == data.shape + assert tenmatCopy.tshape == tshape + + tenmatCopy = deepcopy(tenmatInstance) assert (tenmatCopy.data == data).all() assert (tenmatCopy.rindices == rdims).all() assert (tenmatCopy.cindices == cdims).all() @@ -239,40 +232,26 @@ def test_tenmat_initialization_from_tensor_type( assert tenmatCopy.tshape == tshape # Constructor from tensor using rdims only - tenmatTensorRdims = ttb.tenmat.from_tensor_type(tensorInstance, rdims=rdims) - assert (tenmatInstance.data == tenmatTensorRdims.data).all() - assert (tenmatInstance.rindices == tenmatTensorRdims.rindices).all() - assert (tenmatInstance.cindices == tenmatTensorRdims.cindices).all() - assert tenmatInstance.shape == tenmatTensorRdims.shape - assert tenmatInstance.tshape == tenmatTensorRdims.tshape + tenmatTensorRdims = tensorInstance.to_tenmat(rdims=rdims) + assert tenmatInstance.isequal(tenmatTensorRdims) # Constructor from tensor using empty rdims - tenmatTensorRdims = ttb.tenmat.from_tensor_type(tensorInstance3, rdims=np.array([])) + tenmatTensorRdims = tensorInstance3.to_tenmat(rdims=np.array([])) data = np.reshape(np.arange(1, 13), (1, 12)) assert (tenmatTensorRdims.data == data).all() # Constructor from tensor using cdims only - tenmatTensorCdims = ttb.tenmat.from_tensor_type(tensorInstance, cdims=cdims) - assert (tenmatInstance.data == tenmatTensorCdims.data).all() - assert (tenmatInstance.rindices == tenmatTensorCdims.rindices).all() - assert (tenmatInstance.cindices == tenmatTensorCdims.cindices).all() - assert tenmatInstance.shape == tenmatTensorCdims.shape - assert tenmatInstance.tshape == tenmatTensorCdims.tshape + tenmatTensorCdims = tensorInstance.to_tenmat(cdims=cdims) + assert tenmatInstance.isequal(tenmatTensorCdims) # Constructor from tensor using empty cdims - tenmatTensorCdims = ttb.tenmat.from_tensor_type(tensorInstance3, cdims=np.array([])) + tenmatTensorCdims = tensorInstance3.to_tenmat(cdims=np.array([])) data = np.reshape(np.arange(1, 13), (12, 1)) assert (tenmatTensorCdims.data == data).all() # Constructor from tensor using rdims and cdims - tenmatTensorRdimsCdims = ttb.tenmat.from_tensor_type( - tensorInstance, rdims=rdims, cdims=cdims - ) - assert (tenmatInstance.data == tenmatTensorRdimsCdims.data).all() - assert (tenmatInstance.rindices == tenmatTensorRdimsCdims.rindices).all() - assert (tenmatInstance.cindices == tenmatTensorRdimsCdims.cindices).all() - assert tenmatInstance.shape == tenmatTensorRdimsCdims.shape - assert tenmatInstance.tshape == tenmatTensorRdimsCdims.tshape + tenmatTensorRdimsCdims = tensorInstance.to_tenmat(rdims=rdims, cdims=cdims) + assert tenmatInstance.isequal(tenmatTensorRdimsCdims) # Constructor from tensor using 1D rdims and cdims_cyclic='fc' (forward cyclic) rdimsFC = np.array([1]) @@ -280,9 +259,7 @@ def test_tenmat_initialization_from_tensor_type( tshapeFC = (2, 2, 2, 2) shapeFC = (2, 8) dataFC = np.array([[1, 5, 9, 13, 2, 6, 10, 14], [3, 7, 11, 15, 4, 8, 12, 16]]) - tenmatTensorFC = ttb.tenmat.from_tensor_type( - tensorInstance, rdims=rdimsFC, cdims_cyclic="fc" - ) + tenmatTensorFC = tensorInstance.to_tenmat(rdims=rdimsFC, cdims_cyclic="fc") assert (tenmatTensorFC.rindices == rdimsFC).all() assert (tenmatTensorFC.cindices == cdimsFC).all() assert (tenmatTensorFC.data == dataFC).all() @@ -295,9 +272,7 @@ def test_tenmat_initialization_from_tensor_type( tshapeBC = (2, 2, 2, 2) shapeBC = (2, 8) dataBC = np.array([[1, 2, 9, 10, 5, 6, 13, 14], [3, 4, 11, 12, 7, 8, 15, 16]]) - tenmatTensorBC = ttb.tenmat.from_tensor_type( - tensorInstance, rdims=rdimsBC, cdims_cyclic="bc" - ) + tenmatTensorBC = tensorInstance.to_tenmat(rdims=rdimsBC, cdims_cyclic="bc") assert (tenmatTensorBC.rindices == rdimsBC).all() assert (tenmatTensorBC.cindices == cdimsBC).all() assert (tenmatTensorBC.data == dataBC).all() @@ -309,29 +284,25 @@ def test_tenmat_initialization_from_tensor_type( # cdims_cyclic has incorrect value exc = 'Unrecognized value for cdims_cyclic pattern, must be "fc" or "bc".' with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_tensor_type(tensorInstance, rdims=rdimsBC, cdims_cyclic="c") + tensorInstance.to_tenmat(rdims=rdimsBC, cdims_cyclic="c") assert exc in str(excinfo) # rdims and cdims cannot both be None exc = "Either rdims or cdims or both must be specified." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_tensor_type(tensorInstance, rdims=None, cdims=None) + tensorInstance.to_tenmat(rdims=None, cdims=None) assert exc in str(excinfo) # rdims must be valid dimensions exc = "Values in rdims must be in [0, source.ndims]." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_tensor_type( - tensorInstance, rdims=np.array([0, 1, 4]), cdims=cdims - ) + tensorInstance.to_tenmat(rdims=np.array([0, 1, 4]), cdims=cdims) assert exc in str(excinfo) # cdims must be valid dimensions exc = "Values in cdims must be in [0, source.ndims]." with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_tensor_type( - tensorInstance, rdims=rdims, cdims=np.array([2, 3, 4]) - ) + tensorInstance.to_tenmat(rdims=rdims, cdims=np.array([2, 3, 4])) assert exc in str(excinfo) # incorrect dimensions @@ -344,30 +315,26 @@ def test_tenmat_initialization_from_tensor_type( D.append([np.array([0, 1, 1]), np.array([3])]) for d in D: with pytest.raises(AssertionError) as excinfo: - ttb.tenmat.from_tensor_type(tensorInstance, d[0], d[1], tshape) + tensorInstance.to_tenmat(d[0], d[1], tshape) assert exc in str(excinfo) - # Incorrect source type - with pytest.raises(ValueError): - ttb.tenmat.from_tensor_type("not a tensor") - def test_tenmat_to_tensor(): tensorInstance = ttb.tenrand((4, 3)) tensorInstance4 = ttb.tenrand((4, 3, 2, 2)) # tenmat - tenmatInstance = ttb.tenmat.from_tensor_type(tensorInstance, np.array([0])) + tenmatInstance = tensorInstance.to_tenmat(np.array([0])) tensorTenmatInstance = tenmatInstance.to_tensor() assert tensorInstance.isequal(tensorTenmatInstance) # 1D 1-element tenmat tensorInstance1 = ttb.tensor(np.array([3])) - tenmatInstance1 = ttb.tenmat.from_tensor_type(tensorInstance1, np.array([0])) + tenmatInstance1 = tensorInstance1.to_tenmat(np.array([0])) tensorTenmatInstance1 = tenmatInstance1.to_tensor() assert tensorInstance1.isequal(tensorTenmatInstance1) # 4D tenmat - tenmatInstance4 = ttb.tenmat.from_tensor_type(tensorInstance4, np.array([3, 0])) + tenmatInstance4 = tensorInstance4.to_tenmat(np.array([3, 0])) tensorTenmatInstance4 = tenmatInstance4.to_tensor() assert tensorInstance4.isequal(tensorTenmatInstance4) @@ -414,7 +381,7 @@ def test_tenmat_norm(sample_ndarray_1way, sample_tenmat_4way): # 1D tenmat tensor1 = ttb.tensor(ndarrayInstance1, shape=(16,)) - tenmat1 = ttb.tenmat.from_tensor_type(tensor1, cdims=np.array([0])) + tenmat1 = tensor1.to_tenmat(cdims=np.array([0])) assert tenmat1.norm() == np.linalg.norm(ndarrayInstance1.ravel()) # empty tenmat @@ -424,10 +391,10 @@ def test_tenmat_norm(sample_ndarray_1way, sample_tenmat_4way): def test_tenmat__setitem__(): ndarrayInstance = np.reshape(np.arange(1, 17), (2, 2, 2, 2), order="F") tensorInstance = ttb.tensor(ndarrayInstance, shape=(2, 2, 2, 2)) - tenmatInstance = ttb.tenmat.from_tensor_type(tensorInstance, rdims=np.array([0, 1])) + tenmatInstance = tensorInstance.to_tenmat(rdims=np.array([0, 1])) # single element -> scalar - tenmatInstance2 = ttb.tenmat.from_tensor_type(tenmatInstance) + tenmatInstance2 = tenmatInstance.copy() for i in range(4): for j in range(4): tenmatInstance2[i, j] = i * 4 + j + 10 @@ -447,7 +414,7 @@ def test_tenmat__setitem__(): def test_tenmat__getitem__(): ndarrayInstance = np.reshape(np.arange(1, 17), (4, 4), order="F") tensorInstance = ttb.tensor(ndarrayInstance, shape=(4, 4)) - tenmatInstance = ttb.tenmat.from_tensor_type(tensorInstance, rdims=np.array([0])) + tenmatInstance = tensorInstance.to_tenmat(rdims=np.array([0])) # single element -> scalar for i in range(4): @@ -483,12 +450,9 @@ def test_tenmat__mul__(sample_ndarray_1way, sample_ndarray_4way, sample_tenmat_4 assert ((np.int64(3) * tenmatInstance).data == (np.int64(3) * params["data"])).all() # Tenmat * Tenmat -> 2x2 result - tenmat1 = ttb.tenmat.from_data( - ndarrayInstance4, rdims=np.array([0]), cdims=np.array([1, 2, 3]) - ) - tenmat2 = ttb.tenmat.from_data( - ndarrayInstance4, rdims=np.array([0, 1, 2]), cdims=np.array([3]) - ) + tensor0 = ttb.tensor(ndarrayInstance4) + tenmat1 = tensor0.to_tenmat(rdims=np.array([0]), cdims=np.array([1, 2, 3])) + tenmat2 = tensor0.to_tenmat(rdims=np.array([0, 1, 2]), cdims=np.array([3])) tenmatProd = tenmat1 * tenmat2 data = np.array([[372, 884], [408, 984]]) assert (tenmatProd.data == data).all() @@ -499,8 +463,8 @@ def test_tenmat__mul__(sample_ndarray_1way, sample_ndarray_4way, sample_tenmat_4 # 1D column Tenmat * 1D row Tenmat -> scalar result tensor1 = ttb.tensor(ndarrayInstance1, shape=(16,)) - tenmat1 = ttb.tenmat.from_tensor_type(tensor1, cdims=np.array([0])) - tenmat2 = ttb.tenmat.from_tensor_type(tensor1, rdims=np.array([0])) + tenmat1 = tensor1.to_tenmat(cdims=np.array([0])) + tenmat2 = tensor1.to_tenmat(rdims=np.array([0])) tenmatProd = tenmat1 * tenmat2 assert np.isscalar(tenmatProd) assert tenmatProd == 1496 @@ -509,19 +473,15 @@ def test_tenmat__mul__(sample_ndarray_1way, sample_ndarray_4way, sample_tenmat_4 # shape mismatch exc = "tenmat shape mismatch: number or columns of left operand must match number of rows of right operand." - tenmat1 = ttb.tenmat.from_data( - ndarrayInstance4, rdims=np.array([0, 1]), cdims=np.array([2, 3]) - ) - tenmat2 = ttb.tenmat.from_data( - ndarrayInstance4, rdims=np.array([0, 1, 2]), cdims=np.array([3]) - ) + tenmat1 = tensor0.to_tenmat(rdims=np.array([0, 1]), cdims=np.array([2, 3])) + tenmat2 = tensor0.to_tenmat(rdims=np.array([0, 1, 2]), cdims=np.array([3])) with pytest.raises(AssertionError) as excinfo: tenmat1 * tenmat2 assert exc in str(excinfo) # type mismatch exc = "tenmat multiplication only valid with scalar or tenmat objects." - tenmatNdarray4 = ttb.tenmat.from_data(ndarrayInstance4, rdims, cdims, tshape) + tenmatNdarray4 = tensor0.to_tenmat(rdims, cdims, tshape) with pytest.raises(AssertionError) as excinfo: tenmatInstance * tenmatNdarray4.data assert exc in str(excinfo) @@ -552,7 +512,7 @@ def test_tenmat__add__(sample_ndarray_2way, sample_tenmat_4way): # shape mismatch exc = "tenmat shape mismatch." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, (1, 1, 1, 16)) + tenmatNdarray2 = ttb.tenmat(np.ones((5, 5)), rdims, cdims, (1, 1, 1, 25)) with pytest.raises(AssertionError) as excinfo: tenmatInstance + tenmatNdarray2 assert exc in str(excinfo) @@ -562,7 +522,7 @@ def test_tenmat__add__(sample_ndarray_2way, sample_tenmat_4way): # type mismatch exc = "tenmat addition only valid with scalar or tenmat objects." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, tshape) + tenmatNdarray2 = ttb.tenmat(ndarrayInstance2, rdims, cdims, tshape) with pytest.raises(AssertionError) as excinfo: tenmatInstance + tenmatNdarray2.data assert exc in str(excinfo) @@ -593,14 +553,14 @@ def test_tenmat__sub__(sample_ndarray_2way, sample_tenmat_4way): # shape mismatch exc = "tenmat shape mismatch." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, (1, 1, 1, 16)) + tenmatNdarray2 = ttb.tenmat(np.ones((5, 5)), rdims, cdims, (1, 1, 1, 25)) with pytest.raises(AssertionError) as excinfo: tenmatInstance - tenmatNdarray2 assert exc in str(excinfo) # type mismatch exc = "tenmat subtraction only valid with scalar or tenmat objects." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, tshape) + tenmatNdarray2 = ttb.tenmat(ndarrayInstance2, rdims, cdims, tshape) with pytest.raises(AssertionError) as excinfo: tenmatInstance - tenmatNdarray2.data assert exc in str(excinfo) @@ -629,14 +589,14 @@ def test_tenmat__rsub__(sample_ndarray_2way, sample_tenmat_4way): # shape mismatch exc = "tenmat shape mismatch." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, (1, 1, 1, 16)) + tenmatNdarray2 = ttb.tenmat(np.ones((5, 5)), rdims, cdims, (1, 1, 1, 25)) with pytest.raises(AssertionError) as excinfo: tenmatInstance.__rsub__(tenmatNdarray2) assert exc in str(excinfo) # type mismatch exc = "tenmat subtraction only valid with scalar or tenmat objects." - tenmatNdarray2 = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, tshape) + tenmatNdarray2 = ttb.tenmat(ndarrayInstance2, rdims, cdims, tshape) with pytest.raises(AssertionError) as excinfo: tenmatInstance.__rsub__(tenmatNdarray2.data) assert exc in str(excinfo) @@ -680,7 +640,8 @@ def test_tenmat__str__( assert s == tenmatInstance.__str__() # Test 1D - tenmatInstance = ttb.tenmat.from_data(ndarrayInstance1, rdims, cdims, tshape) + tensor1 = ttb.tensor(ndarrayInstance1, shape=tshape) + tenmatInstance = tensor1.to_tenmat(rdims, cdims, tshape) s = "" s += "matrix corresponding to a tensor of shape " s += str(tenmatInstance.tshape) @@ -697,7 +658,7 @@ def test_tenmat__str__( assert s == tenmatInstance.__str__() ## Test 2D - tenmatInstance = ttb.tenmat.from_data(ndarrayInstance2, rdims, cdims, tshape) + tenmatInstance = ttb.tenmat(ndarrayInstance2, rdims, cdims, tshape) s = "" s += "matrix corresponding to a tensor of shape " s += str(tenmatInstance.tshape) @@ -714,7 +675,8 @@ def test_tenmat__str__( assert s == tenmatInstance.__str__() # Test 4D - tenmatInstance = ttb.tenmat.from_data(ndarrayInstance4, rdims, cdims, tshape) + tensor0 = ttb.tensor(ndarrayInstance4, shape=tshape) + tenmatInstance = tensor0.to_tenmat(rdims, cdims, tshape) s = "" s += "matrix corresponding to a tensor of shape " s += str(tenmatInstance.tshape) @@ -729,3 +691,9 @@ def test_tenmat__str__( s += str(tenmatInstance.data) s += "\n" assert s == tenmatInstance.__str__() + + +def test_tenmat_isequal(): + # Negative test + with pytest.raises(ValueError): + ttb.tenmat().isequal("Not a tenmat") diff --git a/tests/test_tensor.py b/tests/test_tensor.py index 47d2fb7f..27aa06f4 100644 --- a/tests/test_tensor.py +++ b/tests/test_tensor.py @@ -1188,7 +1188,7 @@ def test_tensor_ttm(sample_tensor_2way, sample_tensor_3way, sample_tensor_4way): assert np.array_equal(T3.data, data3) # 3-way, matrix must be np.ndarray - Tmat = ttb.tenmat.from_data(M2, rdims=np.array([0])) + Tmat = ttb.tenmat(M2, rdims=np.array([0])) with pytest.raises(AssertionError) as excinfo: tensorInstance3.ttm(Tmat, 0) assert "matrix must be of type numpy.ndarray" in str(excinfo)