Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nick/tenmat docs #294

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
SPTENMAT: Move from_tensor_type to to_sptenmat
* Add isequal
  • Loading branch information
ntjohnson1 committed Nov 24, 2023
commit f413df9a75c3869e04c46d88550aa0bd15ee0b77
107 changes: 20 additions & 87 deletions pyttb/sptenmat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 tt_ind2sub


class sptenmat(object):
class sptenmat:
"""
SPTENMAT Store sparse tensor as a sparse matrix.

Expand Down Expand Up @@ -131,89 +131,6 @@ def __init__( # noqa: PLR0913
self.subs = newsubs
self.vals = newvals

@classmethod
def from_tensor_type( # noqa: PLR0912
cls,
source: Union[ttb.sptensor],
rdims: Optional[np.ndarray] = None,
cdims: Optional[np.ndarray] = None,
cdims_cyclic: Optional[
Union[Literal["fc"], Literal["bc"], Literal["t"]]
] = None,
):
assert isinstance(source, ttb.sptensor), (
"Can only generate sptenmat from " f"sptensor but received {type(source)}."
)

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(
np.hstack([ridx, cidx], dtype=int),
source.vals.copy(),
rdims.astype(int),
cdims.astype(int),
source.shape,
)

@classmethod
def from_array(
cls,
Expand Down Expand Up @@ -261,7 +178,7 @@ def copy(self) -> sptenmat:

>>> S1 = ttb.sptensor(shape=(2,2))
>>> S1[0,0] = 1
>>> ST1 = ttb.sptenmat.from_tensor_type(S1, np.array([0]))
>>> ST1 = S1.to_sptenmat(np.array([0]))
>>> ST2 = ST1
>>> ST3 = ST1.copy()
>>> ST1[0,0] = 3
Expand Down Expand Up @@ -347,6 +264,22 @@ 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 (+).
Expand Down
144 changes: 141 additions & 3 deletions pyttb/sptensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,146 @@ def full(self) -> ttb.tensor:
B[idx.astype(int)] = self.vals.transpose()[0]
return B

def to_sptenmat( # noqa: PLR0912
self,
rdims: Optional[np.ndarray] = None,
cdims: Optional[np.ndarray] = None,
cdims_cyclic: Optional[
Union[Literal["fc"], Literal["bc"], Literal["t"]]
] = None,
):
"""
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)
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).

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)])

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, n)]
+ [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(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(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:
Expand Down Expand Up @@ -3455,9 +3595,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())
Expand Down
8 changes: 2 additions & 6 deletions pyttb/ttensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,18 +625,14 @@ 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()

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

Expand Down
Loading