Source code for hottbox.core.structures

"""
Classes for different tensor representations
"""
import itertools
import numpy as np
import warnings
from functools import reduce
from .operations import unfold, kolda_unfold, fold, kolda_fold, mode_n_product
from ._meta import Mode, State
from ..errors import TensorModeError, TensorShapeError, TensorStateError, TensorTopologyError, ModeError, StateError


[docs]class Tensor(object): """ This class describes multidimensional data. All its methods implement all common operation on a tensor alone Parameters ---------- array : np.ndarray N-dimensional array custom_state : dict Provides flexibility to create a ``Tensor`` object in folded, unfolded or rotated state. Use with caution. If provided, then should include only the following keys:\n 'mode_order' -> tuple of lists \n 'normal_shape' -> tuple \n 'rtype' -> str mode_names : list[str] Description of the tensor modes. If nothing is specified then all modes of the created ``Tensor`` get generic names 'mode-0', 'mode-1' etc. Attributes ---------- _data : np.ndarray N-dimensional array. _modes : list[Mode] Description of the tensor modes in form of a list where each element is object of ``Mode`` class. Meta information. _state : State Reference to meta information about transformations applied to a tensor, such as unfolding, vectorising etc. Meta information. Raises ------ TypeError If parameter ``array`` is not of ``np.ndarray`` type. StateError If parameter ``custom_state`` is inconsistent with provided ``array`` or does not meet requirements for creating ``State`` of the tensor. ModeError If parameter ``mode_names`` is inconsistent with provided ``array`` and ``custom_state`` or does not meet requirements for creating list of ``Mode`` of the tensor. Examples -------- 1) Creating a tensor with default parameters for meta data >>> import numpy as np >>> from hottbox.core import Tensor >>> data = np.arange(24).reshape(2, 3, 4) >>> tensor = Tensor(data) >>> print(tensor) This tensor is of order 3 and consists of 24 elements. Sizes and names of its modes are (2, 3, 4) and ['mode-0', 'mode-1', 'mode-2'] respectively. 2) Creating a tensor with custom mode names >>> import numpy as np >>> from hottbox.core import Tensor >>> data = np.arange(24).reshape(2, 3, 4) >>> tensor = Tensor(data, mode_names=["Year", "Month", "Day"]) >>> tensor.show_state() State(normal_shape=(2, 3, 4), rtype='Init', mode_order=([0], [1], [2])) >>> print(tensor) This tensor is of order 3 and consists of 24 elements. Sizes and names of its modes are (2, 3, 4) and ['Year', 'Month', 'Day'] respectively. 3) Creating a tensor in custom state >>> import numpy as np >>> from hottbox.core import Tensor >>> data = np.arange(24).reshape(2, 3*4) >>> tensor = Tensor(data, custom_state={"normal_shape": (2, 3, 4), ... "mode_order": ([0], [1, 2]), ... "rtype": "T"} >>> data.shape (2, 12) >>> tensor.shape (2, 12) >>> tensor.show_state() State(normal_shape=(2, 3, 4), rtype='T', mode_order=([0], [1, 2])) >>> print(tensor) This tensor is of order 2 and consists of 24 elements. Sizes and names of its modes are (2, 12) and ['mode-0', 'mode-1_mode-2'] respectively. """ def __init__(self, array, custom_state=None, mode_names=None) -> None: # TODO: provide more details for raises section? self._validate_init_data(array=array, mode_names=mode_names, custom_state=custom_state) self._data = array.copy() self._state, self._modes = self._create_meta(array=array, custom_state=custom_state, mode_names=mode_names) def __repr__(self): return str(self) def __getitem__(self,key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __eq__(self, other): """ Parameters ---------- other : Tensor Returns ------- equal : bool Notes ----- Tensors are equal when everything is the same. """ equal = False if isinstance(self, other.__class__): if self.shape == other.shape and self._state == other._state: data_equal = np.allclose(self.data, other.data, rtol=1e-05, atol=1e-08, equal_nan=True) modes_equal = all([mode == other.modes[i] for i, mode in enumerate(self.modes)]) equal = data_equal and modes_equal return equal def __add__(self, other): """ Summation of objects of ``Tensor`` class Parameters ---------- other : Tensor Returns ------- tensor : Tensor Notes ----- Two objects of ``Tensor`` class can be added together if: 1) Both are in normal state (haven't been rotated, unfolded or folded) 2) Both have the same shape 3) Bath have the same indices : all([self.modes[i].index == other.modes[i].index]) If names of the modes are different the summation will be performed, and """ if not isinstance(self, other.__class__): raise TypeError("Don't know how to sum object of {} class " "with an object of {} class!".format(self.__class__.__name__, other.__class__.__name__)) if not all([self.in_normal_state, other.in_normal_state]): raise TensorStateError("Both tensors should be in normal state!") if self.shape != other.shape: raise TensorShapeError("Both tensors should have the same shape!") if not all([self.modes[i].index == other.modes[i].index for i in range(self.order)]): raise TensorModeError("Both tensors should have the same indices!") array = self.data + other.data tensor = Tensor(array=array).copy_modes(self) if self.mode_names != other.mode_names: for i in range(tensor.order): tensor.reset_mode_name(mode=i) return tensor def __str__(self): """ Provides general information about this instance.""" return "This tensor is of order {} and consists of {} elements.\n" \ "Sizes and names of its modes are {} and {} respectively.".format(self.order, self.size, self.shape, self.mode_names) @staticmethod def _validate_init_data(array, mode_names, custom_state): """ Validate data for ``Tensor`` constructor Parameters ---------- array : np.ndarray mode_names : list[str] custom_state : dict """ # validate data array if not isinstance(array, np.ndarray): raise TypeError('Input data should be a numpy array') # validate custom_state if provided if custom_state is not None: if not isinstance(custom_state, dict): raise StateError("Incorrect type of the parameter `custom_state`!\n" "It should be `dict`") keys_required = ['mode_order', 'normal_shape', 'rtype'] keys_required.sort() keys_presented = list(custom_state.keys()) keys_presented.sort() if keys_presented != keys_required: raise StateError("Some keys missing or extra keys have been provided!!!\n" "`custom_state` should have only {} keys".format(keys_required) ) # ------------------------------ if not isinstance(custom_state['normal_shape'], tuple): raise StateError("Incorrect type of the parameter `custom_state['normal_shape']`!\n" "It should be `tuple`") normal_shape = custom_state['normal_shape'] size = reduce(lambda x, y: x * y, normal_shape) if size != array.size: raise StateError("Values of `normal_shape` are inconsistent " "with the provided data array ({} != {})!".format(size, array.size)) # ------------------------------ mode_order_ = custom_state['mode_order'] if not isinstance(mode_order_, tuple): raise StateError("Incorrect type of the parameter `custom_state['mode_order']`!\n" "It should be `tuple` of lists") if not all(isinstance(mode_seq, list) for mode_seq in mode_order_): raise StateError("Incorrect type of the parameter `mode_order[i]`!\n" "It should be `list` of `int`") if len(mode_order_) != array.ndim: raise StateError("Provided `custom_state` does not correspond to the shape of provided data array!\n" "{}!={} (len(custom_state['mode_order']) != array.ndim)".format(len(mode_order_), array.ndim)) modes_specified = list(itertools.chain.from_iterable(mode_order_)) if len(modes_specified) != len(normal_shape): raise StateError("Number of provided modes is inconsistent with the provided length `normal_shape`!\n" "{} != {}".format(len(modes_specified), len(normal_shape)) ) # validate mode_names if provided if mode_names is not None: if not isinstance(mode_names, list): raise ModeError("You should use list for `mode_names`!") if not all(isinstance(name, str) for name in mode_names): raise ModeError("The list of mode names should contain only strings!") if custom_state is None: if array.ndim != len(mode_names): raise ModeError("Incorrect number of names for the modes of a tensor: {0} != {1} " "('array.ndim != len(mode_names)')!\n".format(array.ndim, len(mode_names) ) ) else: normal_shape = custom_state['normal_shape'] if len(normal_shape) != len(mode_names): raise ModeError("Incorrect number of names for the modes of a tensor: {0} != {1} " "('len(normal_shape) != len(mode_names)')!\n".format(len(normal_shape), len(mode_names) ) ) @staticmethod def _create_meta(array, custom_state, mode_names): """ Create meta data for the tensor Parameters ---------- array : np.ndarray custom_state : dict mode_names : list[str] Returns ------- state : State Meta information related to reshaping of the tensor modes : list[Mode] Meta information related to modes of the tensor """ if custom_state is None: state = State(normal_shape=tuple(mode_size for mode_size in array.shape)) else: state = State(**custom_state) if mode_names is None: mode_names = ["mode-{}".format(i) for i in range(len(state.normal_shape))] modes = [Mode(name=name) for name in mode_names] return state, modes @property def data(self): """ N-dimensional array with data values Returns ------- array : np.ndarray """ array = self._data return array @property def ft_shape(self): """ Shape of the a tensor in normal format (without being in unfolded or folded state) Returns ------- shape : tuple """ shape = self._state.normal_shape return shape @property def modes(self): """ Meta data for the modes of a tensor Returns ------- list[Mode] """ return self._modes @property def in_normal_state(self): """ Checks state of a tensor Returns ------- bool Notes ----- If returns `True`, then can call ``unfold`` and ``mode_n_product``.\n If returns `False`, then can call ``fold`` """ return self._state.is_normal()
[docs] def show_state(self): """ Show the current state of the ``Tensor`` """ return print(self._state)
@property def mode_names(self): """ Description of the tensor modes in current state Returns ------- names : list[str] """ if self.in_normal_state: all_names = [mode.name for mode in self.modes] else: # if tensor is in unfolded state all_names = [] for mode_order in self._state.mode_order: names = [self.modes[i].name for i in mode_order] all_names.append("_".join(names)) return all_names @property def frob_norm(self): """ Frobenious norm of a tensor Returns ------- float """ return np.linalg.norm(self.data) @property def shape(self): """ Shape of a tensor in current state Returns ------- tuple Sizes of all dimensions of a tensor """ return self.data.shape @property def order(self): """ Order of a tensor Returns ------- int """ return self.data.ndim @property def size(self): """ Number of elements in a tensor Returns ------- int """ return self.data.size
[docs] def copy(self): """ Produces a copy of itself as a new object Returns ------- new_object : Tensor New object of Tensor class with attributes having the same values, but no memory space is shared Notes ----- Only the last transformation will be copied if `tensor` is not in normal state """ array = self.data if self.in_normal_state: new_object = Tensor(array=array) else: custom_state = dict(normal_shape=self._state.normal_shape, rtype=self._state.rtype, mode_order=self._state.mode_order ) new_object = Tensor(array=array, custom_state=custom_state) # In order to preserved index if it was specified new_object.copy_modes(self) return new_object
[docs] def copy_modes(self, tensor): """ Copy modes meta from tensor representation Parameters ---------- tensor : {Tensor, TensorCPD, TensorTKD, TensorTT} Returns ------- self : Tensor """ self._modes = [mode.copy() for mode in tensor.modes] return self
[docs] def reset_meta(self): """ Set all meta information with respect to the current shape of data array Returns ------- self : Tensor """ default_mode_names = ["mode-{}".format(i) for i in range(self.data.ndim)] self._modes = [Mode(name=name) for name in default_mode_names] self._state.reset() return self
[docs] def set_mode_names(self, mode_names): """ Rename modes of a tensor Parameters ---------- mode_names : dict New names for the tensor modes in form of a dictionary The name of the mode defined by the Key of the dict will be renamed to the corresponding Value Returns ------- self : Tensor Return self so that methods could be chained Raises ------ ModeError If parameter ``mode_names`` is inconsistent with ``self.modes``. """ if len(mode_names.keys()) > self.order: raise ModeError("Too many mode names have been specified") if not all(isinstance(mode, int) for mode in mode_names.keys()): raise ModeError("The dict of `mode_names` should contain only integer keys!") if not all(mode < self.order for mode in mode_names.keys()): raise ModeError("All specified mode values should not exceed the order of the tensor!") if not all(mode >= 0 for mode in mode_names.keys()): raise ModeError("All specified mode keys should be non-negative!") for i, name in mode_names.items(): self.modes[i].set_name(name=name) return self
[docs] def reset_mode_name(self, mode=None): """ Set default name for the specified mode number Parameters ---------- mode : int Mode number which name to be set to default value By default resets names of all modes Returns ------- self : Tensor """ if mode is None: for i, t_mode in enumerate(self.modes): default_name = "mode-{}".format(i) t_mode.set_name(name=default_name) else: default_name = "mode-{}".format(mode) self.modes[mode].set_name(name=default_name) return self
[docs] def set_mode_index(self, mode_index): """ Set index for specified mode Parameters ---------- mode_index : dict New indices for the tensor modes in form of a dictionary. Key defines the mode whose index to be changed. Value contains a list of new indices for this mode. Returns ------- self : Tensor Raises ------ ModeError If parameter ``mode_index`` is inconsistent with ``self.modes``. """ if len(mode_index.keys()) > self.order: raise ModeError("Too many sets of indices have been specified") if not all(isinstance(mode, int) for mode in mode_index.keys()): raise ModeError("The dict of `mode_index` should contain only integer keys!") if not all(mode < self.order for mode in mode_index.keys()): raise ModeError("All specified mode values should not exceed the order of the tensor!") if not all(mode >= 0 for mode in mode_index.keys()): raise ModeError("All specified mode keys should be non-negative!") if not all([len(index) == self.ft_shape[mode] for mode, index in mode_index.items()]): raise ModeError("Not enough of too many indices for the specified mode") for i, index in mode_index.items(): self.modes[i].set_index(index=index) return self
[docs] def reset_mode_index(self, mode=None): """ Drop index for the specified mode number Parameters ---------- mode : int Mode number which index to be dropped By default resets all indices Returns ------- self : Tensor """ if mode is None: for i in range(self.order): self.modes[i].reset_index() else: self.modes[mode].reset_index() return self
[docs] def describe(self): """ Expose some metrics """ print("In the future this call will print out some statistics about the tensor")
[docs] def unfold(self, mode, rtype="T", inplace=True): """ Perform mode-n unfolding to a matrix Parameters ---------- mode : int Specifies a mode along which a `tensor` will be unfolded rtype : str Defines an unfolding convention. inplace : bool If True, then modifies itself. If False, then creates new object (copy) Returns ---------- tensor : Tensor Unfolded version of a tensor Raises ------ TensorStateError If tensor is not in normal state: ``self.in_normal_state is False``. ValueError If parameter ``rtype`` is not one of {'T', 'K'}. """ if not self.in_normal_state: raise TensorStateError("The tensor is not in the original form") # Unfold data if rtype is "T": unfold_function = unfold elif rtype is "K": unfold_function = kolda_unfold else: raise ValueError("Unknown type of unfolding! Parameter `rtype` should be one of {\"T\", \"K\"}.") data_unfolded = unfold_function(self.data, mode) if inplace: tensor = self else: tensor = self.copy() tensor._data = data_unfolded tensor._state.unfold(mode=mode, rtype=rtype) return tensor
[docs] def vectorise(self, rtype="T", inplace=True): """ Perform vectorisation of a tensor Parameters ---------- rtype : str Defines a vectorisation convention. inplace : bool If True, then modifies itself. If False, then creates new object (copy) Returns ---------- tensor : Tensor Vectorised version of a tensor Raises ------ TensorStateError If tensor is not in normal state: ``self.in_normal_state is False``. ValueError If parameter ``rtype`` is not one of {'T', 'K'}. """ if not self.in_normal_state: raise TensorStateError("The tensor is not in the original form") # Unfold data if rtype is "T": order = "C" elif rtype is "K": order = "F" else: raise ValueError("Unknown type of vectorisation! Parameter `rtype` should be one of {\"T\", \"K\"}.") data_vectorised = np.ravel(self.data, order=order) if inplace: tensor = self else: tensor = self.copy() tensor._data = data_vectorised tensor._state.vectorise(rtype=rtype) return tensor
[docs] def fold(self, inplace=True): """ Fold to the original shape Parameters ---------- inplace : bool If True, then modifies itself. If False, then creates new object (copy) Returns ---------- tensor : Tensor Tensor of original shape (``self.ft_shape``) Raises ------ TensorStateError If tensor is in normal state: ``self.in_normal_state is True``. Notes ----- Basically, this method can be called in order to undo both ``self.unfold`` and ``self.vectorise`` """ # Do not do anything if the tensor is in the normal form (hadn't been unfolded before) if self.in_normal_state: raise TensorStateError("The tensor hadn't bee unfolded before") # Fold data temp = self._state.mode_order[0] folding_mode = temp[0] if len(self._state.mode_order) == 1: # Undo vectorisation if self._state.rtype is "T": order = "C" else: order = "F" data_folded = np.reshape(self.data, self.ft_shape, order=order) else: # Undo matricization (unfolding) if self._state.rtype is "T": fold_function = fold else: fold_function = kolda_fold data_folded = fold_function(matrix=self.data, mode=folding_mode, shape=self.ft_shape) if inplace: tensor = self else: tensor = self.copy() tensor._data = data_folded tensor._state.fold() return tensor
[docs] def mode_n_product(self, matrix, mode, new_name=None, inplace=True): """ Mode-n product of a tensor with a matrix Parameters ---------- matrix : {Tensor, np.ndarray} 2D array mode : int Specifies mode along which a tensor is multiplied by a `matrix` inplace : bool If True, then modifies itself. If False, then creates new object (copy) new_name : str New name for the corresponding `mode` after computing this product. See Notes-3 for more info Returns ------- tensor : Tensor The result of the mode-n product of a tensor with a `matrix` along specified `mode`. Raises ------ TensorStateError If tensor is not in normal state: ``self.in_normal_state is False``. ModeError If there is uncertainty with assigning new name for the ``tensor.modes[mode]``. Notes ------- 1. Mode-n product operation changes the ``self._state._normal_shape`` attribute 2. Remember that mode-n product changes the shape of the tensor. Presumably, it also changes the interpretation of that mode depending on the matrix 3. If ``matrix`` is an object of ``Tensor`` class then you shouldn't specify ``new_name``, since it will be changed to ``matrix.mode_names[0]`` 4. If ``matrix.mode_names[0] == "mode-0"`` then no changes to ``tensor.mode_names`` will be made """ if not self.in_normal_state: raise TensorStateError("The tensor is not in the original form") # TODO: need to rethink this if statements so it would be easier to follow if isinstance(matrix, Tensor) and new_name is not None: raise ModeError("Oops... Don't know which name for the mode description to use!\n" "Either use the default value for `new_name=None` or pass numpy array for `matrix.`") if new_name is not None and not isinstance(new_name, str): raise ModeError("The parameter `new_name` should be of sting type!") # Convert to Tensor class, in order to have consistent interface if isinstance(matrix, np.ndarray): matrix = Tensor(matrix) if new_name is None: new_name = matrix.mode_names[0] new_data = mode_n_product(tensor=self.data, matrix=matrix.data, mode=mode) new_normal_shape = new_data.shape if inplace: tensor = self else: tensor = self.copy() tensor._data = new_data tensor._state.change_normal_shape(normal_shape=new_normal_shape) tensor.reset_mode_index(mode=mode) # The only one case when mode name won't be changed if new_name != "mode-0": new_mode_names = {mode: new_name} tensor.set_mode_names(mode_names=new_mode_names) return tensor
[docs] def access(self, inds, mode): """ Equivalent to multidimnesional slicing Parameters ---------- inds : int The index of the axis. e.g [:,:,0] will be at mode=2, inds=0 mode : int The axis to access overwrite : Tensor Overwrite slice with a subtensor Returns ------- Numpy array with the formulated subtensor """ tensor = self._data tensl = np.array([slice(None)] * tensor.ndim) tensl[mode] = inds tensl = tuple(tensl.tolist()) return tensor[tensl]
# TODO: add some checks to overwite
[docs] def write_subtensor(self, inds, mode, overwrite): """ Works in the same way as `access` but permits changing of the tensor data Parameters ---------- inds : int The index of the axis. e.g [:,:,0] will be at mode=2, inds=0 mode : int The axis to access overwrite : Tensor Overwrite slice with a subtensor """ tensor = self._data tensl = np.array([slice(None)] * tensor.ndim) tensl[mode] = inds tensl = tuple(tensl.tolist()) tensor[tensl] = overwrite return
# TODO: add validation of `mode_names` class BaseTensorTD(object): """ This class provides a general interface for a tensor represented through a tensor decomposition. """ def __init__(self): pass @staticmethod def _validate_init_data(**kwargs): """ Validate data for the constructor of a new object """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') def _create_modes(self, mode_names): """ Create meta data for each mode of tensor representation Parameters ---------- mode_names : list[str] Returns ------- modes : list[Mode] """ if mode_names is None: mode_names = ["mode-{}".format(i) for i in range(self.order)] modes = [Mode(name=name) for name in mode_names] return modes def copy(self): """ Produces a copy of itself as a new object """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def modes(self): """ Meta data for each mode of tensor representation """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def order(self): """ Order of a tensor in full form """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def rank(self): """ Rank of an efficient representation """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def size(self): """ Number of elements for efficient representation """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def frob_norm(self): raise NotImplementedError('Not implemented in base (BaseTensorTD) class') @property def mode_names(self): """ Description of the modes for a current representation of a tensor Returns ------- list[str] """ return [mode.name for mode in self.modes] def reconstruct(self): """ Convert to the full tensor as an object of Tensor class """ raise NotImplementedError('Not implemented in base (BaseTensorTD) class') def unfold(self): raise NotImplementedError('Not implemented in base (BaseTensorTD) class') def fold(self): raise NotImplementedError('Not implemented in base (BaseTensorTD) class') def mode_n_product(self): raise NotImplementedError('Not implemented in base (BaseTensorTD) class') def copy_modes(self, tensor): """ Copy modes meta from tensor Parameters ---------- tensor : {Tensor, TensorCPD, TensorTKD, TensorTT} Returns ------- self """ self._modes = [mode.copy() for mode in tensor.modes] return self def set_mode_names(self, mode_names): """ Rename modes of a tensor representation Parameters ---------- mode_names : dict New names for the tensor modes in form of a dictionary The name of the mode defined by the Key of the dict will be renamed to the corresponding Value Returns ------- self Raises ------ ModeError If parameter ``mode_names`` is inconsistent with ``self.modes``. """ if len(mode_names.keys()) > self.order: raise ModeError("Too many mode names have been specified") if not all(isinstance(mode, int) for mode in mode_names.keys()): raise ModeError("The dict of `mode_names` should contain only integer keys!") if not all(mode < self.order for mode in mode_names.keys()): raise ModeError("All specified mode values should not exceed the order of the tensor!") if not all(mode >= 0 for mode in mode_names.keys()): raise ModeError("All specified mode keys should be non-negative!") for i, name in mode_names.items(): self.modes[i].set_name(name) return self def reset_mode_name(self, mode): """ Set default name for the specified mode number Parameters ---------- mode : int Mode number which name to be set to default value By default resets names of all modes Returns ------- self """ if mode is None: for i, t_mode in enumerate(self.modes): default_name = "mode-{}".format(i) t_mode.set_name(name=default_name) else: default_name = "mode-{}".format(mode) self.modes[mode].set_name(name=default_name) return self def set_mode_index(self, mode_index): """ Set index for specified mode Parameters ---------- mode_index : dict New indices for the factor matrices in form of a dictionary. Key defines the mode whose index to be changed. Value contains a list of new indices for this mode. Returns ------- self Raises ------ ModeError If parameter ``mode_index`` is inconsistent with ``self.modes``. """ if len(mode_index.keys()) > self.order: raise ModeError("Too many sets of indices have been specified") if not all(isinstance(mode, int) for mode in mode_index.keys()): raise ModeError("The dict of `mode_index` should contain only integer keys!") if not all(mode < self.order for mode in mode_index.keys()): raise ModeError("All specified mode values should not exceed the order of the tensor!") if not all(mode >= 0 for mode in mode_index.keys()): raise ModeError("All specified mode keys should be non-negative!") if isinstance(self, TensorTT): index_long_enough = all([len(index) == self.ft_shape[mode] for mode, index in mode_index.items()]) else: index_long_enough = all([len(index) == self.fmat[mode].shape[0] for mode, index in mode_index.items()]) if not index_long_enough: raise ModeError("Not enough of too many indices for the specified mode") for i, index in mode_index.items(): self.modes[i].set_index(index=index) return self def reset_mode_index(self, mode): """ Drop index for the specified mode number Parameters ---------- mode : int Mode number which index to be dropped By default resets all indices Returns ------- self """ if mode is None: for i in range(self.order): self.modes[i].reset_index() else: self.modes[mode].reset_index() return self
[docs]class TensorCPD(BaseTensorTD): """ Representation of a tensor in the Kruskal form (CPD). Parameters ---------- fmat : list[np.ndarray] List of factor matrices for the CP representation of a tensor core_values : np.ndarray Array of coefficients on the super-diagonal of a core for the CP representation of a tensor mode_names : list[str] List of names for the factor matrices Attributes ---------- _fmat : list[np.ndarray] Placeholder for a list of factor matrices for the CP representation of a tensor _core_values : np.ndarray Placeholder for an array of coefficients on the super-diagonal of a core for the CP representation of a tensor _modes : list[Mode] Description of the factor matrix for the corresponding mode Raises ------ TensorTopologyError If there is inconsistency in shapes of factor matrices and core values Examples -------- 1) Create kruskal representation of a tensor with default meta information >>> import numpy as np >>> from hottbox.core import TensorCPD >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R = 4 # Kruskal rank >>> A = np.ones((I, R)) >>> B = np.ones((J, R)) >>> C = np.ones((K, R)) >>> fmat = [A, B , C] >>> core_values = np.arange(R) >>> tensor_cpd = TensorCPD(fmat, core_values) >>> print(tensor_cpd) Kruskal representation of a tensor with rank=(4,). Factor matrices represent properties: ['mode-0', 'mode-1', 'mode-2'] With corresponding latent components described by (5, 6, 7) features respectively. 2) Create kruskal representation of a tensor with custom meta information >>> import numpy as np >>> from hottbox.core import TensorCPD >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R = 4 # Kruskal rank >>> A = np.ones((I, R)) >>> B = np.ones((J, R)) >>> C = np.ones((K, R)) >>> fmat = [A, B , C] >>> core_values = np.arange(R) >>> mode_names = ["Year", "Month", "Day"] >>> tensor_cpd = TensorCPD(fmat, core_values, mode_names) >>> print(tensor_cpd) Kruskal representation of a tensor with rank=(4,). Factor matrices represent properties: ['Year', 'Month', 'Day'] With corresponding latent components described by (5, 6, 7) features respectively. """ def __init__(self, fmat, core_values, mode_names=None): super(TensorCPD, self).__init__() self._validate_init_data(fmat=fmat, core_values=core_values) self._fmat = [mat.copy() for mat in fmat] self._core_values = core_values.copy() self._modes = self._create_modes(mode_names=mode_names) def __eq__(self, other): """ Parameters ---------- other : TensorCPD Returns ------- equal : bool Notes ----- If dimensionality check fails then ``TensorCPD`` objects cannot be equal by definition. """ equal = False if isinstance(self, other.__class__): if self.ft_shape == other.ft_shape and self.rank == other.rank: fmat_equal = all([np.allclose(fmat, other.fmat[i], rtol=1e-05, atol=1e-08, equal_nan=True) for i, fmat in enumerate(self.fmat)]) core_equal = self.core == other.core modes_equal = all([mode == other.modes[i] for i, mode in enumerate(self.modes)]) equal = fmat_equal and core_equal and modes_equal return equal def __add__(self, other): """ Summation of objects of ``TensorCPD`` class Parameters ---------- other : TensorCPD Returns ------- tensor_cpd : TensorCPD """ if not isinstance(self, other.__class__): raise TypeError("Don't know how to sum object of {} class " "with an object of {} class!".format(self.__class__.__name__, other.__class__.__name__)) if self.ft_shape != other.ft_shape: raise TensorTopologyError("Both objects should have the same topology!\n" "{}!={} (`self.ft_shape != other.ft_shape`)".format(self.ft_shape, other.ft_shape)) if not all([self.modes[i].index == other.modes[i].index for i in range(self.order)]): raise ModeError("Both tensors should have the same indices!") core_values = np.concatenate((self._core_values, other._core_values)) fmat_list = [np.concatenate((fmat, other.fmat[i]), axis=1) for i, fmat in enumerate(self.fmat)] tensor_cpd = TensorCPD(fmat=fmat_list, core_values=core_values).copy_modes(self) if self.mode_names != other.mode_names: for i in range(tensor_cpd.order): tensor_cpd.reset_mode_name(mode=i) return tensor_cpd def __str__(self): """ Provides general information about this instance.""" return "Kruskal representation of a tensor with rank={}.\n" \ "Factor matrices represent properties: {}\n" \ "With corresponding latent components described by {} features respectively.".format(self.rank, self.mode_names, self.ft_shape) def __repr__(self): return str(self) @staticmethod def _validate_init_data(fmat, core_values): """ Validate data for the TensorCPD constructor Parameters ---------- fmat : list[np.ndarray] List of factor matrices for the CP representation of a tensor core_values : np.ndarray Array of coefficients on the super-diagonal of a core for the CP representation of a tensor """ if not isinstance(core_values, np.ndarray): raise TypeError("Core values (`core_values`) should be a numpy array") if not isinstance(fmat, list): raise TypeError("All factor matrices (`fmat`) should be passed as a list!") for mat in fmat: if not isinstance(mat, np.ndarray): raise TypeError("Each of the factor matrices should be a numpy array!") if mat.ndim != 2: raise TensorTopologyError("Each of the factor matrices should be a 2-dimensional numpy array!") kryskal_rank = len(core_values) if not all([mat.shape[1] == kryskal_rank for mat in fmat]): raise TensorTopologyError("Dimension mismatch! Number of columns of all " "factor matrices should be the same and equal to len(core_values)!") def _create_modes(self, mode_names): """ Create meta data for each factor matrix Parameters ---------- mode_names : list[str] Returns ------- modes : list[Mode] """ modes = super(TensorCPD, self)._create_modes(mode_names=mode_names) return modes @property def core(self): """ Core tensor of the CP representation of a tensor Returns ------- core_tensor : Tensor """ from hottbox.utils.generation import super_diag_tensor core_shape = self.rank * self.order core_tensor = super_diag_tensor(core_shape, values=self._core_values) return core_tensor @property def fmat(self): """ List of factor matrices for the CP representation of a tensor Returns ------- factor_matrices : list[np.ndarray] """ factor_matrices = self._fmat return factor_matrices @property def modes(self): """ Meta data for the factor matrices Returns ------- list[Mode] """ return self._modes @property def order(self): """ Order of a tensor represented through the CPD Returns ------- order : int """ order = len(self.fmat) return order @property def rank(self): """ Rank of the CP representation of a tensor. Returns ------- rank : tuple Notes ----- Most often referred to as the Kryskal rank """ fmat = self.fmat[0] rank = (fmat.shape[1],) return rank @property def ft_shape(self): """ Shape of a ``TensorCPD`` in the full format Returns ------- full_shape : tuple """ full_shape = tuple(fmat.shape[0] for fmat in self.fmat) return full_shape @property def mode_names(self): """ Description of the physical modes for a ``TensorCPD`` Returns ------- list[str] """ return super(TensorCPD, self).mode_names
[docs] def reconstruct(self, keep_meta=0): """ Converts the CP representation of a tensor into a full tensor Parameters ---------- keep_meta : int Keep meta information about modes of the given `tensor`. 0 - the output will have default values for the meta data 1 - keep only mode names 2 - keep mode names and indices Returns ------- tensor : Tensor """ tensor = self.core for mode, fmat in enumerate(self.fmat): tensor.mode_n_product(fmat, mode=mode, inplace=True) if keep_meta == 1: mode_names = {i: mode.name for i, mode in enumerate(self.modes)} tensor.set_mode_names(mode_names=mode_names) elif keep_meta == 2: tensor.copy_modes(self) else: pass return tensor
[docs] def copy(self): """ Produces a copy of itself as a new object Returns ------- new_object : TensorCPD """ fmat = self._fmat core_values = self._core_values new_object = TensorCPD(fmat=fmat, core_values=core_values) new_object.copy_modes(self) return new_object
[docs] def copy_modes(self, tensor): """ Copy modes meta from tensor Parameters ---------- tensor : {Tensor, TensorCPD, TensorTKD, TensorTT} Returns ------- self : TensorCPD Notes ----- Most of the time this method should only be used by the decomposition algorithms """ # TODO: check for dimensionality super(TensorCPD, self).copy_modes(tensor=tensor) return self
[docs] def set_mode_names(self, mode_names): """ Rename modes of a tensor representation Parameters ---------- mode_names : dict New names for the tensor modes in form of a dictionary The name of the mode defined by the Key of the dict will be renamed to the corresponding Value Returns ------- self : TensorCPD """ super(TensorCPD, self).set_mode_names(mode_names=mode_names) return self
[docs] def reset_mode_name(self, mode=None): """ Set default name for the specified mode number Parameters ---------- mode : int Mode number which name to be set to default value By default resets names of all modes Returns ------- self : TensorCPD """ super(TensorCPD, self).reset_mode_name(mode=mode) return self
[docs] def set_mode_index(self, mode_index): """ Set index for specified mode Parameters ---------- mode_index : dict New indices for the factor matrices in form of a dictionary. Key defines the mode whose index to be changed. Value contains a list of new indices for this mode. Returns ------- self : TensorCPD """ super(TensorCPD, self).set_mode_index(mode_index=mode_index) return self
[docs] def reset_mode_index(self, mode=None): """ Drop index for the specified mode number Parameters ---------- mode : int Mode number which index to be dropped By default resets all indices Returns ------- self : TensorCPD """ super(TensorCPD, self).reset_mode_index(mode=mode) return self
[docs]class TensorTKD(BaseTensorTD): """ Representation of a tensor in the Tucker form (TKD). Parameters ---------- fmat : list[np.ndarray] List of factor matrices for the Tucker representation of a tensor core_values : np.ndarray Core of the Tucker representation of a tensor mode_names : list[str] List of names for the factor matrices Attributes ---------- _fmat : list[np.ndarray] Placeholder for a list of factor matrices for the Tucker representation of a tensor _core_values : np.ndarray Placeholder for a core of the Tucker representation of a tensor _modes : list[Mode] Description of the factor matrix for the corresponding mode Raises ------ TensorTopologyError If there is inconsistency in shapes of factor matrices and core values Examples -------- 1) Create tucker representation of a tensor with default meta information >>> import numpy as np >>> from hottbox.core import TensorTKD >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R_1, R_2, R_3 = 2, 3, 4 # multi-linear rank >>> A = np.ones((I, R_1)) >>> B = np.ones((J, R_2)) >>> C = np.ones((K, R_3)) >>> fmat = [A, B , C] >>> core_values = np.ones((R_1, R_2, R_3)) >>> tensor_tkd = TensorTKD(fmat, core_values) >>> print(tensor_tkd) Tucker representation of a tensor with multi-linear rank=(2, 3, 4). Factor matrices represent properties: ['mode-0', 'mode-1', 'mode-2'] With corresponding latent components described by (5, 6, 7) features respectively. 2) Create tucker representation of a tensor with custom meta information >>> import numpy as np >>> from hottbox.core import TensorTKD >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R_1, R_2, R_3 = 2, 3, 4 # multi-linear rank >>> A = np.ones((I, R_1)) >>> B = np.ones((J, R_2)) >>> C = np.ones((K, R_3)) >>> fmat = [A, B , C] >>> core_values=np.ones((R_1, R_2, R_3)) >>> mode_names=["Year", "Month", "Day"] >>> tensor_tkd = TensorTKD(fmat, core_values, mode_names) >>> print(tensor_tkd) Tucker representation of a tensor with multi-linear rank=(2, 3, 4). Factor matrices represent properties: ['Year', 'Month', 'Day'] With corresponding latent components described by (5, 6, 7) features respectively. """ def __init__(self, fmat, core_values, mode_names=None): super(TensorTKD, self).__init__() self._validate_init_data(fmat=fmat, core_values=core_values) self._fmat = [mat.copy() for mat in fmat] self._core_values = core_values.copy() self._modes = self._create_modes(mode_names=mode_names) def __eq__(self, other): """ Parameters ---------- other : TensorTKD Returns ------- equal : bool Notes ----- If dimensionality check fails then ``TensorTKD`` objects cannot be equal by definition. """ equal = False if isinstance(self, other.__class__): if self.ft_shape == other.ft_shape and self.rank == other.rank: fmat_equal = all([np.allclose(fmat, other.fmat[i], rtol=1e-05, atol=1e-08, equal_nan=True) for i, fmat in enumerate(self.fmat)]) core_equal = self.core == other.core modes_equal = all([mode == other.modes[i] for i, mode in enumerate(self.modes)]) equal = fmat_equal and core_equal and modes_equal return equal def __add__(self, other): """ Summation of objects of ``TensorTKD`` class Parameters ---------- other : TensorTKD Returns ------- tensor_tkd : TensorTKD Notes ----- Block-wise assignment of values from the core tensors via generating list of indices for blocks whose values will be reassigned ``https://stackoverflow.com/questions/44357591/assigning-values-to-a-block-in-a-numpy-array`` This implementation seems quit quick for the core tensors with relatively small sizes: 8.42 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) for mlrank = (10, 10, 10, 10, 10) """ if not isinstance(self, other.__class__): raise TypeError("Don't know how to sum object of {} class " "with an object of {} class!".format(self.__class__.__name__, other.__class__.__name__)) if self.ft_shape != other.ft_shape: raise TensorTopologyError("Both objects should have the same topology!\n" "{}!={} (`self.ft_shape != other.ft_shape`)".format(self.ft_shape, other.ft_shape)) if not all([self.modes[i].index == other.modes[i].index for i in range(self.order)]): raise ModeError("Both tensors should have the same indices!") # Stack core tensors along main diagonal. # Block-wise assignment of values from the core tensors # via generating list of indices for blocks whose values will be reassigned # https://stackoverflow.com/questions/44357591/assigning-values-to-a-block-in-a-numpy-array new_core_shape = tuple(sum(dims) for dims in zip(self.rank, other.rank)) core_values = np.zeros(new_core_shape) # has to be np.zeros. Don't use np.empty idx_list_1 = [[r for r in range(rank)] for rank in self.rank] idx_list_2 = [[r + self.rank[i] for r in range(rank)] for i, rank in enumerate(other.rank)] core_values[np.ix_(*idx_list_1)] = self._core_values core_values[np.ix_(*idx_list_2)] = other._core_values fmat_list = [np.concatenate((fmat, other.fmat[i]), axis=1) for i, fmat in enumerate(self.fmat)] tensor_tkd = TensorTKD(fmat=fmat_list, core_values=core_values).copy_modes(self) if self.mode_names != other.mode_names: for i in range(tensor_tkd.order): tensor_tkd.reset_mode_name(mode=i) return tensor_tkd def __str__(self): """ Provides general information about this instance.""" return "Tucker representation of a tensor with multi-linear rank={}.\n" \ "Factor matrices represent properties: {}\n" \ "With corresponding latent components described by {} features respectively.".format(self.rank, self.mode_names, self.ft_shape) def __repr__(self): return str(self) @staticmethod def _validate_init_data(fmat, core_values): """ Validate data for the TensorTKD constructor Parameters ---------- fmat : list[np.ndarray] List of factor matrices for the Tucker representation of a tensor core_values : np.ndarray Core of the Tucker representation of a tensor """ if not isinstance(core_values, np.ndarray): raise TypeError("Core values (`core_values`) should be a numpy array") if not isinstance(fmat, list): raise TypeError("All factor matrices (`fmat`) should be passed as a list!") for mat in fmat: if not isinstance(mat, np.ndarray): raise TypeError("Each of the factor matrices should be a numpy array!") if mat.ndim != 2: raise TensorTopologyError("Each of the factor matrices should a 2-dimensional numpy array!") ml_rank = core_values.shape order = core_values.ndim if len(fmat) != order: raise TensorTopologyError("Not enough or too many factor matrices for the specified core tensor!\n" "{}!={} (`len(fmat) != core_values.ndim`)".format(len(fmat), order)) mat_shapes = tuple(mat.shape[1] for mat in fmat) if not all([mat_shapes[i] == ml_rank[i] for i in range(order)]): raise TensorTopologyError("Dimension mismatch between the factor matrices and the core tensor!\n" "The number of columns of a factor matrix should match the corresponding " "dimension size of the core tensor:\n" "{} != {} (fmat[i].shape[1] != core_values.shape)".format(mat_shapes, ml_rank)) def _create_modes(self, mode_names): """ Create meta data for each factor matrix Parameters ---------- mode_names : list[str] Returns ------- modes : list[Mode] """ modes = super(TensorTKD, self)._create_modes(mode_names=mode_names) return modes @property def core(self): """ Core tensor of the Tucker representation of a tensor Returns ------- core_tensor : Tensor """ core_tensor = Tensor(self._core_values) return core_tensor @property def fmat(self): """ List of factor matrices for the Tucker representation of a tensor Returns ------- factor_matrices : list[np.ndarray] """ factor_matrices = self._fmat return factor_matrices @property def modes(self): """ Meta data for the factor matrices Returns ------- list[Mode] """ return self._modes @property def order(self): """ Order of a tensor represented through the TKD Returns ------- order : int """ order = len(self.fmat) return order @property def rank(self): """ Multi-linear rank of the Tucker representation of a tensor Returns ------- rank : tuple Notes ----- Most often referred to as the Tucker rank """ rank = tuple(fmat.shape[1] for fmat in self.fmat) return rank @property def ft_shape(self): """ Shape of a ``TensorTKD`` in the full format Returns ------- full_shape : tuple """ full_shape = tuple(fmat.shape[0] for fmat in self.fmat) return full_shape @property def mode_names(self): """ Description of the physical modes for a ``TensorTKD`` Returns ------- list[str] """ return super(TensorTKD, self).mode_names
[docs] def reconstruct(self, keep_meta=0): """ Converts the Tucker representation of a tensor into a full tensor Parameters ---------- keep_meta : int Keep meta information about modes of the given `tensor`. 0 - the output will have default values for the meta data 1 - keep only mode names 2 - keep mode names and indices Returns ------- tensor : Tensor """ tensor = self.core for mode, fmat in enumerate(self.fmat): tensor.mode_n_product(fmat, mode=mode, inplace=True) if keep_meta == 1: mode_names = {i: mode.name for i, mode in enumerate(self.modes)} tensor.set_mode_names(mode_names=mode_names) elif keep_meta == 2: tensor.copy_modes(self) else: pass return tensor
[docs] def copy(self): """ Produces a copy of itself as a new object Returns ------- new_object : TensorTKD """ fmat = self._fmat core_values = self._core_values new_object = TensorTKD(fmat=fmat, core_values=core_values) new_object.copy_modes(self) return new_object
[docs] def copy_modes(self, tensor): """ Copy modes meta from tensor Parameters ---------- tensor : {Tensor, TensorCPD, TensorTKD, TensorTT} Returns ------- self : TensorTKD Notes ----- Most of the time this method should only be used by the decomposition algorithms """ # TODO: check for dimensionality super(TensorTKD, self).copy_modes(tensor=tensor) return self
[docs] def set_mode_names(self, mode_names): """ Rename modes of a tensor representation Parameters ---------- mode_names : dict New names for the tensor modes in form of a dictionary The name of the mode defined by the Key of the dict will be renamed to the corresponding Value Returns ------- self : TensorTKD """ super(TensorTKD, self).set_mode_names(mode_names=mode_names) return self
[docs] def reset_mode_name(self, mode=None): """ Set default name for the specified mode number Parameters ---------- mode : int Mode number which name to be set to default value By default resets names of all modes Returns ------- self : TensorTKD """ super(TensorTKD, self).reset_mode_name(mode=mode) return self
[docs] def set_mode_index(self, mode_index): """ Set index for specified mode Parameters ---------- mode_index : dict New indices for the factor matrices in form of a dictionary. Key defines the mode whose index to be changed. Value contains a list of new indices for this mode. Returns ------- self : TensorTKD """ super(TensorTKD, self).set_mode_index(mode_index=mode_index) return self
[docs] def reset_mode_index(self, mode=None): """ Drop index for the specified mode number Parameters ---------- mode : int Mode number which index to be dropped By default resets all indices Returns ------- self : TensorTKD """ super(TensorTKD, self).reset_mode_index(mode=mode) return self
[docs]class TensorTT(BaseTensorTD): """ Representation of a tensor in the Tensor Train form (TT). Parameters ---------- core_values : list[np.ndarray] List of cores for the Tensor Train representation of a tensor. mode_names : list[str] List of names for the physical modes Attributes ---------- _core_values : list[np.ndarray] Placeholder for a list of cores for the Tensor Train representation of a tensor. _modes : list[Mode] Description of the physical modes Raises ------ TensorTopologyError If there is inconsistency in shapes of core values Examples -------- 1) Create tensor train representation of a tensor with default meta information >>> import numpy as np >>> from hottbox.core import TensorTT >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R_1, R_2 = 3, 4 # tt-rank >>> core_0 = np.ones((I, R_1)) >>> core_1 = np.ones((R1, J, R2)) >>> core_2 = np.ones((R2, K)) >>> core_values = [core_0, core_1, core_2] >>> tensor_tt = TensorTT(core_values) >>> print(tensor_tt) Tensor train representation of a tensor with tt-rank=(3, 4). Shape of this representation in the full format is (5, 6, 7). Physical modes of its cores represent properties: ['mode-0', 'mode-1', 'mode-2'] 2) Create tensor train representation of a tensor with custom meta information >>> import numpy as np >>> from hottbox.core import TensorTT >>> I, J, K = 5, 6, 7 # shape of the tensor in full form >>> R_1, R_2 = 3, 4 # tt-rank >>> core_0 = np.ones((I, R_1)) >>> core_1 = np.ones((R_1, J, R_2)) >>> core_2 = np.ones((R_2, K)) >>> core_values = [core_0, core_1, core_2] >>> mode_names = ["Year", "Month", "Day"] >>> tensor_tt = TensorTT(core_values, mode_names) >>> print(tensor_tt) Tensor train representation of a tensor with tt-rank=(3, 4). Shape of this representation in the full format is (5, 6, 7). Physical modes of its cores represent properties: ['Year', 'Month', 'Day'] """ def __init__(self, core_values, mode_names=None): super(TensorTT, self).__init__() self._validate_init_data(core_values=core_values) self._core_values = [core.copy() for core in core_values] self._modes = self._create_modes(mode_names=mode_names) def __eq__(self, other): """ Parameters ---------- other : TensorTT Returns ------- equal : bool Notes ----- If dimensionality check fails then ``TensorTT`` objects cannot be equal by definition. """ equal = False if isinstance(self, other.__class__): if self.ft_shape == other.ft_shape and self.rank == other.rank: cores_equal = all([core == other.core(i) for i, core in enumerate(self.cores)]) modes_equal = all([mode == other.modes[i] for i, mode in enumerate(self.modes)]) equal = cores_equal and modes_equal return equal def __str__(self): """ Provides general information about this instance.""" return "Tensor train representation of a tensor with tt-rank={}.\n" \ "Shape of this representation in the full format is {}.\n" \ "Physical modes of its cores represent properties: {}".format(self.rank, self.ft_shape, self.mode_names) def __repr__(self): return str(self) def _validate_init_data(self, core_values): """ Validate data for the TensorTT constructor Parameters ---------- core_values : list[np.ndarray] List of cores for the Tensor Train representation of a tensor. """ # validate types of the input data if not isinstance(core_values, list): raise TypeError("The parameter `core_values` should be passed as list!") for core in core_values: if not isinstance(core, np.ndarray): raise TypeError("Each element from `core_values` should be a numpy array!") # validate sizes of the cores if (core_values[0].ndim != 2) or (core_values[-1].ndim != 2): raise TensorTopologyError("The first and the last elements of the `core_values` " "should be 2-dimensional numpy arrays!") for i in range(1, len(core_values) - 1): if core_values[i].ndim != 3: raise TensorTopologyError("All but first and the last elements of the `core_values` " "should be 3-dimensional numpy arrays!") for i in range(len(core_values)-1): if core_values[i].shape[-1] != core_values[i+1].shape[0]: raise TensorTopologyError("Dimension mismatch for the specified cores:\n" "Last dimension of core_values[{}] should be the same as the " "first dimension of core_values[{}]".format(i, i+1)) def _create_modes(self, mode_names): """ Create meta data for each factor matrix Parameters ---------- mode_names : list[str] Returns ------- modes : list[Mode] """ modes = super(TensorTT, self)._create_modes(mode_names=mode_names) return modes
[docs] def copy(self): """ Produces a copy of itself as a new object Returns ------- new_object : TensorTT """ core_values = self._core_values new_object = TensorTT(core_values=core_values) new_object.copy_modes(self) return new_object
[docs] def core(self, i): """ Specific core of the TensorTT representation Parameters ---------- i : int Should not exceed the order of ``TensorTT.order`` representation Returns ------- core_tensor : Tensor """ if abs(i) >= self.order: raise IndexError("List index out of range!\n" "Index for the core of interest cannot exceed the order of TT representation: " "abs({}) >= {} (abs(i) >= self.order)".format(i, self.order)) core_tensor = Tensor(self._core_values[i]) return core_tensor
@property def cores(self): """ All cores of the TensorTT representation Returns ------- core_list : list[Tensor] """ core_list = [self.core(i) for i in range(len(self._core_values))] return core_list @property def modes(self): """ Meta data for the factor matrices Returns ------- list[Mode] """ return self._modes @property def order(self): """ Order of a tensor represented through the TT Returns ------- order : int """ return len(self._core_values) @property def rank(self): """ Rank of the TT representation of a tensor Returns ------- rank : tuple Notes ----- Most often referred to as the TT rank """ return tuple(core_values.shape[-1] for core_values in self._core_values[:-1]) @property def ft_shape(self): """ Shape of a ``TensorTT`` in the full format Returns ------- full_shape : tuple """ full_shape = [None] * len(self.cores) full_shape[0] = self.cores[0].shape[0] full_shape[-1] = self.cores[-1].shape[1] for i in range(1, len(self.cores) - 1): full_shape[i] = self.cores[i].shape[1] return tuple(full_shape) @property def mode_names(self): """ Description of the physical modes for a ``TensorTT`` Returns ------- list[str] """ return super(TensorTT, self).mode_names
[docs] def reconstruct(self, keep_meta=0): """ Converts the TT representation of a tensor into a full tensor Parameters ---------- keep_meta : int Keep meta information about modes of the given `tensor`. 0 - the output will have default values for the meta data 1 - keep only mode names 2 - keep mode names and indices Returns ------- tensor : Tensor """ rank = self.rank + (1,) core = self.cores[0] data = core.data for i, core in enumerate(self.cores[1:]): shape_2d = [rank[i], rank[i+1] * self.ft_shape[i + 1]] core_flat = np.reshape(core.data, shape_2d, order='F') data = np.reshape(data, [-1, rank[i]], order='F') data = np.dot(data, core_flat) data = np.reshape(data, self.ft_shape, order='F') tensor = Tensor(data) if keep_meta == 1: mode_names = {i: mode.name for i, mode in enumerate(self.modes)} tensor.set_mode_names(mode_names=mode_names) elif keep_meta == 2: tensor.copy_modes(self) else: pass return tensor
[docs] def copy_modes(self, tensor): """ Copy modes meta from tensor Parameters ---------- tensor : {Tensor, TensorCPD, TensorTKD, TensorTT} Returns ------- self : TensorTT Notes ----- Most of the time this method should only be used by the decomposition algorithms """ # TODO: check for dimensionality super(TensorTT, self).copy_modes(tensor=tensor) return self
[docs] def set_mode_names(self, mode_names): """ Rename modes of a tensor representation Parameters ---------- mode_names : dict New names for the tensor modes in form of a dictionary The name of the mode defined by the Key of the dict will be renamed to the corresponding Value Returns ------- self : TensorTT """ super(TensorTT, self).set_mode_names(mode_names=mode_names) return self
[docs] def reset_mode_name(self, mode=None): """ Set default name for the specified mode number Parameters ---------- mode : int Mode number which name to be set to default value By default resets names of all modes Returns ------- self : TensorTT """ super(TensorTT, self).reset_mode_name(mode=mode) return self
[docs] def set_mode_index(self, mode_index): """ Set index for specified mode Parameters ---------- mode_index : dict New indices for the factor matrices in form of a dictionary. Key defines the mode whose index to be changed. Value contains a list of new indices for this mode. Returns ------- self : TensorTT """ super(TensorTT, self).set_mode_index(mode_index=mode_index) return self
[docs] def reset_mode_index(self, mode=None): """ Drop index for the specified mode number Parameters ---------- mode : int Mode number which index to be dropped By default resets all indices Returns ------- self : TensorTT """ super(TensorTT, self).reset_mode_index(mode=mode) return self