DaMAT.ttObject

This is a python class for tensors in TT-format. Through this object you can compute TT-decomposition of multidimensional arrays in one shot using TTSVD algorithm or incrementally using TT-ICE algorithm.

Furthermore, this object allows exporting and importing TT-cores using native format (.ttc) and intermediary formats (.txt).

   1# -*- coding: utf-8 -*-
   2"""
   3This is a python class for tensors in TT-format.
   4Through this object you can compute TT-decomposition of multidimensional arrays in
   5one shot using [TTSVD algorithm](https://epubs.siam.org/doi/epdf/10.1137/090752286)
   6or incrementally using [TT-ICE algorithm](https://arxiv.org/abs/2211.12487).
   7
   8Furthermore, this object allows exporting and importing TT-cores using native
   9format (`.ttc`) and intermediary formats (`.txt`).
  10"""
  11
  12import warnings
  13import time
  14
  15import numpy as np
  16
  17from logging import warning
  18from .utils import deltaSVD, ttsvd, coreContraction, mode_n_unfolding
  19from pickle import dump, load
  20
  21
  22class ttObject:
  23    """
  24    Python object for tensors in Tensor-Train format.
  25
  26    This object computes the TT-decomposition of multidimensional arrays using `TTSVD`_,
  27    `TT-ICE`_, and `TT-ICE*`_.
  28    It furthermore contains an inferior method, ITTD, for benchmarking purposes.
  29
  30    This object handles various operations in TT-format such as dot product and norm.
  31    Also provides supporting operations for uncompressed multidimensional arrays such as
  32    reshaping and transpose.
  33    Once the tensor train approximation for a multidimensional array is computed, you can
  34    compute projection of appropriate arrays onto the TT-cores, reconstruct projected
  35    or compressed tensors and compute projection/compression accuracy.
  36
  37    You can also save/load tensors as `.ttc` or `.txt` files.
  38
  39    Attributes
  40    ----------
  41    inputType: type
  42        Type of the input data. This determines how the object is initialized.
  43    originalData:
  44        Original multidimensional input array. Generally will not be stored
  45        after computing
  46        an initial set of TT-cores.
  47    method: :obj:`str`
  48        Method of computing the initial set of TT-cores. Currently only accepts
  49        `'ttsvd'` as input
  50    ttEpsilon: :obj:`float`
  51        Desired relative error upper bound.
  52    ttCores: :obj:`list` of :obj:`numpy.array`
  53        Cores of the TT-decomposition. Stored as a list of numpy arrays.
  54    ttRanks: :obj:`list` of :obj:`int`
  55        TT-ranks of the decomposition. Stored as a list of integers.
  56    nCores: :obj:`int`
  57        Number of TT-cores in the decomposition.
  58    nElements: :obj:`int`
  59        Number of entries present in the current `ttObject`.
  60    originalShape: :obj:`tuple` or :obj:`list`
  61        Original shape of the multidimensional array.
  62    reshapedShape: :obj:`tuple` or :obj:`list`
  63        Shape of the multidimensional array after reshaping. Note that
  64        this attribute is only meaningful before computing a TT-decomposition
  65    indexOrder: :obj:`list` of :obj:`int`
  66        Keeps track of original indices in case of transposing the input array.
  67    .. _TTSVD:
  68        https://epubs.siam.org/doi/epdf/10.1137/090752286
  69        _TT-ICE:
  70        https://arxiv.org/abs/2211.12487
  71        _TT-ICE*:
  72        https://arxiv.org/abs/2211.12487
  73
  74    """
  75
  76    def __init__(
  77        self,
  78        data,
  79        epsilon: float = None,
  80        keepData: bool = False,
  81        samplesAlongLastDimension: bool = True,
  82        method: str = "ttsvd",
  83    ) -> None:
  84        """
  85        Initializes the ttObject.
  86
  87        Parameters
  88        ----------
  89        data: :obj:`numpy.array` or :obj:`list`
  90            Main input to the ttObject. It can either be a multidimensional `numpy array`
  91            or `list of numpy arrays`.
  92            If list of numpy arrays are presented as input, the object will interpret it
  93            as the TT-cores of an existing decomposition.
  94        epsilon: :obj:`float`, optional
  95            The relative error upper bound desired for approximation. Optional for cases
  96            when `data` has type `list`.
  97        keepData: :obj:`bool`, optional
  98            Optional boolean variable to determine if the original array will be kept
  99            after compression. Set to `False` by default.
 100        samplesAlongLastDimension: :obj:`bool`, optional
 101            Boolean variable to ensure if the samples are stacked along the last
 102            dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE.
 103        method: :obj:`str`, optional
 104            Determines the computation method for tensor train decomposition of
 105            the multidimensional array presented as `data`. Set to `'ttsvd'` by default.
 106
 107            Currently the package only has support for ttsvd, additional support such as
 108            `ttcross` might be included in the future.
 109
 110        """
 111        # self.ttRanks=ranks
 112        self.ttCores = None
 113        self.nCores = None
 114        self.nElements = None
 115        self.inputType = type(data)
 116        self.method = method
 117        self.keepOriginal = keepData
 118        self.originalData = data
 119        self.samplesAlongLastDimension = samplesAlongLastDimension
 120        if self.inputType is np.memmap:
 121            self.inputType = np.ndarray
 122
 123        if self.inputType == np.ndarray:
 124            self.ttEpsilon = epsilon
 125            self.originalShape = list(data.shape)
 126            self.reshapedShape = self.originalShape
 127            self.indexOrder = [idx for idx in range(len(self.originalShape))]
 128        elif self.inputType == list:
 129            self.nCores = len(data)
 130            self.ttCores = data
 131            self.reshapedShape = [core.shape[1] for core in self.ttCores]
 132        else:
 133            raise TypeError("Unknown input type!")
 134
 135    @property
 136    def coreOccupancy(self) -> None:
 137        """
 138        :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each
 139        TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*`
 140        algorithm
 141        """
 142        try:
 143            return [
 144                core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1]
 145            ]
 146        except ValueError:
 147            warnings.warn(
 148                "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning
 149            )
 150            return None
 151
 152    @property
 153    def compressionRatio(self) -> float:
 154        """
 155        :obj:`float`: A metric showing how much compression with respect to the
 156        original multidimensional array is achieved.
 157        """
 158        originalNumEl = 1
 159        compressedNumEl = 0
 160        for core in self.ttCores:
 161            originalNumEl *= core.shape[1]
 162            compressedNumEl += np.prod(core.shape)
 163        return originalNumEl / compressedNumEl
 164
 165    def changeShape(self, newShape: tuple or list) -> None:
 166        """
 167        Function to change shape of input tensors and keeping track of the reshaping.
 168        Reshapes `originalData` and saves the final shape in `reshapedShape`
 169
 170        Note
 171        ----
 172        A simple `numpy.reshape` would be sufficient for this purpose but in order to keep
 173        track of the shape changes the `reshapedShape` attribute also needs to be updated
 174        accordingly.
 175
 176        Parameters
 177        ----------
 178        newShape:obj:`tuple` or `list`
 179            New shape of the tensor
 180
 181        Raises
 182        ------
 183        warning
 184            If an attempt is done to modify the shape after computing a TT-decomposition.
 185            This is important since it will cause incompatibilities in other functions
 186            regarding the shape of the uncompressed tensor.
 187
 188        """
 189        if self.ttCores is not None:
 190            warning(
 191                "Warning! You are reshaping the original data after computing a\
 192                TT-decomposition! We will proceed without reshaping self.originalData!!"
 193            )
 194            return None
 195        self.reshapedShape = newShape
 196        self.originalData = np.reshape(self.originalData, self.reshapedShape)
 197        self.reshapedShape = list(self.originalData.shape)
 198        # Changed reshapedShape to a list for the trick in ttICEstar
 199        if self.samplesAlongLastDimension:
 200            self.singleDataShape = self.reshapedShape[:-1]
 201            # This line assumes we keep the last index as the samples index and don't
 202            # interfere with its shape
 203
 204    def computeTranspose(self, newOrder: list) -> None:
 205        """
 206        Transposes the axes of `originalData`.
 207
 208        Similar to `changeShape`, a simple
 209        `numpy.transpose` would be sufficient for this purpose but in order to
 210        keep track of the transposition order `indexOrder` attribute also needs
 211        to be updated accordingly.
 212
 213        Parameters
 214        ----------
 215        newOrder:obj:`list`
 216            New order of the axes.
 217
 218        Raises
 219        ------
 220        ValueError
 221            When the number of transposition axes are not equal to the number of
 222            dimensions of `originalData`.
 223        """
 224        assert self.inputType == np.ndarray and self.ttCores is None
 225        if len(newOrder) != len(self.indexOrder):
 226            raise ValueError(
 227                "size of newOrder and self.indexOrder does not match. \
 228                    Maybe you forgot reshaping?"
 229            )
 230        self.indexOrder = [self.indexOrder[idx] for idx in newOrder]
 231        self.originalData = self.originalData.transpose(newOrder)
 232        self.reshapedShape = list(self.originalData.shape)
 233
 234    def indexMap(self) -> None:
 235        """
 236        Function to map the original indices to the reshaped indices.
 237        Currently not implemented.
 238        """
 239        self.A = 2
 240
 241    def primeReshaping(self) -> None:
 242        """
 243        Function to reshape the first d dimensions to prime factors.
 244        Currently not implemented
 245        """
 246        self.A = 2
 247
 248    def saveData(
 249        self, fileName: str, directory="./", justCores=True, outputType="ttc"
 250    ) -> None:
 251        """
 252        Writes the computed TT-cores to an external file.
 253
 254        Parameters
 255        ----------
 256        fileName:obj:`str`
 257
 258        directory:obj:`str`
 259            Location to save files with respect to the present working directory.
 260        justCores:obj:`bool`
 261            Boolean variable to determine if `originalData` will be discarded
 262            or not while saving.
 263        outputType:obj:`str`
 264            Type of the output file. `ttc` for pickled `ttObject`, `txt` for
 265            individual text files for each TT-core.
 266        """
 267        if justCores:
 268            if outputType == "ttc":
 269                with open(directory + fileName + ".ttc", "wb") as saveFile:
 270                    temp = ttObject(self.ttCores)
 271                    for attribute in vars(self):
 272                        if attribute != "originalData":
 273                            setattr(temp, attribute, eval(f"self.{attribute}"))
 274                    dump(temp, saveFile)
 275            elif outputType == "txt":
 276                for coreIdx, core in enumerate(self.ttCores):
 277                    np.savetxt(
 278                        directory + f"{fileName}_{coreIdx}.txt",
 279                        core.reshape(-1, core.shape[-1]),
 280                        header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}",
 281                        delimiter=" ",
 282                    )
 283            elif outputType == "npy":
 284                for coreIdx, core in enumerate(self.ttCores):
 285                    np.save(
 286                        directory + f"{fileName}_{coreIdx}.npy",
 287                        core,
 288                    )
 289            else:
 290                raise ValueError(f"Output type {outputType} is not supported!")
 291        else:
 292            if outputType == "txt":
 293                raise ValueError(
 294                    ".txt type outputs are only supported for justCores=True!!"
 295                )
 296            if self.method == "ttsvd":
 297                with open(directory + fileName + ".ttc", "wb") as saveFile:
 298                    dump(self, saveFile)
 299            else:
 300                raise ValueError("Unknown Method!")
 301
 302    @staticmethod
 303    def loadData(fileName: str, numCores=None) -> "ttObject":
 304        """
 305        Loads data from a `.ttc`, `.npy`, or `.txt` file
 306
 307
 308        Static method to load TT-cores into a ttObject object.
 309        Note
 310        ----
 311        If data is stored in {coreFile}_{coreIdx}.txt format,
 312        the input fileName should just be coreFile.txt
 313
 314        Parameters
 315        ----------
 316        fileName:obj:`str`
 317            Name of the file that will be loaded.
 318        numCores:obj:`int`
 319            Number of cores that the resulting `ttObject` will have
 320            (Only required when input data format is `.txt`)
 321        """
 322        fileExt = fileName.split(".")[-1]
 323        if fileExt == "ttc":
 324            with open(fileName, "rb") as f:
 325                dataSetObject = load(f)
 326            return dataSetObject
 327        elif fileExt == "txt":
 328            if numCores is None:
 329                raise ValueError("Number of cores are not defined!!")
 330            fileBody = fileName.split(".")[0]
 331            coreList = []
 332            for coreIdx in range(numCores):
 333                with open(f"{fileBody}_{coreIdx}.{fileExt}"):
 334                    coreShape = f.readline()[2:-1]
 335                    coreShape = [int(item) for item in coreShape.split(" ")]
 336                coreList.append(
 337                    np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape]
 338                )
 339            return coreList
 340        elif fileExt == "npy":
 341            if numCores is None:
 342                raise ValueError("Number of cores are not defined!!")
 343            fileBody = fileName.split(".")[0]
 344            coreList = []
 345            for coreIdx in range(numCores):
 346                coreList.append(
 347                    np.load(f"{fileBody}_{coreIdx}.{fileExt}")
 348                )
 349            return ttObject(coreList)
 350        else:
 351            raise ValueError(f"{fileExt} files are not supported!")
 352
 353    @staticmethod
 354    def ttDot(tt1, tt2) -> float():
 355        """
 356        Computes the dot product of two tensors in TT format.
 357
 358        Parameters
 359        ----------
 360        tt1:obj:`ttObject`
 361            Tensor in TT-format
 362        tt2:obj:`ttObject`
 363            Tensor in TT-format
 364
 365        Returns
 366        -------
 367        prod:obj:`float`
 368            Dot product of `tt1` and `tt2`.
 369
 370        Raises
 371        ------
 372        AttributeError
 373            When either one of the input parameters are not `ttObject`s.
 374
 375        """
 376        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
 377            if isinstance(tt1, list):
 378                tt1 = ttObject(tt1)
 379            if isinstance(tt2, list):
 380                tt2 = ttObject(tt2)
 381            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
 382                raise AttributeError("One of the passed objects is not in TT-format!")
 383        prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :])
 384        for i1 in range(1, tt1.ttCores[0].shape[1]):
 385            prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :])
 386        for coreIdx in range(1, len(tt1.ttCores)):
 387            p = []
 388            for ik in range(tt1.ttCores[coreIdx].shape[1]):
 389                p.append(
 390                    prod
 391                    @ (
 392                        np.kron(
 393                            tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :]
 394                        )
 395                    )
 396                )
 397            prod = np.sum(p, axis=0)
 398        return prod.item()
 399
 400    @staticmethod
 401    def ttNorm(tt1) -> float:
 402        """
 403        Computes norm of a tensor in TT-format.
 404
 405        Parameters
 406        ----------
 407        tt1:obj:`ttObject`
 408            Tensor in TT-format
 409
 410        Returns
 411        -------
 412        norm:obj:`float`
 413            Norm of `tt1`.
 414
 415        Raises
 416        ------
 417        AttributeError
 418            When the input parameter is not a `ttObject`.
 419        """
 420        if not isinstance(tt1, ttObject):
 421            if isinstance(tt1, list):
 422                tt1 = ttObject(tt1)
 423            else:
 424                raise AttributeError("Passed object is not in TT-format!")
 425        try:
 426            norm = ttObject.ttDot(tt1, tt1)
 427            norm = np.sqrt(norm)
 428        except MemoryError:
 429            norm = np.linalg.norm(tt1.ttCores[-1])
 430        return norm
 431
 432    def projectTensor(self, newData: np.array, upTo=None) -> np.array:
 433        """
 434        Projects tensors onto basis spanned by TT-cores.
 435
 436        Given a tensor with appropriate dimensions, this function leverages
 437        the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are
 438        column-orthonormal in the mode-2 unfolding.
 439
 440        Note
 441        ----
 442        This function will not yield correct results if the TT-cores are
 443        not comprised of orthogonal vectors.
 444
 445        Parameters
 446        ----------
 447        newData:obj:`np.aray`
 448            Tensor to be projected
 449        upTo:obj:`int`, optional
 450            Index that the projection will be terminated. If an integer is
 451            passed as this parameter, `newTensor` will be projected up to
 452            (not including) the core that has index `upTo`. Assumes 1-based
 453            indexing.
 454
 455         .. _TTSVD:
 456            https://epubs.siam.org/doi/epdf/10.1137/090752286
 457            _TT-ICE:
 458            https://arxiv.org/abs/2211.12487
 459            _TT-ICE*:
 460            https://arxiv.org/abs/2211.12487
 461        """
 462        for coreIdx, core in enumerate(self.ttCores):
 463            if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo:
 464                break
 465            newData = (
 466                core.reshape(np.prod(core.shape[:2]), -1).transpose()
 467            ) @ newData.reshape(
 468                self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1
 469            )
 470        return newData
 471
 472    def reconstruct(self, projectedData, upTo=None):
 473        """
 474        Reconstructs tensors using TT-cores.
 475
 476        Assumes that `projectedData` is a slice from the last
 477        TT-core.
 478        While reconstructing any projected tensor from  `projectTensor`,
 479        this function leverages the fact that TT-cores obtained through
 480        `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2
 481        unfolding.
 482
 483        Note
 484        ----
 485        This function might not yield correct results for projected tensors
 486        if the TT-cores are not comprised of orthogonal vectors.
 487
 488        Parameters
 489        ----------
 490        projectedData:obj:`np.aray`
 491            A tensor slice (or alternatively an array)
 492        upTo:obj:`int`, optional
 493            Index that the reconstruction will be terminated. If an integer is
 494            passed as this parameter, `projectedData` will be projected up to
 495            (not including) the core that has index `upTo`. Assumes 1-based
 496            indexing.
 497        .. _TTSVD:
 498            https://epubs.siam.org/doi/epdf/10.1137/090752286
 499            _TT-ICE:
 500            https://arxiv.org/abs/2211.12487
 501
 502        """
 503        if upTo is None:
 504            upTo = len(self.ttCores) - 1  # Write the core index in 1-indexed form!!
 505        for core in self.ttCores[:upTo][::-1]:
 506            projectedData = np.tensordot(core, projectedData, axes=(-1, 0))
 507        return projectedData
 508
 509    def updateRanks(self) -> None:
 510        """
 511        Updates the ranks of the `ttObject` after incremental updates.
 512        """
 513        self.ttRanks = [1]
 514        for core in self.ttCores:
 515            self.ttRanks.append(core.shape[-1])
 516        return None
 517
 518    def computeRelError(self, newTensor: np.array, useExact=True) -> np.array:
 519        """
 520        Computes relative error by projecting data onto TT-cores.
 521
 522        This function computes the error induced by projecting `data` onto the TT-cores
 523        of `ttObject`. The last index of `newTensor` is assumed to be the number of
 524        individual observations.
 525
 526        Note
 527        ----
 528        - In order to compute the projection onto the TT-cores, the dimensions of `data`
 529        should match that of `ttObject`.
 530        - If a single observation will be passed as `newTensor`, an additional
 531        index/dimension should be introduced either through reshaping or [:,None]
 532
 533        Parameters
 534        ----------
 535        newTensor:obj:`np.array`
 536            Tensor for which the projection error is computed
 537        useExact:`bool`
 538            Boolean flag determining whether a projection error will be
 539            returned for the entire batch or each observation in the batch.
 540
 541        Returns
 542        -------
 543        relError:obj:`np.array`
 544            Array of relative errors
 545        """
 546        elementwiseNorm = np.linalg.norm(newTensor, axis=0)
 547        for _ in range(len(newTensor.shape) - 2):
 548            elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
 549        projectedData = self.projectTensor(newTensor)
 550        reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape)
 551        difference = newTensor - reconstructedData
 552        differenceNorm = np.linalg.norm(difference, axis=0)
 553        for _ in range(len(difference.shape) - 2):
 554            differenceNorm = np.linalg.norm(differenceNorm, axis=0)
 555        
 556        if useExact:
 557            relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
 558        else:
 559            relError = differenceNorm / elementwiseNorm
 560        return relError
 561
 562    def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None:
 563        """
 564        Function to compute relative error by reconstructing data from slices
 565        of TT-cores.
 566
 567        Parameters
 568        ----------
 569        data:obj:`np.array`
 570            Tensor for which the reconstruction error is computed
 571        start:`int`
 572            Index for the start of the batch in the compressed last core
 573        finish:`int`
 574            Index for the end of the batch in the compressed last core 
 575        useExact:`bool`
 576            Boolean flag determining whether a reconstruction error will be
 577            returned for the entire batch or each observation in the batch.
 578
 579        Returns
 580        -------
 581        recError:obj:`float` or `np.array`
 582            Reconstruction error of the data from cores. Returns an array
 583            of reconstruction errors for each observation in the batch if
 584            `useExact` is `False`.
 585        """
 586        if start is None:
 587            start = self.ttCores[-1].shape[1]-1
 588        if finish is None:
 589            finish = start+1
 590        rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape)
 591        elementwiseNorm=np.linalg.norm(data,axis=0)
 592        for idx in range(len(data.shape)-2):
 593            elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0)
 594        difference=data-rec
 595        differenceNorm=np.linalg.norm(difference,axis=0)
 596        for idx in range(len(difference.shape)-2):
 597            differenceNorm=np.linalg.norm(differenceNorm,axis=0)
 598
 599        if useExact:
 600            recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
 601        else:
 602            recError=differenceNorm/elementwiseNorm
 603
 604        return recError
 605
 606    def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores":
 607        """
 608        Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm.
 609
 610        Currently only supports `ttsvd` as method. In the future additional formats may
 611        be covered.
 612
 613        Parameters
 614        ----------
 615        norm:obj:`float`, optional
 616            Norm of the tensor to be compressed
 617        dtype:obj:`type`, optional
 618            Desired data type for the compression. Intended to allow lower precision
 619            if needed.
 620
 621        Raises
 622        ------
 623        ValueError
 624            When `method` is not one of the admissible methods.
 625
 626
 627        The following attributes are modified as a result of this function:
 628        -------
 629        - `ttObject.ttCores`
 630        - `ttObject.ttRanks`
 631        - `ttObject.compressionRatio`
 632
 633        .. _TTSVD:
 634            https://epubs.siam.org/doi/epdf/10.1137/090752286
 635        """
 636        if norm is None:
 637            norm = np.linalg.norm(self.originalData)
 638        if self.method == "ttsvd":
 639            startTime = time.time()
 640            self.ttRanks, self.ttCores = ttsvd(
 641                self.originalData, norm, self.ttEpsilon, dtype=dtype
 642            )
 643            self.compressionTime = time.time() - startTime
 644            self.nCores = len(self.ttCores)
 645            self.nElements = 0
 646            for cores in self.ttCores:
 647                self.nElements += np.prod(cores.shape)
 648            if not self.keepOriginal:
 649                self.originalData = None
 650            return None
 651        else:
 652            raise ValueError("Method unknown. Please select a valid method!")
 653
 654    def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None:
 655        """
 656        `TT-ICE`_ algorithmn without any heuristic upgrades.
 657
 658        Given a set of TT-cores, this function provides incremental updates
 659        to the TT-cores to approximate `newTensor` within a relative error
 660        defined in `epsilon`
 661
 662        Note
 663        ----
 664        This algorithm/function relies on the fact that TT-cores are columnwise
 665        orthonormal in the mode-2 unfolding.
 666
 667        Parameters
 668        ----------
 669        newTensor:obj:`np.array`
 670            New/streamed tensor that will be used to expand the orthonormal bases defined
 671            in TT-cores
 672        epsilon:obj:`float`, optional
 673            Relative error upper bound for approximating `newTensor` after incremental
 674            updates. If not defined, `ttObject.ttEpsilon` is used.
 675        tenNorm:obj:`float`, optional
 676            Norm of `newTensor`
 677
 678        Notes
 679        -------
 680        **The following attributes are modified as a result of this function:**
 681        - `ttObject.ttCores`
 682        - `ttObject.ttRanks`
 683        - `ttObject.compressionRatio`
 684        .. _TT-ICE:
 685            https://arxiv.org/abs/2211.12487
 686        """
 687        if tenNorm is None:
 688            tenNorm = np.linalg.norm(newTensor)
 689        if epsilon is None:
 690            epsilon = self.ttEpsilon
 691        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 692        newTensorSize = len(newTensor.shape) - 1
 693        newTensor = newTensor.reshape(self.reshapedShape[0], -1)
 694        Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1)
 695        Ri = newTensor - Ui @ (Ui.T @ newTensor)
 696        for coreIdx in range(0, len(self.ttCores) - 2):
 697            URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
 698            self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape(
 699                self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1
 700            )
 701            self.ttCores[coreIdx + 1] = np.concatenate(
 702                (
 703                    self.ttCores[coreIdx + 1],
 704                    np.zeros(
 705                        (
 706                            URi.shape[-1],
 707                            self.reshapedShape[coreIdx + 1],
 708                            self.ttRanks[coreIdx + 2],
 709                        )
 710                    ),
 711                ),
 712                axis=0,
 713            )
 714            Ui = self.ttCores[coreIdx].reshape(
 715                self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
 716            )
 717            newTensor = (Ui.T @ newTensor).reshape(
 718                np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1
 719            )
 720            Ui = self.ttCores[coreIdx + 1].reshape(
 721                self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1
 722            )
 723            Ri = newTensor - Ui @ (Ui.T @ newTensor)
 724        coreIdx = len(self.ttCores) - 2
 725        URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
 726        self.ttCores[coreIdx] = np.hstack((Ui, URi))
 727        self.ttCores[coreIdx + 1] = np.concatenate(
 728            (
 729                self.ttCores[coreIdx + 1],
 730                np.zeros(
 731                    (
 732                        URi.shape[-1],
 733                        self.ttCores[coreIdx + 1].shape[1],
 734                        self.ttRanks[coreIdx + 2],
 735                    )
 736                ),
 737            ),
 738            axis=0,
 739        )
 740        newTensor = self.ttCores[coreIdx].T @ newTensor
 741        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 742            self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1
 743        )
 744        coreIdx += 1
 745        Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1)
 746        self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape(
 747            self.ttCores[coreIdx].shape[0], -1, 1
 748        )
 749        self.updateRanks()
 750        return None
 751
 752    def ttICEstar(
 753        self,
 754        newTensor: np.array,
 755        epsilon: float = None,
 756        tenNorm: float = None,
 757        elementwiseNorm: np.array = None,
 758        elementwiseEpsilon: np.array = None,
 759        heuristicsToUse: list = ["skip", "subselect", "occupancy"],
 760        occupancyThreshold: float = 0.8,
 761        simpleEpsilonUpdate: bool = False,
 762        surrogateThreshold=True,
 763        sampleEpsilon=False,
 764        samplePercent=0.1
 765    ) -> None:
 766        """
 767        `TT-ICE*`_ algorithmn with heuristic performance upgrades.
 768
 769        Given a set of TT-cores, this function provides incremental updates
 770        to the TT-cores to approximate `newTensor` within a relative error
 771        defined in `epsilon`.
 772
 773        Note
 774        ----
 775        This algorithm/function relies on the fact that TT-cores are columnwise
 776        orthonormal in the mode-2 unfolding.
 777
 778        Parameters
 779        ----------
 780        newTensor:obj:`np.array`
 781            New/streamed tensor that will be used to expand the orthonormal bases defined
 782            in TT-cores
 783        epsilon:obj:`float`, optional
 784            Relative error upper bound for approximating `newTensor` after incremental
 785            updates. If not defined, `ttObject.ttEpsilon` is used.
 786        tenNorm:obj:`float`, optional
 787            Norm of `newTensor`.
 788        elementwiseNorm:obj:`np.array`, optional
 789            Individual norms of the observations in `newTensor`.
 790        elementwiseEpsilon:obj:`np.array`, optional
 791            Individual relative projection errors of the observations in `newTensor`.
 792        heuristicsToUse:obj:`list`, optional
 793            List of heuristics to use while updating TT-cores. Currently only accepts
 794            `'skip'`, `'subselect'`, and `'occupancy'`.
 795        occupancyThreshold:obj:`float`, optional
 796            Threshold determining whether to skip updating a single core or not. Not used
 797            if `'occupancy'` is not in `heuristicsToUse`
 798        simpleEpsilonUpdate:obj:`bool`, optional
 799            Uses the simple epsilon update equation. *Warning*: this relies on the
 800            assumption that all observations in `newTensor` have similar norms.
 801
 802        Notes
 803        -------
 804        **The following attributes are modified as a result of this function:**
 805        - `ttObject.ttCores`
 806        - `ttObject.ttRanks`
 807        - `ttObject.compressionRatio`
 808        .. _TT-ICE*:
 809            https://arxiv.org/abs/2211.12487
 810        """
 811        if epsilon is None:
 812            epsilon = self.ttEpsilon
 813        if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1):
 814            warning(
 815                "The streamed tensor has only 1 observation in it. \
 816                    Subselect heuristic will not be useful!!"
 817            )
 818        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 819        updEpsilon = epsilon
 820        newTensorSize = len(newTensor.shape) - 1
 821
 822        if elementwiseEpsilon is None:
 823            elementwiseEpsilon = self.computeRelError(newTensor)
 824        if "skip" in heuristicsToUse:
 825            if surrogateThreshold:
 826                if sampleEpsilon:
 827                    sampleSize=int(newTensor.shape[-1]*samplePercent)
 828                    skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False))
 829                else:
 830                    skipCriterion=np.mean(elementwiseEpsilon)
 831                if  skipCriterion<= epsilon:
 832                    newTensor = self.projectTensor(newTensor)
 833                    self.ttCores[-1] = np.hstack(
 834                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
 835                    ).reshape(self.ttRanks[-2], -1, 1)
 836                    return None
 837            else:
 838                errorNorm=elementwiseEpsilon*elementwiseNorm
 839                errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm)
 840                if errorNorm <= epsilon:
 841                    newTensor = self.projectTensor(newTensor)
 842                    self.ttCores[-1] = np.hstack(
 843                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
 844                    ).reshape(self.ttRanks[-2], -1, 1)
 845                    return None
 846        if tenNorm is None and elementwiseNorm is None:
 847            tenNorm = np.linalg.norm(newTensor)
 848        elif tenNorm is None:
 849            tenNorm = np.linalg.norm(elementwiseNorm)
 850
 851        select = [True] * newTensor.shape[-1]
 852        discard = [False] * newTensor.shape[-1]
 853        if "subselect" in heuristicsToUse:
 854            select = elementwiseEpsilon > epsilon
 855            discard = elementwiseEpsilon <= epsilon
 856            if simpleEpsilonUpdate:
 857                updEpsilon = (
 858                    epsilon * newTensor.shape[-1]
 859                    - np.mean(elementwiseEpsilon[discard]) * discard.sum()
 860                ) / (select.sum())
 861            else:
 862                if elementwiseNorm is None:
 863                    elementwiseNorm = np.linalg.norm(newTensor, axis=0)
 864                    for _ in range(len(self.ttCores) - 1):
 865                        elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
 866                allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2
 867                discardedError = np.sum(
 868                    (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2
 869                )
 870                updEpsilon = np.sqrt(
 871                    (allowedError - discardedError)
 872                    / (np.linalg.norm(elementwiseNorm[select]) ** 2)
 873                )
 874        self.reshapedShape[-1] = np.array(
 875            select
 876        ).sum()  # a little trick for ease of coding
 877
 878        indexString = "["
 879        for _ in range(len(self.reshapedShape)):
 880            # this heuristic assumes that the last dimension is for observations
 881            indexString += ":,"
 882        selectString = indexString + "select]"
 883        selected = eval("newTensor" + selectString)
 884
 885        selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 886        for coreIdx in range(0, len(self.ttCores) - 1):
 887            selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1)
 888            if ("occupancy" in heuristicsToUse) and (
 889                self.coreOccupancy[coreIdx] >= occupancyThreshold
 890            ):
 891                pass
 892            else:
 893                Ui = self.ttCores[coreIdx].reshape(
 894                    self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
 895                )
 896                Ri = selected - Ui @ (Ui.T @ selected)
 897                if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse):
 898                    URi, _, _ = deltaSVD(
 899                        Ri, np.linalg.norm(selected), newTensorSize, updEpsilon
 900                    )
 901                else:
 902                    URi, _, _ = deltaSVD(
 903                        Ri,
 904                        np.linalg.norm(elementwiseNorm[select]),
 905                        newTensorSize,
 906                        updEpsilon,
 907                    )
 908                self.ttCores[coreIdx] = np.hstack((Ui, URi))
 909                self.ttCores[coreIdx + 1] = np.concatenate(
 910                    (
 911                        self.ttCores[coreIdx + 1],
 912                        np.zeros(
 913                            (
 914                                URi.shape[-1],
 915                                self.ttCores[coreIdx + 1].shape[1],
 916                                self.ttRanks[coreIdx + 2],
 917                            )
 918                        ),
 919                    ),
 920                    axis=0,
 921                )
 922
 923            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 924                np.prod(self.ttCores[coreIdx].shape[:-1]), -1
 925            )
 926            # #project onto existing core and reshape for next core
 927            selected = (self.ttCores[coreIdx].T @ selected).reshape(
 928                self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1
 929            )
 930            # fold back the previous core
 931            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 932                -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1]
 933            )
 934            # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1)
 935            # #fold back the previous core
 936        self.updateRanks()
 937        coreIdx += 1
 938        # coreIdx=len(self.ttCores), i.e working on the last core
 939        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 940            self.ttCores[coreIdx].shape[0], -1
 941        )
 942        self.ttCores[coreIdx] = np.hstack(
 943            (self.ttCores[coreIdx], self.projectTensor(newTensor))
 944        ).reshape(self.ttCores[coreIdx].shape[0], -1, 1)
 945        return None
 946
 947    def ttRound(self, norm, epsilon=0) -> None:
 948        """
 949        Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_.
 950
 951        This is the TT-rounding algorithm described in `TTSVD`_ paper. This
 952        function first reorthogonalizes the cores from last to first using
 953        QR decomposition and then recompresses the reorthogonalized TT-cores
 954        from first to last using `TTSVD`_ algorithm.
 955
 956        Parameters
 957        ----------
 958        norm:obj:`float`
 959            Norm of the TT-approximation. If there is no information on the
 960            uncompressed tensor, `ttNorm` function can be used.
 961        epsilon:obj:`float`, optional
 962            Relative error upper bound for the recompression step. Note that
 963            this relative error upper bound is not with respect to the original
 964            tensor but to the `compressed` tensor. Set to 0 by default.
 965
 966        Notes
 967        -----
 968        **The following attributes are modified as a result of this function:**
 969        - `ttObject.ttCores`
 970        - `ttObject.ttRanks`
 971        - `ttObject.compressionRatio`
 972
 973        .. _Oseledets 2011:
 974            https://epubs.siam.org/doi/epdf/10.1137/090752286
 975            TTSVD:
 976            https://epubs.siam.org/doi/epdf/10.1137/090752286
 977        """
 978        d = [core.shape[1] for core in self.ttCores]
 979        for coreIdx in np.arange(len(self.ttCores))[::-1]:
 980            currCore = self.ttCores[coreIdx]
 981            # Compute mode 1 unfolding of the tt-core
 982            currCore = currCore.reshape(currCore.shape[0], -1)
 983            # Using the transpose results in a row orthogonal Q matrix !!!
 984            q, r = np.linalg.qr(currCore.T)
 985            q, r = q.T, r.T
 986            self.ttCores[coreIdx] = q
 987            # Contract previous tt-core and R matrix
 988            self.ttCores[coreIdx - 1] = np.tensordot(
 989                self.ttCores[coreIdx - 1], r, axes=(-1, 0)
 990            )
 991            if coreIdx == 1:
 992                break
 993                # pass #TODO: write the case for the last core here.
 994        # Compression of the orthogonalized representation #
 995        ranks = [1]
 996        for coreIdx in range(len(self.ttCores) - 1):
 997            core = self.ttCores[coreIdx]
 998            core = core.reshape(np.prod(core.shape[:-1]), -1)
 999            self.ttCores[coreIdx], sigma, v = deltaSVD(
1000                core, norm, len(self.ttCores), epsilon
1001            )
1002            ranks.append(self.ttCores[coreIdx].shape[-1])
1003            # fold matrices into tt-cores
1004            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
1005                ranks[coreIdx], d[coreIdx], -1
1006            )
1007            self.ttCores[coreIdx + 1] = (
1008                (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1]
1009            ).reshape(ranks[-1] * d[coreIdx + 1], -1)
1010        self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1)
1011        self.ttRanks = [1]
1012        for core in self.ttCores:
1013            self.ttRanks.append(core.shape[-1])
1014        return None
1015
1016    @staticmethod
1017    def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject":
1018        """
1019        Alternative incremental tensor decomposition algorithm used to benchmark.
1020
1021        Note
1022        ----
1023        This method is empirically shown to be inferior to `TT-ICE`_ and
1024        `TT-ICE*`_.
1025
1026        Parameters
1027        ----------
1028        tt1:obj:`ttObject`
1029            Tensor in TT-format.
1030        tt2:obj:`ttObject`
1031            Tensor in TT-format.
1032        rounding:obj:`bool`, optional
1033            Determines if `ttRounding` will be done to the incremented TT-core.
1034            Note that for unbounded streams, rounding is essential. Otherwise
1035            TT-ranks will grow unboundedly.
1036        epsilon:obj:`float`, optional
1037            Relative error upper bound for the recompression step. Note that
1038            this relative error upper bound is not with respect to the original
1039            tensor but to the `compressed` tensor. Set to 0 by default. Not used
1040            if `rounding` is set to `False`.
1041
1042        Notes
1043        -----
1044        **The following attributes are modified as a result of this function:**
1045        - `ttObject.ttCores`
1046        - `ttObject.ttRanks`
1047        - `ttObject.compressionRatio`
1048
1049        .. _TT-ICE:
1050            https://arxiv.org/abs/2211.12487
1051            _TT-ICE*:
1052            https://arxiv.org/abs/2211.12487
1053        """
1054        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1055            if isinstance(tt1, list):
1056                tt1 = ttObject(tt1)
1057            if isinstance(tt2, list):
1058                tt2 = ttObject(tt2)
1059            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1060                raise AttributeError("One of the passed objects is not in TT-format!")
1061        tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1)
1062        tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1)
1063        rank1, numObs1 = tt1.ttCores[-1].shape
1064        rank2, numObs2 = tt2.ttCores[-1].shape
1065        tt1.ttCores[-1] = np.concatenate(
1066            (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1
1067        )
1068        tt2.ttCores[-1] = np.concatenate(
1069            (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1
1070        )
1071        # Addition in TT-format #
1072        for coreIdx in range(len(tt1.ttCores)):
1073            if coreIdx == 0:
1074                tt1.ttCores[coreIdx] = np.concatenate(
1075                    (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)),
1076                    axis=1,
1077                )[None, :, :]
1078            elif coreIdx == len(tt1.ttCores) - 1:
1079                tt1.ttCores[coreIdx] = np.concatenate(
1080                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1081                )[:, :, None]
1082            else:
1083                s11, s12, s13 = tt1.ttCores[coreIdx].shape
1084                s21, s22, s23 = tt2.ttCores[coreIdx].shape
1085                tt1.ttCores[coreIdx] = np.concatenate(
1086                    (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2
1087                )
1088                tt2.ttCores[coreIdx] = np.concatenate(
1089                    (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2
1090                )
1091                tt1.ttCores[coreIdx] = np.concatenate(
1092                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1093                )
1094        tt1.ttRanks = [1]
1095        for core in tt1.ttCores:
1096            tt1.ttRanks.append(core.shape[-1])
1097        if rounding:
1098            tenNorm = ttObject.ttNorm(tt1)
1099            tt1.ttRound(tenNorm, epsilon=epsilon)
1100        return tt1
1101
1102    @staticmethod
1103    def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1104        nDims=len(data.shape)
1105        dims=list(data.shape)
1106        coreSwap=False
1107        if forgettingFactor is None:
1108            forgettingFactor=0.7
1109
1110        if len(ttRanks) != nDims-1:
1111            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1112        
1113        if ttCores is None:
1114            # Initialize TT-cores randomly if no core is given.
1115            ttCores=[]
1116            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1117            for dimIdx in range(1,nDims-1):
1118                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1119            ttCores.append(np.empty((ttRanks[-1],1)))
1120            coreSwap=True
1121        # else:
1122        ttCores_old=ttCores.copy()
1123        if S is None:
1124            # Initialize S matrices as identity matrices if no S is given from previous step.
1125            S=[]
1126            S.append(100*np.eye(ttRanks[0]))
1127            for dimIdx in range(1,nDims-1):
1128                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1129
1130        # Update the last core G6
1131        
1132        Ht_1=coreContraction(ttCores_old[:-1])
1133        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1134        Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5
1135        g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1136        if coreSwap:
1137            ttCores[-1]=g6
1138        else:
1139            ttCores[-1]=np.hstack((ttCores[-1],g6))
1140        recData=(Ht_1@g6).reshape(dims,order='F')
1141        delta=data-recData
1142
1143        # Update G1
1144        deltaunfold=mode_n_unfolding(delta,0)
1145        Bk=coreContraction(ttCores_old[1:-1]+[g6])
1146        Wk=mode_n_unfolding(Bk,0)
1147        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1148        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1149        Vk=np.linalg.pinv(S[0])@Wk
1150        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1151
1152        # Update G2
1153        deltaunfold=mode_n_unfolding(delta,1)
1154        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1155        Bk=coreContraction(ttCores_old[2:-1]+[g6])
1156        Bk=mode_n_unfolding(Bk,0)
1157        Wk=np.kron(Bk,Ak)
1158        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1159        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1160        Vk=np.linalg.pinv(S[1])@Wk
1161        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1162        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1163        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1164
1165        # Update G3
1166        deltaunfold=mode_n_unfolding(delta,2)
1167        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1168        Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1169        Bk=mode_n_unfolding(Bk,0)
1170        Wk=np.kron(Bk,Ak)
1171        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1172        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1173        Vk=np.linalg.pinv(S[2])@Wk
1174        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1175        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1176        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1177
1178        # Update G4
1179        deltaunfold=mode_n_unfolding(delta,3)
1180        Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3)
1181        Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1182        Wk=np.kron(Bk,Ak)
1183        S[3]=forgettingFactor=S[3]+Wk@Wk.T
1184        # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0]
1185        Vk=np.linalg.pinv(S[3])@Wk
1186        ttCores[3]=mode_n_unfolding(ttCores_old[3],1)
1187        ttCores[3]=ttCores[3]+deltaunfold@Vk.T
1188        ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2)
1189
1190        # Update G5
1191        deltaunfold=mode_n_unfolding(delta,4)
1192        Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4)
1193        # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary
1194        Wk=np.kron(g6,Ak)
1195        S[4]=forgettingFactor=S[4]+Wk@Wk.T
1196        # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0]
1197        Vk=np.linalg.pinv(S[4])@Wk
1198        ttCores[4]=mode_n_unfolding(ttCores_old[4],1)
1199        ttCores[4]=ttCores[4]+deltaunfold@Vk.T
1200        ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2)
1201
1202        return S,ttCores
1203
1204    @staticmethod
1205    def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1206        nDims=len(data.shape)
1207        dims=list(data.shape)
1208        coreSwap=False
1209        if forgettingFactor is None:
1210            forgettingFactor=0.7
1211
1212        if len(ttRanks) != nDims-1:
1213            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1214        
1215        if ttCores is None:
1216            # Initialize TT-cores randomly if no core is given.
1217            ttCores=[]
1218            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1219            for dimIdx in range(1,nDims-1):
1220                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1221            ttCores.append(np.empty((ttRanks[-1],1)))
1222            coreSwap=True
1223        # else:
1224        ttCores_old=ttCores.copy()
1225        if S is None:
1226            # Initialize S matrices as identity matrices if no S is given from previous step.
1227            S=[]
1228            S.append(100*np.eye(ttRanks[0]))
1229            for dimIdx in range(1,nDims-1):
1230                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1231
1232        Ht_1=coreContraction(ttCores_old[:-1])
1233        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1234        Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4
1235        g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1236        if coreSwap:
1237            ttCores[-1]=g4
1238        else:
1239            ttCores[-1]=np.hstack((ttCores[-1],g4))
1240        recData=(Ht_1@g4).reshape(dims,order='F')
1241        delta=data-recData
1242
1243        # Update G1
1244        deltaunfold=mode_n_unfolding(delta,0)
1245        # Bk=coreContraction(ttCores_old[1:-1]+[g4])
1246        Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0])
1247        Wk=mode_n_unfolding(Bk,0)
1248        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1249        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1250        try:
1251            Vk=np.linalg.solve(S[0],Wk)
1252        except np.linalg.LinAlgError:
1253            print('Singular S[0] matrix, using Moore-Penrose inverse')
1254            Vk=np.linalg.pinv(S[0])@Wk
1255        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1256
1257        # Update G2
1258        deltaunfold=mode_n_unfolding(delta,1)
1259        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1260        Bk=coreContraction(ttCores_old[2:-1]+[g4])
1261        Bk=mode_n_unfolding(Bk,0)
1262        Wk=np.kron(Bk,Ak)
1263        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1264        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1265        try:
1266            Vk=np.linalg.solve(S[1],Wk)
1267        except np.linalg.LinAlgError:
1268            print('Singular S[1] matrix, using Moore-Penrose inverse')
1269            Vk=np.linalg.pinv(S[1])@Wk
1270        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1271        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1272        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1273
1274        # Update G3
1275        deltaunfold=mode_n_unfolding(delta,2)
1276        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1277        # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary
1278        # Bk=mode_n_unfolding(Bk,0)
1279        Wk=np.kron(g4,Ak)
1280        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1281        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1282        try:
1283            Vk=np.linalg.solve(S[2],Wk)
1284        except np.linalg.LinAlgError:
1285            print('Singular S[2] matrix, using Moore-Penrose inverse')
1286            Vk=np.linalg.pinv(S[2])@Wk
1287        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1288        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1289        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1290
1291        return S,ttCores
class ttObject:
  23class ttObject:
  24    """
  25    Python object for tensors in Tensor-Train format.
  26
  27    This object computes the TT-decomposition of multidimensional arrays using `TTSVD`_,
  28    `TT-ICE`_, and `TT-ICE*`_.
  29    It furthermore contains an inferior method, ITTD, for benchmarking purposes.
  30
  31    This object handles various operations in TT-format such as dot product and norm.
  32    Also provides supporting operations for uncompressed multidimensional arrays such as
  33    reshaping and transpose.
  34    Once the tensor train approximation for a multidimensional array is computed, you can
  35    compute projection of appropriate arrays onto the TT-cores, reconstruct projected
  36    or compressed tensors and compute projection/compression accuracy.
  37
  38    You can also save/load tensors as `.ttc` or `.txt` files.
  39
  40    Attributes
  41    ----------
  42    inputType: type
  43        Type of the input data. This determines how the object is initialized.
  44    originalData:
  45        Original multidimensional input array. Generally will not be stored
  46        after computing
  47        an initial set of TT-cores.
  48    method: :obj:`str`
  49        Method of computing the initial set of TT-cores. Currently only accepts
  50        `'ttsvd'` as input
  51    ttEpsilon: :obj:`float`
  52        Desired relative error upper bound.
  53    ttCores: :obj:`list` of :obj:`numpy.array`
  54        Cores of the TT-decomposition. Stored as a list of numpy arrays.
  55    ttRanks: :obj:`list` of :obj:`int`
  56        TT-ranks of the decomposition. Stored as a list of integers.
  57    nCores: :obj:`int`
  58        Number of TT-cores in the decomposition.
  59    nElements: :obj:`int`
  60        Number of entries present in the current `ttObject`.
  61    originalShape: :obj:`tuple` or :obj:`list`
  62        Original shape of the multidimensional array.
  63    reshapedShape: :obj:`tuple` or :obj:`list`
  64        Shape of the multidimensional array after reshaping. Note that
  65        this attribute is only meaningful before computing a TT-decomposition
  66    indexOrder: :obj:`list` of :obj:`int`
  67        Keeps track of original indices in case of transposing the input array.
  68    .. _TTSVD:
  69        https://epubs.siam.org/doi/epdf/10.1137/090752286
  70        _TT-ICE:
  71        https://arxiv.org/abs/2211.12487
  72        _TT-ICE*:
  73        https://arxiv.org/abs/2211.12487
  74
  75    """
  76
  77    def __init__(
  78        self,
  79        data,
  80        epsilon: float = None,
  81        keepData: bool = False,
  82        samplesAlongLastDimension: bool = True,
  83        method: str = "ttsvd",
  84    ) -> None:
  85        """
  86        Initializes the ttObject.
  87
  88        Parameters
  89        ----------
  90        data: :obj:`numpy.array` or :obj:`list`
  91            Main input to the ttObject. It can either be a multidimensional `numpy array`
  92            or `list of numpy arrays`.
  93            If list of numpy arrays are presented as input, the object will interpret it
  94            as the TT-cores of an existing decomposition.
  95        epsilon: :obj:`float`, optional
  96            The relative error upper bound desired for approximation. Optional for cases
  97            when `data` has type `list`.
  98        keepData: :obj:`bool`, optional
  99            Optional boolean variable to determine if the original array will be kept
 100            after compression. Set to `False` by default.
 101        samplesAlongLastDimension: :obj:`bool`, optional
 102            Boolean variable to ensure if the samples are stacked along the last
 103            dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE.
 104        method: :obj:`str`, optional
 105            Determines the computation method for tensor train decomposition of
 106            the multidimensional array presented as `data`. Set to `'ttsvd'` by default.
 107
 108            Currently the package only has support for ttsvd, additional support such as
 109            `ttcross` might be included in the future.
 110
 111        """
 112        # self.ttRanks=ranks
 113        self.ttCores = None
 114        self.nCores = None
 115        self.nElements = None
 116        self.inputType = type(data)
 117        self.method = method
 118        self.keepOriginal = keepData
 119        self.originalData = data
 120        self.samplesAlongLastDimension = samplesAlongLastDimension
 121        if self.inputType is np.memmap:
 122            self.inputType = np.ndarray
 123
 124        if self.inputType == np.ndarray:
 125            self.ttEpsilon = epsilon
 126            self.originalShape = list(data.shape)
 127            self.reshapedShape = self.originalShape
 128            self.indexOrder = [idx for idx in range(len(self.originalShape))]
 129        elif self.inputType == list:
 130            self.nCores = len(data)
 131            self.ttCores = data
 132            self.reshapedShape = [core.shape[1] for core in self.ttCores]
 133        else:
 134            raise TypeError("Unknown input type!")
 135
 136    @property
 137    def coreOccupancy(self) -> None:
 138        """
 139        :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each
 140        TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*`
 141        algorithm
 142        """
 143        try:
 144            return [
 145                core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1]
 146            ]
 147        except ValueError:
 148            warnings.warn(
 149                "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning
 150            )
 151            return None
 152
 153    @property
 154    def compressionRatio(self) -> float:
 155        """
 156        :obj:`float`: A metric showing how much compression with respect to the
 157        original multidimensional array is achieved.
 158        """
 159        originalNumEl = 1
 160        compressedNumEl = 0
 161        for core in self.ttCores:
 162            originalNumEl *= core.shape[1]
 163            compressedNumEl += np.prod(core.shape)
 164        return originalNumEl / compressedNumEl
 165
 166    def changeShape(self, newShape: tuple or list) -> None:
 167        """
 168        Function to change shape of input tensors and keeping track of the reshaping.
 169        Reshapes `originalData` and saves the final shape in `reshapedShape`
 170
 171        Note
 172        ----
 173        A simple `numpy.reshape` would be sufficient for this purpose but in order to keep
 174        track of the shape changes the `reshapedShape` attribute also needs to be updated
 175        accordingly.
 176
 177        Parameters
 178        ----------
 179        newShape:obj:`tuple` or `list`
 180            New shape of the tensor
 181
 182        Raises
 183        ------
 184        warning
 185            If an attempt is done to modify the shape after computing a TT-decomposition.
 186            This is important since it will cause incompatibilities in other functions
 187            regarding the shape of the uncompressed tensor.
 188
 189        """
 190        if self.ttCores is not None:
 191            warning(
 192                "Warning! You are reshaping the original data after computing a\
 193                TT-decomposition! We will proceed without reshaping self.originalData!!"
 194            )
 195            return None
 196        self.reshapedShape = newShape
 197        self.originalData = np.reshape(self.originalData, self.reshapedShape)
 198        self.reshapedShape = list(self.originalData.shape)
 199        # Changed reshapedShape to a list for the trick in ttICEstar
 200        if self.samplesAlongLastDimension:
 201            self.singleDataShape = self.reshapedShape[:-1]
 202            # This line assumes we keep the last index as the samples index and don't
 203            # interfere with its shape
 204
 205    def computeTranspose(self, newOrder: list) -> None:
 206        """
 207        Transposes the axes of `originalData`.
 208
 209        Similar to `changeShape`, a simple
 210        `numpy.transpose` would be sufficient for this purpose but in order to
 211        keep track of the transposition order `indexOrder` attribute also needs
 212        to be updated accordingly.
 213
 214        Parameters
 215        ----------
 216        newOrder:obj:`list`
 217            New order of the axes.
 218
 219        Raises
 220        ------
 221        ValueError
 222            When the number of transposition axes are not equal to the number of
 223            dimensions of `originalData`.
 224        """
 225        assert self.inputType == np.ndarray and self.ttCores is None
 226        if len(newOrder) != len(self.indexOrder):
 227            raise ValueError(
 228                "size of newOrder and self.indexOrder does not match. \
 229                    Maybe you forgot reshaping?"
 230            )
 231        self.indexOrder = [self.indexOrder[idx] for idx in newOrder]
 232        self.originalData = self.originalData.transpose(newOrder)
 233        self.reshapedShape = list(self.originalData.shape)
 234
 235    def indexMap(self) -> None:
 236        """
 237        Function to map the original indices to the reshaped indices.
 238        Currently not implemented.
 239        """
 240        self.A = 2
 241
 242    def primeReshaping(self) -> None:
 243        """
 244        Function to reshape the first d dimensions to prime factors.
 245        Currently not implemented
 246        """
 247        self.A = 2
 248
 249    def saveData(
 250        self, fileName: str, directory="./", justCores=True, outputType="ttc"
 251    ) -> None:
 252        """
 253        Writes the computed TT-cores to an external file.
 254
 255        Parameters
 256        ----------
 257        fileName:obj:`str`
 258
 259        directory:obj:`str`
 260            Location to save files with respect to the present working directory.
 261        justCores:obj:`bool`
 262            Boolean variable to determine if `originalData` will be discarded
 263            or not while saving.
 264        outputType:obj:`str`
 265            Type of the output file. `ttc` for pickled `ttObject`, `txt` for
 266            individual text files for each TT-core.
 267        """
 268        if justCores:
 269            if outputType == "ttc":
 270                with open(directory + fileName + ".ttc", "wb") as saveFile:
 271                    temp = ttObject(self.ttCores)
 272                    for attribute in vars(self):
 273                        if attribute != "originalData":
 274                            setattr(temp, attribute, eval(f"self.{attribute}"))
 275                    dump(temp, saveFile)
 276            elif outputType == "txt":
 277                for coreIdx, core in enumerate(self.ttCores):
 278                    np.savetxt(
 279                        directory + f"{fileName}_{coreIdx}.txt",
 280                        core.reshape(-1, core.shape[-1]),
 281                        header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}",
 282                        delimiter=" ",
 283                    )
 284            elif outputType == "npy":
 285                for coreIdx, core in enumerate(self.ttCores):
 286                    np.save(
 287                        directory + f"{fileName}_{coreIdx}.npy",
 288                        core,
 289                    )
 290            else:
 291                raise ValueError(f"Output type {outputType} is not supported!")
 292        else:
 293            if outputType == "txt":
 294                raise ValueError(
 295                    ".txt type outputs are only supported for justCores=True!!"
 296                )
 297            if self.method == "ttsvd":
 298                with open(directory + fileName + ".ttc", "wb") as saveFile:
 299                    dump(self, saveFile)
 300            else:
 301                raise ValueError("Unknown Method!")
 302
 303    @staticmethod
 304    def loadData(fileName: str, numCores=None) -> "ttObject":
 305        """
 306        Loads data from a `.ttc`, `.npy`, or `.txt` file
 307
 308
 309        Static method to load TT-cores into a ttObject object.
 310        Note
 311        ----
 312        If data is stored in {coreFile}_{coreIdx}.txt format,
 313        the input fileName should just be coreFile.txt
 314
 315        Parameters
 316        ----------
 317        fileName:obj:`str`
 318            Name of the file that will be loaded.
 319        numCores:obj:`int`
 320            Number of cores that the resulting `ttObject` will have
 321            (Only required when input data format is `.txt`)
 322        """
 323        fileExt = fileName.split(".")[-1]
 324        if fileExt == "ttc":
 325            with open(fileName, "rb") as f:
 326                dataSetObject = load(f)
 327            return dataSetObject
 328        elif fileExt == "txt":
 329            if numCores is None:
 330                raise ValueError("Number of cores are not defined!!")
 331            fileBody = fileName.split(".")[0]
 332            coreList = []
 333            for coreIdx in range(numCores):
 334                with open(f"{fileBody}_{coreIdx}.{fileExt}"):
 335                    coreShape = f.readline()[2:-1]
 336                    coreShape = [int(item) for item in coreShape.split(" ")]
 337                coreList.append(
 338                    np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape]
 339                )
 340            return coreList
 341        elif fileExt == "npy":
 342            if numCores is None:
 343                raise ValueError("Number of cores are not defined!!")
 344            fileBody = fileName.split(".")[0]
 345            coreList = []
 346            for coreIdx in range(numCores):
 347                coreList.append(
 348                    np.load(f"{fileBody}_{coreIdx}.{fileExt}")
 349                )
 350            return ttObject(coreList)
 351        else:
 352            raise ValueError(f"{fileExt} files are not supported!")
 353
 354    @staticmethod
 355    def ttDot(tt1, tt2) -> float():
 356        """
 357        Computes the dot product of two tensors in TT format.
 358
 359        Parameters
 360        ----------
 361        tt1:obj:`ttObject`
 362            Tensor in TT-format
 363        tt2:obj:`ttObject`
 364            Tensor in TT-format
 365
 366        Returns
 367        -------
 368        prod:obj:`float`
 369            Dot product of `tt1` and `tt2`.
 370
 371        Raises
 372        ------
 373        AttributeError
 374            When either one of the input parameters are not `ttObject`s.
 375
 376        """
 377        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
 378            if isinstance(tt1, list):
 379                tt1 = ttObject(tt1)
 380            if isinstance(tt2, list):
 381                tt2 = ttObject(tt2)
 382            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
 383                raise AttributeError("One of the passed objects is not in TT-format!")
 384        prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :])
 385        for i1 in range(1, tt1.ttCores[0].shape[1]):
 386            prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :])
 387        for coreIdx in range(1, len(tt1.ttCores)):
 388            p = []
 389            for ik in range(tt1.ttCores[coreIdx].shape[1]):
 390                p.append(
 391                    prod
 392                    @ (
 393                        np.kron(
 394                            tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :]
 395                        )
 396                    )
 397                )
 398            prod = np.sum(p, axis=0)
 399        return prod.item()
 400
 401    @staticmethod
 402    def ttNorm(tt1) -> float:
 403        """
 404        Computes norm of a tensor in TT-format.
 405
 406        Parameters
 407        ----------
 408        tt1:obj:`ttObject`
 409            Tensor in TT-format
 410
 411        Returns
 412        -------
 413        norm:obj:`float`
 414            Norm of `tt1`.
 415
 416        Raises
 417        ------
 418        AttributeError
 419            When the input parameter is not a `ttObject`.
 420        """
 421        if not isinstance(tt1, ttObject):
 422            if isinstance(tt1, list):
 423                tt1 = ttObject(tt1)
 424            else:
 425                raise AttributeError("Passed object is not in TT-format!")
 426        try:
 427            norm = ttObject.ttDot(tt1, tt1)
 428            norm = np.sqrt(norm)
 429        except MemoryError:
 430            norm = np.linalg.norm(tt1.ttCores[-1])
 431        return norm
 432
 433    def projectTensor(self, newData: np.array, upTo=None) -> np.array:
 434        """
 435        Projects tensors onto basis spanned by TT-cores.
 436
 437        Given a tensor with appropriate dimensions, this function leverages
 438        the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are
 439        column-orthonormal in the mode-2 unfolding.
 440
 441        Note
 442        ----
 443        This function will not yield correct results if the TT-cores are
 444        not comprised of orthogonal vectors.
 445
 446        Parameters
 447        ----------
 448        newData:obj:`np.aray`
 449            Tensor to be projected
 450        upTo:obj:`int`, optional
 451            Index that the projection will be terminated. If an integer is
 452            passed as this parameter, `newTensor` will be projected up to
 453            (not including) the core that has index `upTo`. Assumes 1-based
 454            indexing.
 455
 456         .. _TTSVD:
 457            https://epubs.siam.org/doi/epdf/10.1137/090752286
 458            _TT-ICE:
 459            https://arxiv.org/abs/2211.12487
 460            _TT-ICE*:
 461            https://arxiv.org/abs/2211.12487
 462        """
 463        for coreIdx, core in enumerate(self.ttCores):
 464            if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo:
 465                break
 466            newData = (
 467                core.reshape(np.prod(core.shape[:2]), -1).transpose()
 468            ) @ newData.reshape(
 469                self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1
 470            )
 471        return newData
 472
 473    def reconstruct(self, projectedData, upTo=None):
 474        """
 475        Reconstructs tensors using TT-cores.
 476
 477        Assumes that `projectedData` is a slice from the last
 478        TT-core.
 479        While reconstructing any projected tensor from  `projectTensor`,
 480        this function leverages the fact that TT-cores obtained through
 481        `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2
 482        unfolding.
 483
 484        Note
 485        ----
 486        This function might not yield correct results for projected tensors
 487        if the TT-cores are not comprised of orthogonal vectors.
 488
 489        Parameters
 490        ----------
 491        projectedData:obj:`np.aray`
 492            A tensor slice (or alternatively an array)
 493        upTo:obj:`int`, optional
 494            Index that the reconstruction will be terminated. If an integer is
 495            passed as this parameter, `projectedData` will be projected up to
 496            (not including) the core that has index `upTo`. Assumes 1-based
 497            indexing.
 498        .. _TTSVD:
 499            https://epubs.siam.org/doi/epdf/10.1137/090752286
 500            _TT-ICE:
 501            https://arxiv.org/abs/2211.12487
 502
 503        """
 504        if upTo is None:
 505            upTo = len(self.ttCores) - 1  # Write the core index in 1-indexed form!!
 506        for core in self.ttCores[:upTo][::-1]:
 507            projectedData = np.tensordot(core, projectedData, axes=(-1, 0))
 508        return projectedData
 509
 510    def updateRanks(self) -> None:
 511        """
 512        Updates the ranks of the `ttObject` after incremental updates.
 513        """
 514        self.ttRanks = [1]
 515        for core in self.ttCores:
 516            self.ttRanks.append(core.shape[-1])
 517        return None
 518
 519    def computeRelError(self, newTensor: np.array, useExact=True) -> np.array:
 520        """
 521        Computes relative error by projecting data onto TT-cores.
 522
 523        This function computes the error induced by projecting `data` onto the TT-cores
 524        of `ttObject`. The last index of `newTensor` is assumed to be the number of
 525        individual observations.
 526
 527        Note
 528        ----
 529        - In order to compute the projection onto the TT-cores, the dimensions of `data`
 530        should match that of `ttObject`.
 531        - If a single observation will be passed as `newTensor`, an additional
 532        index/dimension should be introduced either through reshaping or [:,None]
 533
 534        Parameters
 535        ----------
 536        newTensor:obj:`np.array`
 537            Tensor for which the projection error is computed
 538        useExact:`bool`
 539            Boolean flag determining whether a projection error will be
 540            returned for the entire batch or each observation in the batch.
 541
 542        Returns
 543        -------
 544        relError:obj:`np.array`
 545            Array of relative errors
 546        """
 547        elementwiseNorm = np.linalg.norm(newTensor, axis=0)
 548        for _ in range(len(newTensor.shape) - 2):
 549            elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
 550        projectedData = self.projectTensor(newTensor)
 551        reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape)
 552        difference = newTensor - reconstructedData
 553        differenceNorm = np.linalg.norm(difference, axis=0)
 554        for _ in range(len(difference.shape) - 2):
 555            differenceNorm = np.linalg.norm(differenceNorm, axis=0)
 556        
 557        if useExact:
 558            relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
 559        else:
 560            relError = differenceNorm / elementwiseNorm
 561        return relError
 562
 563    def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None:
 564        """
 565        Function to compute relative error by reconstructing data from slices
 566        of TT-cores.
 567
 568        Parameters
 569        ----------
 570        data:obj:`np.array`
 571            Tensor for which the reconstruction error is computed
 572        start:`int`
 573            Index for the start of the batch in the compressed last core
 574        finish:`int`
 575            Index for the end of the batch in the compressed last core 
 576        useExact:`bool`
 577            Boolean flag determining whether a reconstruction error will be
 578            returned for the entire batch or each observation in the batch.
 579
 580        Returns
 581        -------
 582        recError:obj:`float` or `np.array`
 583            Reconstruction error of the data from cores. Returns an array
 584            of reconstruction errors for each observation in the batch if
 585            `useExact` is `False`.
 586        """
 587        if start is None:
 588            start = self.ttCores[-1].shape[1]-1
 589        if finish is None:
 590            finish = start+1
 591        rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape)
 592        elementwiseNorm=np.linalg.norm(data,axis=0)
 593        for idx in range(len(data.shape)-2):
 594            elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0)
 595        difference=data-rec
 596        differenceNorm=np.linalg.norm(difference,axis=0)
 597        for idx in range(len(difference.shape)-2):
 598            differenceNorm=np.linalg.norm(differenceNorm,axis=0)
 599
 600        if useExact:
 601            recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
 602        else:
 603            recError=differenceNorm/elementwiseNorm
 604
 605        return recError
 606
 607    def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores":
 608        """
 609        Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm.
 610
 611        Currently only supports `ttsvd` as method. In the future additional formats may
 612        be covered.
 613
 614        Parameters
 615        ----------
 616        norm:obj:`float`, optional
 617            Norm of the tensor to be compressed
 618        dtype:obj:`type`, optional
 619            Desired data type for the compression. Intended to allow lower precision
 620            if needed.
 621
 622        Raises
 623        ------
 624        ValueError
 625            When `method` is not one of the admissible methods.
 626
 627
 628        The following attributes are modified as a result of this function:
 629        -------
 630        - `ttObject.ttCores`
 631        - `ttObject.ttRanks`
 632        - `ttObject.compressionRatio`
 633
 634        .. _TTSVD:
 635            https://epubs.siam.org/doi/epdf/10.1137/090752286
 636        """
 637        if norm is None:
 638            norm = np.linalg.norm(self.originalData)
 639        if self.method == "ttsvd":
 640            startTime = time.time()
 641            self.ttRanks, self.ttCores = ttsvd(
 642                self.originalData, norm, self.ttEpsilon, dtype=dtype
 643            )
 644            self.compressionTime = time.time() - startTime
 645            self.nCores = len(self.ttCores)
 646            self.nElements = 0
 647            for cores in self.ttCores:
 648                self.nElements += np.prod(cores.shape)
 649            if not self.keepOriginal:
 650                self.originalData = None
 651            return None
 652        else:
 653            raise ValueError("Method unknown. Please select a valid method!")
 654
 655    def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None:
 656        """
 657        `TT-ICE`_ algorithmn without any heuristic upgrades.
 658
 659        Given a set of TT-cores, this function provides incremental updates
 660        to the TT-cores to approximate `newTensor` within a relative error
 661        defined in `epsilon`
 662
 663        Note
 664        ----
 665        This algorithm/function relies on the fact that TT-cores are columnwise
 666        orthonormal in the mode-2 unfolding.
 667
 668        Parameters
 669        ----------
 670        newTensor:obj:`np.array`
 671            New/streamed tensor that will be used to expand the orthonormal bases defined
 672            in TT-cores
 673        epsilon:obj:`float`, optional
 674            Relative error upper bound for approximating `newTensor` after incremental
 675            updates. If not defined, `ttObject.ttEpsilon` is used.
 676        tenNorm:obj:`float`, optional
 677            Norm of `newTensor`
 678
 679        Notes
 680        -------
 681        **The following attributes are modified as a result of this function:**
 682        - `ttObject.ttCores`
 683        - `ttObject.ttRanks`
 684        - `ttObject.compressionRatio`
 685        .. _TT-ICE:
 686            https://arxiv.org/abs/2211.12487
 687        """
 688        if tenNorm is None:
 689            tenNorm = np.linalg.norm(newTensor)
 690        if epsilon is None:
 691            epsilon = self.ttEpsilon
 692        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 693        newTensorSize = len(newTensor.shape) - 1
 694        newTensor = newTensor.reshape(self.reshapedShape[0], -1)
 695        Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1)
 696        Ri = newTensor - Ui @ (Ui.T @ newTensor)
 697        for coreIdx in range(0, len(self.ttCores) - 2):
 698            URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
 699            self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape(
 700                self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1
 701            )
 702            self.ttCores[coreIdx + 1] = np.concatenate(
 703                (
 704                    self.ttCores[coreIdx + 1],
 705                    np.zeros(
 706                        (
 707                            URi.shape[-1],
 708                            self.reshapedShape[coreIdx + 1],
 709                            self.ttRanks[coreIdx + 2],
 710                        )
 711                    ),
 712                ),
 713                axis=0,
 714            )
 715            Ui = self.ttCores[coreIdx].reshape(
 716                self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
 717            )
 718            newTensor = (Ui.T @ newTensor).reshape(
 719                np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1
 720            )
 721            Ui = self.ttCores[coreIdx + 1].reshape(
 722                self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1
 723            )
 724            Ri = newTensor - Ui @ (Ui.T @ newTensor)
 725        coreIdx = len(self.ttCores) - 2
 726        URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
 727        self.ttCores[coreIdx] = np.hstack((Ui, URi))
 728        self.ttCores[coreIdx + 1] = np.concatenate(
 729            (
 730                self.ttCores[coreIdx + 1],
 731                np.zeros(
 732                    (
 733                        URi.shape[-1],
 734                        self.ttCores[coreIdx + 1].shape[1],
 735                        self.ttRanks[coreIdx + 2],
 736                    )
 737                ),
 738            ),
 739            axis=0,
 740        )
 741        newTensor = self.ttCores[coreIdx].T @ newTensor
 742        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 743            self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1
 744        )
 745        coreIdx += 1
 746        Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1)
 747        self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape(
 748            self.ttCores[coreIdx].shape[0], -1, 1
 749        )
 750        self.updateRanks()
 751        return None
 752
 753    def ttICEstar(
 754        self,
 755        newTensor: np.array,
 756        epsilon: float = None,
 757        tenNorm: float = None,
 758        elementwiseNorm: np.array = None,
 759        elementwiseEpsilon: np.array = None,
 760        heuristicsToUse: list = ["skip", "subselect", "occupancy"],
 761        occupancyThreshold: float = 0.8,
 762        simpleEpsilonUpdate: bool = False,
 763        surrogateThreshold=True,
 764        sampleEpsilon=False,
 765        samplePercent=0.1
 766    ) -> None:
 767        """
 768        `TT-ICE*`_ algorithmn with heuristic performance upgrades.
 769
 770        Given a set of TT-cores, this function provides incremental updates
 771        to the TT-cores to approximate `newTensor` within a relative error
 772        defined in `epsilon`.
 773
 774        Note
 775        ----
 776        This algorithm/function relies on the fact that TT-cores are columnwise
 777        orthonormal in the mode-2 unfolding.
 778
 779        Parameters
 780        ----------
 781        newTensor:obj:`np.array`
 782            New/streamed tensor that will be used to expand the orthonormal bases defined
 783            in TT-cores
 784        epsilon:obj:`float`, optional
 785            Relative error upper bound for approximating `newTensor` after incremental
 786            updates. If not defined, `ttObject.ttEpsilon` is used.
 787        tenNorm:obj:`float`, optional
 788            Norm of `newTensor`.
 789        elementwiseNorm:obj:`np.array`, optional
 790            Individual norms of the observations in `newTensor`.
 791        elementwiseEpsilon:obj:`np.array`, optional
 792            Individual relative projection errors of the observations in `newTensor`.
 793        heuristicsToUse:obj:`list`, optional
 794            List of heuristics to use while updating TT-cores. Currently only accepts
 795            `'skip'`, `'subselect'`, and `'occupancy'`.
 796        occupancyThreshold:obj:`float`, optional
 797            Threshold determining whether to skip updating a single core or not. Not used
 798            if `'occupancy'` is not in `heuristicsToUse`
 799        simpleEpsilonUpdate:obj:`bool`, optional
 800            Uses the simple epsilon update equation. *Warning*: this relies on the
 801            assumption that all observations in `newTensor` have similar norms.
 802
 803        Notes
 804        -------
 805        **The following attributes are modified as a result of this function:**
 806        - `ttObject.ttCores`
 807        - `ttObject.ttRanks`
 808        - `ttObject.compressionRatio`
 809        .. _TT-ICE*:
 810            https://arxiv.org/abs/2211.12487
 811        """
 812        if epsilon is None:
 813            epsilon = self.ttEpsilon
 814        if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1):
 815            warning(
 816                "The streamed tensor has only 1 observation in it. \
 817                    Subselect heuristic will not be useful!!"
 818            )
 819        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 820        updEpsilon = epsilon
 821        newTensorSize = len(newTensor.shape) - 1
 822
 823        if elementwiseEpsilon is None:
 824            elementwiseEpsilon = self.computeRelError(newTensor)
 825        if "skip" in heuristicsToUse:
 826            if surrogateThreshold:
 827                if sampleEpsilon:
 828                    sampleSize=int(newTensor.shape[-1]*samplePercent)
 829                    skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False))
 830                else:
 831                    skipCriterion=np.mean(elementwiseEpsilon)
 832                if  skipCriterion<= epsilon:
 833                    newTensor = self.projectTensor(newTensor)
 834                    self.ttCores[-1] = np.hstack(
 835                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
 836                    ).reshape(self.ttRanks[-2], -1, 1)
 837                    return None
 838            else:
 839                errorNorm=elementwiseEpsilon*elementwiseNorm
 840                errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm)
 841                if errorNorm <= epsilon:
 842                    newTensor = self.projectTensor(newTensor)
 843                    self.ttCores[-1] = np.hstack(
 844                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
 845                    ).reshape(self.ttRanks[-2], -1, 1)
 846                    return None
 847        if tenNorm is None and elementwiseNorm is None:
 848            tenNorm = np.linalg.norm(newTensor)
 849        elif tenNorm is None:
 850            tenNorm = np.linalg.norm(elementwiseNorm)
 851
 852        select = [True] * newTensor.shape[-1]
 853        discard = [False] * newTensor.shape[-1]
 854        if "subselect" in heuristicsToUse:
 855            select = elementwiseEpsilon > epsilon
 856            discard = elementwiseEpsilon <= epsilon
 857            if simpleEpsilonUpdate:
 858                updEpsilon = (
 859                    epsilon * newTensor.shape[-1]
 860                    - np.mean(elementwiseEpsilon[discard]) * discard.sum()
 861                ) / (select.sum())
 862            else:
 863                if elementwiseNorm is None:
 864                    elementwiseNorm = np.linalg.norm(newTensor, axis=0)
 865                    for _ in range(len(self.ttCores) - 1):
 866                        elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
 867                allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2
 868                discardedError = np.sum(
 869                    (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2
 870                )
 871                updEpsilon = np.sqrt(
 872                    (allowedError - discardedError)
 873                    / (np.linalg.norm(elementwiseNorm[select]) ** 2)
 874                )
 875        self.reshapedShape[-1] = np.array(
 876            select
 877        ).sum()  # a little trick for ease of coding
 878
 879        indexString = "["
 880        for _ in range(len(self.reshapedShape)):
 881            # this heuristic assumes that the last dimension is for observations
 882            indexString += ":,"
 883        selectString = indexString + "select]"
 884        selected = eval("newTensor" + selectString)
 885
 886        selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
 887        for coreIdx in range(0, len(self.ttCores) - 1):
 888            selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1)
 889            if ("occupancy" in heuristicsToUse) and (
 890                self.coreOccupancy[coreIdx] >= occupancyThreshold
 891            ):
 892                pass
 893            else:
 894                Ui = self.ttCores[coreIdx].reshape(
 895                    self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
 896                )
 897                Ri = selected - Ui @ (Ui.T @ selected)
 898                if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse):
 899                    URi, _, _ = deltaSVD(
 900                        Ri, np.linalg.norm(selected), newTensorSize, updEpsilon
 901                    )
 902                else:
 903                    URi, _, _ = deltaSVD(
 904                        Ri,
 905                        np.linalg.norm(elementwiseNorm[select]),
 906                        newTensorSize,
 907                        updEpsilon,
 908                    )
 909                self.ttCores[coreIdx] = np.hstack((Ui, URi))
 910                self.ttCores[coreIdx + 1] = np.concatenate(
 911                    (
 912                        self.ttCores[coreIdx + 1],
 913                        np.zeros(
 914                            (
 915                                URi.shape[-1],
 916                                self.ttCores[coreIdx + 1].shape[1],
 917                                self.ttRanks[coreIdx + 2],
 918                            )
 919                        ),
 920                    ),
 921                    axis=0,
 922                )
 923
 924            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 925                np.prod(self.ttCores[coreIdx].shape[:-1]), -1
 926            )
 927            # #project onto existing core and reshape for next core
 928            selected = (self.ttCores[coreIdx].T @ selected).reshape(
 929                self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1
 930            )
 931            # fold back the previous core
 932            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 933                -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1]
 934            )
 935            # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1)
 936            # #fold back the previous core
 937        self.updateRanks()
 938        coreIdx += 1
 939        # coreIdx=len(self.ttCores), i.e working on the last core
 940        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
 941            self.ttCores[coreIdx].shape[0], -1
 942        )
 943        self.ttCores[coreIdx] = np.hstack(
 944            (self.ttCores[coreIdx], self.projectTensor(newTensor))
 945        ).reshape(self.ttCores[coreIdx].shape[0], -1, 1)
 946        return None
 947
 948    def ttRound(self, norm, epsilon=0) -> None:
 949        """
 950        Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_.
 951
 952        This is the TT-rounding algorithm described in `TTSVD`_ paper. This
 953        function first reorthogonalizes the cores from last to first using
 954        QR decomposition and then recompresses the reorthogonalized TT-cores
 955        from first to last using `TTSVD`_ algorithm.
 956
 957        Parameters
 958        ----------
 959        norm:obj:`float`
 960            Norm of the TT-approximation. If there is no information on the
 961            uncompressed tensor, `ttNorm` function can be used.
 962        epsilon:obj:`float`, optional
 963            Relative error upper bound for the recompression step. Note that
 964            this relative error upper bound is not with respect to the original
 965            tensor but to the `compressed` tensor. Set to 0 by default.
 966
 967        Notes
 968        -----
 969        **The following attributes are modified as a result of this function:**
 970        - `ttObject.ttCores`
 971        - `ttObject.ttRanks`
 972        - `ttObject.compressionRatio`
 973
 974        .. _Oseledets 2011:
 975            https://epubs.siam.org/doi/epdf/10.1137/090752286
 976            TTSVD:
 977            https://epubs.siam.org/doi/epdf/10.1137/090752286
 978        """
 979        d = [core.shape[1] for core in self.ttCores]
 980        for coreIdx in np.arange(len(self.ttCores))[::-1]:
 981            currCore = self.ttCores[coreIdx]
 982            # Compute mode 1 unfolding of the tt-core
 983            currCore = currCore.reshape(currCore.shape[0], -1)
 984            # Using the transpose results in a row orthogonal Q matrix !!!
 985            q, r = np.linalg.qr(currCore.T)
 986            q, r = q.T, r.T
 987            self.ttCores[coreIdx] = q
 988            # Contract previous tt-core and R matrix
 989            self.ttCores[coreIdx - 1] = np.tensordot(
 990                self.ttCores[coreIdx - 1], r, axes=(-1, 0)
 991            )
 992            if coreIdx == 1:
 993                break
 994                # pass #TODO: write the case for the last core here.
 995        # Compression of the orthogonalized representation #
 996        ranks = [1]
 997        for coreIdx in range(len(self.ttCores) - 1):
 998            core = self.ttCores[coreIdx]
 999            core = core.reshape(np.prod(core.shape[:-1]), -1)
1000            self.ttCores[coreIdx], sigma, v = deltaSVD(
1001                core, norm, len(self.ttCores), epsilon
1002            )
1003            ranks.append(self.ttCores[coreIdx].shape[-1])
1004            # fold matrices into tt-cores
1005            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
1006                ranks[coreIdx], d[coreIdx], -1
1007            )
1008            self.ttCores[coreIdx + 1] = (
1009                (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1]
1010            ).reshape(ranks[-1] * d[coreIdx + 1], -1)
1011        self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1)
1012        self.ttRanks = [1]
1013        for core in self.ttCores:
1014            self.ttRanks.append(core.shape[-1])
1015        return None
1016
1017    @staticmethod
1018    def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject":
1019        """
1020        Alternative incremental tensor decomposition algorithm used to benchmark.
1021
1022        Note
1023        ----
1024        This method is empirically shown to be inferior to `TT-ICE`_ and
1025        `TT-ICE*`_.
1026
1027        Parameters
1028        ----------
1029        tt1:obj:`ttObject`
1030            Tensor in TT-format.
1031        tt2:obj:`ttObject`
1032            Tensor in TT-format.
1033        rounding:obj:`bool`, optional
1034            Determines if `ttRounding` will be done to the incremented TT-core.
1035            Note that for unbounded streams, rounding is essential. Otherwise
1036            TT-ranks will grow unboundedly.
1037        epsilon:obj:`float`, optional
1038            Relative error upper bound for the recompression step. Note that
1039            this relative error upper bound is not with respect to the original
1040            tensor but to the `compressed` tensor. Set to 0 by default. Not used
1041            if `rounding` is set to `False`.
1042
1043        Notes
1044        -----
1045        **The following attributes are modified as a result of this function:**
1046        - `ttObject.ttCores`
1047        - `ttObject.ttRanks`
1048        - `ttObject.compressionRatio`
1049
1050        .. _TT-ICE:
1051            https://arxiv.org/abs/2211.12487
1052            _TT-ICE*:
1053            https://arxiv.org/abs/2211.12487
1054        """
1055        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1056            if isinstance(tt1, list):
1057                tt1 = ttObject(tt1)
1058            if isinstance(tt2, list):
1059                tt2 = ttObject(tt2)
1060            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1061                raise AttributeError("One of the passed objects is not in TT-format!")
1062        tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1)
1063        tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1)
1064        rank1, numObs1 = tt1.ttCores[-1].shape
1065        rank2, numObs2 = tt2.ttCores[-1].shape
1066        tt1.ttCores[-1] = np.concatenate(
1067            (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1
1068        )
1069        tt2.ttCores[-1] = np.concatenate(
1070            (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1
1071        )
1072        # Addition in TT-format #
1073        for coreIdx in range(len(tt1.ttCores)):
1074            if coreIdx == 0:
1075                tt1.ttCores[coreIdx] = np.concatenate(
1076                    (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)),
1077                    axis=1,
1078                )[None, :, :]
1079            elif coreIdx == len(tt1.ttCores) - 1:
1080                tt1.ttCores[coreIdx] = np.concatenate(
1081                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1082                )[:, :, None]
1083            else:
1084                s11, s12, s13 = tt1.ttCores[coreIdx].shape
1085                s21, s22, s23 = tt2.ttCores[coreIdx].shape
1086                tt1.ttCores[coreIdx] = np.concatenate(
1087                    (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2
1088                )
1089                tt2.ttCores[coreIdx] = np.concatenate(
1090                    (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2
1091                )
1092                tt1.ttCores[coreIdx] = np.concatenate(
1093                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1094                )
1095        tt1.ttRanks = [1]
1096        for core in tt1.ttCores:
1097            tt1.ttRanks.append(core.shape[-1])
1098        if rounding:
1099            tenNorm = ttObject.ttNorm(tt1)
1100            tt1.ttRound(tenNorm, epsilon=epsilon)
1101        return tt1
1102
1103    @staticmethod
1104    def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1105        nDims=len(data.shape)
1106        dims=list(data.shape)
1107        coreSwap=False
1108        if forgettingFactor is None:
1109            forgettingFactor=0.7
1110
1111        if len(ttRanks) != nDims-1:
1112            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1113        
1114        if ttCores is None:
1115            # Initialize TT-cores randomly if no core is given.
1116            ttCores=[]
1117            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1118            for dimIdx in range(1,nDims-1):
1119                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1120            ttCores.append(np.empty((ttRanks[-1],1)))
1121            coreSwap=True
1122        # else:
1123        ttCores_old=ttCores.copy()
1124        if S is None:
1125            # Initialize S matrices as identity matrices if no S is given from previous step.
1126            S=[]
1127            S.append(100*np.eye(ttRanks[0]))
1128            for dimIdx in range(1,nDims-1):
1129                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1130
1131        # Update the last core G6
1132        
1133        Ht_1=coreContraction(ttCores_old[:-1])
1134        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1135        Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5
1136        g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1137        if coreSwap:
1138            ttCores[-1]=g6
1139        else:
1140            ttCores[-1]=np.hstack((ttCores[-1],g6))
1141        recData=(Ht_1@g6).reshape(dims,order='F')
1142        delta=data-recData
1143
1144        # Update G1
1145        deltaunfold=mode_n_unfolding(delta,0)
1146        Bk=coreContraction(ttCores_old[1:-1]+[g6])
1147        Wk=mode_n_unfolding(Bk,0)
1148        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1149        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1150        Vk=np.linalg.pinv(S[0])@Wk
1151        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1152
1153        # Update G2
1154        deltaunfold=mode_n_unfolding(delta,1)
1155        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1156        Bk=coreContraction(ttCores_old[2:-1]+[g6])
1157        Bk=mode_n_unfolding(Bk,0)
1158        Wk=np.kron(Bk,Ak)
1159        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1160        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1161        Vk=np.linalg.pinv(S[1])@Wk
1162        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1163        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1164        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1165
1166        # Update G3
1167        deltaunfold=mode_n_unfolding(delta,2)
1168        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1169        Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1170        Bk=mode_n_unfolding(Bk,0)
1171        Wk=np.kron(Bk,Ak)
1172        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1173        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1174        Vk=np.linalg.pinv(S[2])@Wk
1175        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1176        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1177        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1178
1179        # Update G4
1180        deltaunfold=mode_n_unfolding(delta,3)
1181        Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3)
1182        Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1183        Wk=np.kron(Bk,Ak)
1184        S[3]=forgettingFactor=S[3]+Wk@Wk.T
1185        # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0]
1186        Vk=np.linalg.pinv(S[3])@Wk
1187        ttCores[3]=mode_n_unfolding(ttCores_old[3],1)
1188        ttCores[3]=ttCores[3]+deltaunfold@Vk.T
1189        ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2)
1190
1191        # Update G5
1192        deltaunfold=mode_n_unfolding(delta,4)
1193        Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4)
1194        # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary
1195        Wk=np.kron(g6,Ak)
1196        S[4]=forgettingFactor=S[4]+Wk@Wk.T
1197        # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0]
1198        Vk=np.linalg.pinv(S[4])@Wk
1199        ttCores[4]=mode_n_unfolding(ttCores_old[4],1)
1200        ttCores[4]=ttCores[4]+deltaunfold@Vk.T
1201        ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2)
1202
1203        return S,ttCores
1204
1205    @staticmethod
1206    def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1207        nDims=len(data.shape)
1208        dims=list(data.shape)
1209        coreSwap=False
1210        if forgettingFactor is None:
1211            forgettingFactor=0.7
1212
1213        if len(ttRanks) != nDims-1:
1214            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1215        
1216        if ttCores is None:
1217            # Initialize TT-cores randomly if no core is given.
1218            ttCores=[]
1219            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1220            for dimIdx in range(1,nDims-1):
1221                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1222            ttCores.append(np.empty((ttRanks[-1],1)))
1223            coreSwap=True
1224        # else:
1225        ttCores_old=ttCores.copy()
1226        if S is None:
1227            # Initialize S matrices as identity matrices if no S is given from previous step.
1228            S=[]
1229            S.append(100*np.eye(ttRanks[0]))
1230            for dimIdx in range(1,nDims-1):
1231                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1232
1233        Ht_1=coreContraction(ttCores_old[:-1])
1234        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1235        Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4
1236        g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1237        if coreSwap:
1238            ttCores[-1]=g4
1239        else:
1240            ttCores[-1]=np.hstack((ttCores[-1],g4))
1241        recData=(Ht_1@g4).reshape(dims,order='F')
1242        delta=data-recData
1243
1244        # Update G1
1245        deltaunfold=mode_n_unfolding(delta,0)
1246        # Bk=coreContraction(ttCores_old[1:-1]+[g4])
1247        Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0])
1248        Wk=mode_n_unfolding(Bk,0)
1249        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1250        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1251        try:
1252            Vk=np.linalg.solve(S[0],Wk)
1253        except np.linalg.LinAlgError:
1254            print('Singular S[0] matrix, using Moore-Penrose inverse')
1255            Vk=np.linalg.pinv(S[0])@Wk
1256        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1257
1258        # Update G2
1259        deltaunfold=mode_n_unfolding(delta,1)
1260        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1261        Bk=coreContraction(ttCores_old[2:-1]+[g4])
1262        Bk=mode_n_unfolding(Bk,0)
1263        Wk=np.kron(Bk,Ak)
1264        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1265        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1266        try:
1267            Vk=np.linalg.solve(S[1],Wk)
1268        except np.linalg.LinAlgError:
1269            print('Singular S[1] matrix, using Moore-Penrose inverse')
1270            Vk=np.linalg.pinv(S[1])@Wk
1271        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1272        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1273        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1274
1275        # Update G3
1276        deltaunfold=mode_n_unfolding(delta,2)
1277        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1278        # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary
1279        # Bk=mode_n_unfolding(Bk,0)
1280        Wk=np.kron(g4,Ak)
1281        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1282        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1283        try:
1284            Vk=np.linalg.solve(S[2],Wk)
1285        except np.linalg.LinAlgError:
1286            print('Singular S[2] matrix, using Moore-Penrose inverse')
1287            Vk=np.linalg.pinv(S[2])@Wk
1288        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1289        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1290        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1291
1292        return S,ttCores

Python object for tensors in Tensor-Train format.

This object computes the TT-decomposition of multidimensional arrays using TTSVD, TT-ICE, and TT-ICE*. It furthermore contains an inferior method, ITTD, for benchmarking purposes.

This object handles various operations in TT-format such as dot product and norm. Also provides supporting operations for uncompressed multidimensional arrays such as reshaping and transpose. Once the tensor train approximation for a multidimensional array is computed, you can compute projection of appropriate arrays onto the TT-cores, reconstruct projected or compressed tensors and compute projection/compression accuracy.

You can also save/load tensors as .ttc or .txt files.

Attributes
  • inputType (type): Type of the input data. This determines how the object is initialized.
  • originalData:: Original multidimensional input array. Generally will not be stored after computing an initial set of TT-cores.
  • method (str): Method of computing the initial set of TT-cores. Currently only accepts 'ttsvd' as input
  • ttEpsilon (float): Desired relative error upper bound.
  • ttCores (list of numpy.array): Cores of the TT-decomposition. Stored as a list of numpy arrays.
  • ttRanks (list of int): TT-ranks of the decomposition. Stored as a list of integers.
  • nCores (int): Number of TT-cores in the decomposition.
  • nElements (int): Number of entries present in the current ttObject.
  • originalShape (tuple or list): Original shape of the multidimensional array.
  • reshapedShape (tuple or list): Shape of the multidimensional array after reshaping. Note that this attribute is only meaningful before computing a TT-decomposition
  • indexOrder (list of int): Keeps track of original indices in case of transposing the input array.
ttObject( data, epsilon: float = None, keepData: bool = False, samplesAlongLastDimension: bool = True, method: str = 'ttsvd')
 77    def __init__(
 78        self,
 79        data,
 80        epsilon: float = None,
 81        keepData: bool = False,
 82        samplesAlongLastDimension: bool = True,
 83        method: str = "ttsvd",
 84    ) -> None:
 85        """
 86        Initializes the ttObject.
 87
 88        Parameters
 89        ----------
 90        data: :obj:`numpy.array` or :obj:`list`
 91            Main input to the ttObject. It can either be a multidimensional `numpy array`
 92            or `list of numpy arrays`.
 93            If list of numpy arrays are presented as input, the object will interpret it
 94            as the TT-cores of an existing decomposition.
 95        epsilon: :obj:`float`, optional
 96            The relative error upper bound desired for approximation. Optional for cases
 97            when `data` has type `list`.
 98        keepData: :obj:`bool`, optional
 99            Optional boolean variable to determine if the original array will be kept
100            after compression. Set to `False` by default.
101        samplesAlongLastDimension: :obj:`bool`, optional
102            Boolean variable to ensure if the samples are stacked along the last
103            dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE.
104        method: :obj:`str`, optional
105            Determines the computation method for tensor train decomposition of
106            the multidimensional array presented as `data`. Set to `'ttsvd'` by default.
107
108            Currently the package only has support for ttsvd, additional support such as
109            `ttcross` might be included in the future.
110
111        """
112        # self.ttRanks=ranks
113        self.ttCores = None
114        self.nCores = None
115        self.nElements = None
116        self.inputType = type(data)
117        self.method = method
118        self.keepOriginal = keepData
119        self.originalData = data
120        self.samplesAlongLastDimension = samplesAlongLastDimension
121        if self.inputType is np.memmap:
122            self.inputType = np.ndarray
123
124        if self.inputType == np.ndarray:
125            self.ttEpsilon = epsilon
126            self.originalShape = list(data.shape)
127            self.reshapedShape = self.originalShape
128            self.indexOrder = [idx for idx in range(len(self.originalShape))]
129        elif self.inputType == list:
130            self.nCores = len(data)
131            self.ttCores = data
132            self.reshapedShape = [core.shape[1] for core in self.ttCores]
133        else:
134            raise TypeError("Unknown input type!")

Initializes the ttObject.

Parameters
  • data (numpy.array or list): Main input to the ttObject. It can either be a multidimensional numpy array or list of numpy arrays. If list of numpy arrays are presented as input, the object will interpret it as the TT-cores of an existing decomposition.
  • epsilon (float, optional): The relative error upper bound desired for approximation. Optional for cases when data has type list.
  • keepData (bool, optional): Optional boolean variable to determine if the original array will be kept after compression. Set to False by default.
  • samplesAlongLastDimension (bool, optional): Boolean variable to ensure if the samples are stacked along the last dimension. Assumed to be True since it is one of the assumptions in TT-ICE.
  • method (str, optional): Determines the computation method for tensor train decomposition of the multidimensional array presented as data. Set to 'ttsvd' by default.

    Currently the package only has support for ttsvd, additional support such as ttcross might be included in the future.

ttCores
nCores
nElements
inputType
method
keepOriginal
originalData
samplesAlongLastDimension
coreOccupancy: None
136    @property
137    def coreOccupancy(self) -> None:
138        """
139        :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each
140        TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*`
141        algorithm
142        """
143        try:
144            return [
145                core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1]
146            ]
147        except ValueError:
148            warnings.warn(
149                "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning
150            )
151            return None

list of float: A metric showing the relative rank of each TT-core. This metric is used for a heuristic enhancement tool in TT-ICE* algorithm

compressionRatio: float
153    @property
154    def compressionRatio(self) -> float:
155        """
156        :obj:`float`: A metric showing how much compression with respect to the
157        original multidimensional array is achieved.
158        """
159        originalNumEl = 1
160        compressedNumEl = 0
161        for core in self.ttCores:
162            originalNumEl *= core.shape[1]
163            compressedNumEl += np.prod(core.shape)
164        return originalNumEl / compressedNumEl

float: A metric showing how much compression with respect to the original multidimensional array is achieved.

def changeShape(self, newShape: tuple) -> None:
166    def changeShape(self, newShape: tuple or list) -> None:
167        """
168        Function to change shape of input tensors and keeping track of the reshaping.
169        Reshapes `originalData` and saves the final shape in `reshapedShape`
170
171        Note
172        ----
173        A simple `numpy.reshape` would be sufficient for this purpose but in order to keep
174        track of the shape changes the `reshapedShape` attribute also needs to be updated
175        accordingly.
176
177        Parameters
178        ----------
179        newShape:obj:`tuple` or `list`
180            New shape of the tensor
181
182        Raises
183        ------
184        warning
185            If an attempt is done to modify the shape after computing a TT-decomposition.
186            This is important since it will cause incompatibilities in other functions
187            regarding the shape of the uncompressed tensor.
188
189        """
190        if self.ttCores is not None:
191            warning(
192                "Warning! You are reshaping the original data after computing a\
193                TT-decomposition! We will proceed without reshaping self.originalData!!"
194            )
195            return None
196        self.reshapedShape = newShape
197        self.originalData = np.reshape(self.originalData, self.reshapedShape)
198        self.reshapedShape = list(self.originalData.shape)
199        # Changed reshapedShape to a list for the trick in ttICEstar
200        if self.samplesAlongLastDimension:
201            self.singleDataShape = self.reshapedShape[:-1]
202            # This line assumes we keep the last index as the samples index and don't
203            # interfere with its shape

Function to change shape of input tensors and keeping track of the reshaping. Reshapes originalData and saves the final shape in reshapedShape

Note

A simple numpy.reshape would be sufficient for this purpose but in order to keep track of the shape changes the reshapedShape attribute also needs to be updated accordingly.

Parameters
  • newShapetuple or list: New shape of the tensor
Raises
  • warning: If an attempt is done to modify the shape after computing a TT-decomposition. This is important since it will cause incompatibilities in other functions regarding the shape of the uncompressed tensor.
def computeTranspose(self, newOrder: list) -> None:
205    def computeTranspose(self, newOrder: list) -> None:
206        """
207        Transposes the axes of `originalData`.
208
209        Similar to `changeShape`, a simple
210        `numpy.transpose` would be sufficient for this purpose but in order to
211        keep track of the transposition order `indexOrder` attribute also needs
212        to be updated accordingly.
213
214        Parameters
215        ----------
216        newOrder:obj:`list`
217            New order of the axes.
218
219        Raises
220        ------
221        ValueError
222            When the number of transposition axes are not equal to the number of
223            dimensions of `originalData`.
224        """
225        assert self.inputType == np.ndarray and self.ttCores is None
226        if len(newOrder) != len(self.indexOrder):
227            raise ValueError(
228                "size of newOrder and self.indexOrder does not match. \
229                    Maybe you forgot reshaping?"
230            )
231        self.indexOrder = [self.indexOrder[idx] for idx in newOrder]
232        self.originalData = self.originalData.transpose(newOrder)
233        self.reshapedShape = list(self.originalData.shape)

Transposes the axes of originalData.

Similar to changeShape, a simple numpy.transpose would be sufficient for this purpose but in order to keep track of the transposition order indexOrder attribute also needs to be updated accordingly.

Parameters
  • newOrderlist: New order of the axes.
Raises
  • ValueError: When the number of transposition axes are not equal to the number of dimensions of originalData.
def indexMap(self) -> None:
235    def indexMap(self) -> None:
236        """
237        Function to map the original indices to the reshaped indices.
238        Currently not implemented.
239        """
240        self.A = 2

Function to map the original indices to the reshaped indices. Currently not implemented.

def primeReshaping(self) -> None:
242    def primeReshaping(self) -> None:
243        """
244        Function to reshape the first d dimensions to prime factors.
245        Currently not implemented
246        """
247        self.A = 2

Function to reshape the first d dimensions to prime factors. Currently not implemented

def saveData( self, fileName: str, directory='./', justCores=True, outputType='ttc') -> None:
249    def saveData(
250        self, fileName: str, directory="./", justCores=True, outputType="ttc"
251    ) -> None:
252        """
253        Writes the computed TT-cores to an external file.
254
255        Parameters
256        ----------
257        fileName:obj:`str`
258
259        directory:obj:`str`
260            Location to save files with respect to the present working directory.
261        justCores:obj:`bool`
262            Boolean variable to determine if `originalData` will be discarded
263            or not while saving.
264        outputType:obj:`str`
265            Type of the output file. `ttc` for pickled `ttObject`, `txt` for
266            individual text files for each TT-core.
267        """
268        if justCores:
269            if outputType == "ttc":
270                with open(directory + fileName + ".ttc", "wb") as saveFile:
271                    temp = ttObject(self.ttCores)
272                    for attribute in vars(self):
273                        if attribute != "originalData":
274                            setattr(temp, attribute, eval(f"self.{attribute}"))
275                    dump(temp, saveFile)
276            elif outputType == "txt":
277                for coreIdx, core in enumerate(self.ttCores):
278                    np.savetxt(
279                        directory + f"{fileName}_{coreIdx}.txt",
280                        core.reshape(-1, core.shape[-1]),
281                        header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}",
282                        delimiter=" ",
283                    )
284            elif outputType == "npy":
285                for coreIdx, core in enumerate(self.ttCores):
286                    np.save(
287                        directory + f"{fileName}_{coreIdx}.npy",
288                        core,
289                    )
290            else:
291                raise ValueError(f"Output type {outputType} is not supported!")
292        else:
293            if outputType == "txt":
294                raise ValueError(
295                    ".txt type outputs are only supported for justCores=True!!"
296                )
297            if self.method == "ttsvd":
298                with open(directory + fileName + ".ttc", "wb") as saveFile:
299                    dump(self, saveFile)
300            else:
301                raise ValueError("Unknown Method!")

Writes the computed TT-cores to an external file.

Parameters
  • fileNamestr
  • directorystr: Location to save files with respect to the present working directory.
  • justCoresbool: Boolean variable to determine if originalData will be discarded or not while saving.
  • outputTypestr: Type of the output file. ttc for pickled ttObject, txt for individual text files for each TT-core.
@staticmethod
def loadData(fileName: str, numCores=None) -> ttObject:
303    @staticmethod
304    def loadData(fileName: str, numCores=None) -> "ttObject":
305        """
306        Loads data from a `.ttc`, `.npy`, or `.txt` file
307
308
309        Static method to load TT-cores into a ttObject object.
310        Note
311        ----
312        If data is stored in {coreFile}_{coreIdx}.txt format,
313        the input fileName should just be coreFile.txt
314
315        Parameters
316        ----------
317        fileName:obj:`str`
318            Name of the file that will be loaded.
319        numCores:obj:`int`
320            Number of cores that the resulting `ttObject` will have
321            (Only required when input data format is `.txt`)
322        """
323        fileExt = fileName.split(".")[-1]
324        if fileExt == "ttc":
325            with open(fileName, "rb") as f:
326                dataSetObject = load(f)
327            return dataSetObject
328        elif fileExt == "txt":
329            if numCores is None:
330                raise ValueError("Number of cores are not defined!!")
331            fileBody = fileName.split(".")[0]
332            coreList = []
333            for coreIdx in range(numCores):
334                with open(f"{fileBody}_{coreIdx}.{fileExt}"):
335                    coreShape = f.readline()[2:-1]
336                    coreShape = [int(item) for item in coreShape.split(" ")]
337                coreList.append(
338                    np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape]
339                )
340            return coreList
341        elif fileExt == "npy":
342            if numCores is None:
343                raise ValueError("Number of cores are not defined!!")
344            fileBody = fileName.split(".")[0]
345            coreList = []
346            for coreIdx in range(numCores):
347                coreList.append(
348                    np.load(f"{fileBody}_{coreIdx}.{fileExt}")
349                )
350            return ttObject(coreList)
351        else:
352            raise ValueError(f"{fileExt} files are not supported!")

Loads data from a .ttc, .npy, or .txt file

Static method to load TT-cores into a ttObject object.

Note

If data is stored in {coreFile}_{coreIdx}.txt format, the input fileName should just be coreFile.txt

Parameters
  • fileNamestr: Name of the file that will be loaded.
  • numCoresint: Number of cores that the resulting ttObject will have (Only required when input data format is .txt)
@staticmethod
def ttDot(tt1, tt2) -> 0.0:
354    @staticmethod
355    def ttDot(tt1, tt2) -> float():
356        """
357        Computes the dot product of two tensors in TT format.
358
359        Parameters
360        ----------
361        tt1:obj:`ttObject`
362            Tensor in TT-format
363        tt2:obj:`ttObject`
364            Tensor in TT-format
365
366        Returns
367        -------
368        prod:obj:`float`
369            Dot product of `tt1` and `tt2`.
370
371        Raises
372        ------
373        AttributeError
374            When either one of the input parameters are not `ttObject`s.
375
376        """
377        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
378            if isinstance(tt1, list):
379                tt1 = ttObject(tt1)
380            if isinstance(tt2, list):
381                tt2 = ttObject(tt2)
382            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
383                raise AttributeError("One of the passed objects is not in TT-format!")
384        prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :])
385        for i1 in range(1, tt1.ttCores[0].shape[1]):
386            prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :])
387        for coreIdx in range(1, len(tt1.ttCores)):
388            p = []
389            for ik in range(tt1.ttCores[coreIdx].shape[1]):
390                p.append(
391                    prod
392                    @ (
393                        np.kron(
394                            tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :]
395                        )
396                    )
397                )
398            prod = np.sum(p, axis=0)
399        return prod.item()

Computes the dot product of two tensors in TT format.

Parameters
Returns
  • prodfloat: Dot product of tt1 and tt2.
Raises
  • AttributeError: When either one of the input parameters are not ttObjects.
@staticmethod
def ttNorm(tt1) -> float:
401    @staticmethod
402    def ttNorm(tt1) -> float:
403        """
404        Computes norm of a tensor in TT-format.
405
406        Parameters
407        ----------
408        tt1:obj:`ttObject`
409            Tensor in TT-format
410
411        Returns
412        -------
413        norm:obj:`float`
414            Norm of `tt1`.
415
416        Raises
417        ------
418        AttributeError
419            When the input parameter is not a `ttObject`.
420        """
421        if not isinstance(tt1, ttObject):
422            if isinstance(tt1, list):
423                tt1 = ttObject(tt1)
424            else:
425                raise AttributeError("Passed object is not in TT-format!")
426        try:
427            norm = ttObject.ttDot(tt1, tt1)
428            norm = np.sqrt(norm)
429        except MemoryError:
430            norm = np.linalg.norm(tt1.ttCores[-1])
431        return norm

Computes norm of a tensor in TT-format.

Parameters
Returns
  • normfloat: Norm of tt1.
Raises
  • AttributeError: When the input parameter is not a ttObject.
def projectTensor( self, newData: <built-in function array>, upTo=None) -> <built-in function array>:
433    def projectTensor(self, newData: np.array, upTo=None) -> np.array:
434        """
435        Projects tensors onto basis spanned by TT-cores.
436
437        Given a tensor with appropriate dimensions, this function leverages
438        the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are
439        column-orthonormal in the mode-2 unfolding.
440
441        Note
442        ----
443        This function will not yield correct results if the TT-cores are
444        not comprised of orthogonal vectors.
445
446        Parameters
447        ----------
448        newData:obj:`np.aray`
449            Tensor to be projected
450        upTo:obj:`int`, optional
451            Index that the projection will be terminated. If an integer is
452            passed as this parameter, `newTensor` will be projected up to
453            (not including) the core that has index `upTo`. Assumes 1-based
454            indexing.
455
456         .. _TTSVD:
457            https://epubs.siam.org/doi/epdf/10.1137/090752286
458            _TT-ICE:
459            https://arxiv.org/abs/2211.12487
460            _TT-ICE*:
461            https://arxiv.org/abs/2211.12487
462        """
463        for coreIdx, core in enumerate(self.ttCores):
464            if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo:
465                break
466            newData = (
467                core.reshape(np.prod(core.shape[:2]), -1).transpose()
468            ) @ newData.reshape(
469                self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1
470            )
471        return newData

Projects tensors onto basis spanned by TT-cores.

Given a tensor with appropriate dimensions, this function leverages the fact that TT-cores obtained through TTSVD and TT-ICE are column-orthonormal in the mode-2 unfolding.

Note

This function will not yield correct results if the TT-cores are not comprised of orthogonal vectors.

Parameters
  • newDatanp.aray: Tensor to be projected
  • upToint, optional: Index that the projection will be terminated. If an integer is passed as this parameter, newTensor will be projected up to (not including) the core that has index upTo. Assumes 1-based indexing.
def reconstruct(self, projectedData, upTo=None):
473    def reconstruct(self, projectedData, upTo=None):
474        """
475        Reconstructs tensors using TT-cores.
476
477        Assumes that `projectedData` is a slice from the last
478        TT-core.
479        While reconstructing any projected tensor from  `projectTensor`,
480        this function leverages the fact that TT-cores obtained through
481        `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2
482        unfolding.
483
484        Note
485        ----
486        This function might not yield correct results for projected tensors
487        if the TT-cores are not comprised of orthogonal vectors.
488
489        Parameters
490        ----------
491        projectedData:obj:`np.aray`
492            A tensor slice (or alternatively an array)
493        upTo:obj:`int`, optional
494            Index that the reconstruction will be terminated. If an integer is
495            passed as this parameter, `projectedData` will be projected up to
496            (not including) the core that has index `upTo`. Assumes 1-based
497            indexing.
498        .. _TTSVD:
499            https://epubs.siam.org/doi/epdf/10.1137/090752286
500            _TT-ICE:
501            https://arxiv.org/abs/2211.12487
502
503        """
504        if upTo is None:
505            upTo = len(self.ttCores) - 1  # Write the core index in 1-indexed form!!
506        for core in self.ttCores[:upTo][::-1]:
507            projectedData = np.tensordot(core, projectedData, axes=(-1, 0))
508        return projectedData

Reconstructs tensors using TT-cores.

Assumes that projectedData is a slice from the last TT-core. While reconstructing any projected tensor from projectTensor, this function leverages the fact that TT-cores obtained through TTSVD and TT-ICE are column-orthonormal in the mode-2 unfolding.

Note

This function might not yield correct results for projected tensors if the TT-cores are not comprised of orthogonal vectors.

Parameters
  • projectedDatanp.aray: A tensor slice (or alternatively an array)
  • upToint, optional: Index that the reconstruction will be terminated. If an integer is passed as this parameter, projectedData will be projected up to (not including) the core that has index upTo. Assumes 1-based indexing.
def updateRanks(self) -> None:
510    def updateRanks(self) -> None:
511        """
512        Updates the ranks of the `ttObject` after incremental updates.
513        """
514        self.ttRanks = [1]
515        for core in self.ttCores:
516            self.ttRanks.append(core.shape[-1])
517        return None

Updates the ranks of the ttObject after incremental updates.

def computeRelError( self, newTensor: <built-in function array>, useExact=True) -> <built-in function array>:
519    def computeRelError(self, newTensor: np.array, useExact=True) -> np.array:
520        """
521        Computes relative error by projecting data onto TT-cores.
522
523        This function computes the error induced by projecting `data` onto the TT-cores
524        of `ttObject`. The last index of `newTensor` is assumed to be the number of
525        individual observations.
526
527        Note
528        ----
529        - In order to compute the projection onto the TT-cores, the dimensions of `data`
530        should match that of `ttObject`.
531        - If a single observation will be passed as `newTensor`, an additional
532        index/dimension should be introduced either through reshaping or [:,None]
533
534        Parameters
535        ----------
536        newTensor:obj:`np.array`
537            Tensor for which the projection error is computed
538        useExact:`bool`
539            Boolean flag determining whether a projection error will be
540            returned for the entire batch or each observation in the batch.
541
542        Returns
543        -------
544        relError:obj:`np.array`
545            Array of relative errors
546        """
547        elementwiseNorm = np.linalg.norm(newTensor, axis=0)
548        for _ in range(len(newTensor.shape) - 2):
549            elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
550        projectedData = self.projectTensor(newTensor)
551        reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape)
552        difference = newTensor - reconstructedData
553        differenceNorm = np.linalg.norm(difference, axis=0)
554        for _ in range(len(difference.shape) - 2):
555            differenceNorm = np.linalg.norm(differenceNorm, axis=0)
556        
557        if useExact:
558            relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
559        else:
560            relError = differenceNorm / elementwiseNorm
561        return relError

Computes relative error by projecting data onto TT-cores.

This function computes the error induced by projecting data onto the TT-cores of ttObject. The last index of newTensor is assumed to be the number of individual observations.

Note
  • In order to compute the projection onto the TT-cores, the dimensions of data should match that of ttObject.
  • If a single observation will be passed as newTensor, an additional index/dimension should be introduced either through reshaping or [:,None]
Parameters
  • newTensornp.array: Tensor for which the projection error is computed
  • useExact (bool): Boolean flag determining whether a projection error will be returned for the entire batch or each observation in the batch.
Returns
  • relErrornp.array: Array of relative errors
def computeRecError( self, data: <built-in function array>, start=None, finish=None, useExact=True) -> None:
563    def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None:
564        """
565        Function to compute relative error by reconstructing data from slices
566        of TT-cores.
567
568        Parameters
569        ----------
570        data:obj:`np.array`
571            Tensor for which the reconstruction error is computed
572        start:`int`
573            Index for the start of the batch in the compressed last core
574        finish:`int`
575            Index for the end of the batch in the compressed last core 
576        useExact:`bool`
577            Boolean flag determining whether a reconstruction error will be
578            returned for the entire batch or each observation in the batch.
579
580        Returns
581        -------
582        recError:obj:`float` or `np.array`
583            Reconstruction error of the data from cores. Returns an array
584            of reconstruction errors for each observation in the batch if
585            `useExact` is `False`.
586        """
587        if start is None:
588            start = self.ttCores[-1].shape[1]-1
589        if finish is None:
590            finish = start+1
591        rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape)
592        elementwiseNorm=np.linalg.norm(data,axis=0)
593        for idx in range(len(data.shape)-2):
594            elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0)
595        difference=data-rec
596        differenceNorm=np.linalg.norm(difference,axis=0)
597        for idx in range(len(difference.shape)-2):
598            differenceNorm=np.linalg.norm(differenceNorm,axis=0)
599
600        if useExact:
601            recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm)
602        else:
603            recError=differenceNorm/elementwiseNorm
604
605        return recError

Function to compute relative error by reconstructing data from slices of TT-cores.

Parameters
  • datanp.array: Tensor for which the reconstruction error is computed
  • start (int): Index for the start of the batch in the compressed last core
  • finish (int): Index for the end of the batch in the compressed last core
  • useExact (bool): Boolean flag determining whether a reconstruction error will be returned for the entire batch or each observation in the batch.
Returns
  • recErrorfloat or np.array: Reconstruction error of the data from cores. Returns an array of reconstruction errors for each observation in the batch if useExact is False.
def ttDecomp(self, norm=None, dtype=<class 'numpy.float32'>) -> 'ttObject.ttCores':
607    def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores":
608        """
609        Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm.
610
611        Currently only supports `ttsvd` as method. In the future additional formats may
612        be covered.
613
614        Parameters
615        ----------
616        norm:obj:`float`, optional
617            Norm of the tensor to be compressed
618        dtype:obj:`type`, optional
619            Desired data type for the compression. Intended to allow lower precision
620            if needed.
621
622        Raises
623        ------
624        ValueError
625            When `method` is not one of the admissible methods.
626
627
628        The following attributes are modified as a result of this function:
629        -------
630        - `ttObject.ttCores`
631        - `ttObject.ttRanks`
632        - `ttObject.compressionRatio`
633
634        .. _TTSVD:
635            https://epubs.siam.org/doi/epdf/10.1137/090752286
636        """
637        if norm is None:
638            norm = np.linalg.norm(self.originalData)
639        if self.method == "ttsvd":
640            startTime = time.time()
641            self.ttRanks, self.ttCores = ttsvd(
642                self.originalData, norm, self.ttEpsilon, dtype=dtype
643            )
644            self.compressionTime = time.time() - startTime
645            self.nCores = len(self.ttCores)
646            self.nElements = 0
647            for cores in self.ttCores:
648                self.nElements += np.prod(cores.shape)
649            if not self.keepOriginal:
650                self.originalData = None
651            return None
652        else:
653            raise ValueError("Method unknown. Please select a valid method!")

Computes TT-decomposition of a multidimensional array using TTSVD algorithm.

Currently only supports ttsvd as method. In the future additional formats may be covered.

Parameters
  • normfloat, optional: Norm of the tensor to be compressed
  • dtypetype, optional: Desired data type for the compression. Intended to allow lower precision if needed.
Raises
def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None:
655    def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None:
656        """
657        `TT-ICE`_ algorithmn without any heuristic upgrades.
658
659        Given a set of TT-cores, this function provides incremental updates
660        to the TT-cores to approximate `newTensor` within a relative error
661        defined in `epsilon`
662
663        Note
664        ----
665        This algorithm/function relies on the fact that TT-cores are columnwise
666        orthonormal in the mode-2 unfolding.
667
668        Parameters
669        ----------
670        newTensor:obj:`np.array`
671            New/streamed tensor that will be used to expand the orthonormal bases defined
672            in TT-cores
673        epsilon:obj:`float`, optional
674            Relative error upper bound for approximating `newTensor` after incremental
675            updates. If not defined, `ttObject.ttEpsilon` is used.
676        tenNorm:obj:`float`, optional
677            Norm of `newTensor`
678
679        Notes
680        -------
681        **The following attributes are modified as a result of this function:**
682        - `ttObject.ttCores`
683        - `ttObject.ttRanks`
684        - `ttObject.compressionRatio`
685        .. _TT-ICE:
686            https://arxiv.org/abs/2211.12487
687        """
688        if tenNorm is None:
689            tenNorm = np.linalg.norm(newTensor)
690        if epsilon is None:
691            epsilon = self.ttEpsilon
692        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
693        newTensorSize = len(newTensor.shape) - 1
694        newTensor = newTensor.reshape(self.reshapedShape[0], -1)
695        Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1)
696        Ri = newTensor - Ui @ (Ui.T @ newTensor)
697        for coreIdx in range(0, len(self.ttCores) - 2):
698            URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
699            self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape(
700                self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1
701            )
702            self.ttCores[coreIdx + 1] = np.concatenate(
703                (
704                    self.ttCores[coreIdx + 1],
705                    np.zeros(
706                        (
707                            URi.shape[-1],
708                            self.reshapedShape[coreIdx + 1],
709                            self.ttRanks[coreIdx + 2],
710                        )
711                    ),
712                ),
713                axis=0,
714            )
715            Ui = self.ttCores[coreIdx].reshape(
716                self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
717            )
718            newTensor = (Ui.T @ newTensor).reshape(
719                np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1
720            )
721            Ui = self.ttCores[coreIdx + 1].reshape(
722                self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1
723            )
724            Ri = newTensor - Ui @ (Ui.T @ newTensor)
725        coreIdx = len(self.ttCores) - 2
726        URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon)
727        self.ttCores[coreIdx] = np.hstack((Ui, URi))
728        self.ttCores[coreIdx + 1] = np.concatenate(
729            (
730                self.ttCores[coreIdx + 1],
731                np.zeros(
732                    (
733                        URi.shape[-1],
734                        self.ttCores[coreIdx + 1].shape[1],
735                        self.ttRanks[coreIdx + 2],
736                    )
737                ),
738            ),
739            axis=0,
740        )
741        newTensor = self.ttCores[coreIdx].T @ newTensor
742        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
743            self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1
744        )
745        coreIdx += 1
746        Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1)
747        self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape(
748            self.ttCores[coreIdx].shape[0], -1, 1
749        )
750        self.updateRanks()
751        return None

TT-ICE algorithmn without any heuristic upgrades.

Given a set of TT-cores, this function provides incremental updates to the TT-cores to approximate newTensor within a relative error defined in epsilon

Note

This algorithm/function relies on the fact that TT-cores are columnwise orthonormal in the mode-2 unfolding.

Parameters
  • newTensornp.array: New/streamed tensor that will be used to expand the orthonormal bases defined in TT-cores
  • epsilonfloat, optional: Relative error upper bound for approximating newTensor after incremental updates. If not defined, ttObject.ttEpsilon is used.
  • tenNormfloat, optional: Norm of newTensor
Notes

The following attributes are modified as a result of this function:

def ttICEstar( self, newTensor: <built-in function array>, epsilon: float = None, tenNorm: float = None, elementwiseNorm: <built-in function array> = None, elementwiseEpsilon: <built-in function array> = None, heuristicsToUse: list = ['skip', 'subselect', 'occupancy'], occupancyThreshold: float = 0.8, simpleEpsilonUpdate: bool = False, surrogateThreshold=True, sampleEpsilon=False, samplePercent=0.1) -> None:
753    def ttICEstar(
754        self,
755        newTensor: np.array,
756        epsilon: float = None,
757        tenNorm: float = None,
758        elementwiseNorm: np.array = None,
759        elementwiseEpsilon: np.array = None,
760        heuristicsToUse: list = ["skip", "subselect", "occupancy"],
761        occupancyThreshold: float = 0.8,
762        simpleEpsilonUpdate: bool = False,
763        surrogateThreshold=True,
764        sampleEpsilon=False,
765        samplePercent=0.1
766    ) -> None:
767        """
768        `TT-ICE*`_ algorithmn with heuristic performance upgrades.
769
770        Given a set of TT-cores, this function provides incremental updates
771        to the TT-cores to approximate `newTensor` within a relative error
772        defined in `epsilon`.
773
774        Note
775        ----
776        This algorithm/function relies on the fact that TT-cores are columnwise
777        orthonormal in the mode-2 unfolding.
778
779        Parameters
780        ----------
781        newTensor:obj:`np.array`
782            New/streamed tensor that will be used to expand the orthonormal bases defined
783            in TT-cores
784        epsilon:obj:`float`, optional
785            Relative error upper bound for approximating `newTensor` after incremental
786            updates. If not defined, `ttObject.ttEpsilon` is used.
787        tenNorm:obj:`float`, optional
788            Norm of `newTensor`.
789        elementwiseNorm:obj:`np.array`, optional
790            Individual norms of the observations in `newTensor`.
791        elementwiseEpsilon:obj:`np.array`, optional
792            Individual relative projection errors of the observations in `newTensor`.
793        heuristicsToUse:obj:`list`, optional
794            List of heuristics to use while updating TT-cores. Currently only accepts
795            `'skip'`, `'subselect'`, and `'occupancy'`.
796        occupancyThreshold:obj:`float`, optional
797            Threshold determining whether to skip updating a single core or not. Not used
798            if `'occupancy'` is not in `heuristicsToUse`
799        simpleEpsilonUpdate:obj:`bool`, optional
800            Uses the simple epsilon update equation. *Warning*: this relies on the
801            assumption that all observations in `newTensor` have similar norms.
802
803        Notes
804        -------
805        **The following attributes are modified as a result of this function:**
806        - `ttObject.ttCores`
807        - `ttObject.ttRanks`
808        - `ttObject.compressionRatio`
809        .. _TT-ICE*:
810            https://arxiv.org/abs/2211.12487
811        """
812        if epsilon is None:
813            epsilon = self.ttEpsilon
814        if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1):
815            warning(
816                "The streamed tensor has only 1 observation in it. \
817                    Subselect heuristic will not be useful!!"
818            )
819        newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
820        updEpsilon = epsilon
821        newTensorSize = len(newTensor.shape) - 1
822
823        if elementwiseEpsilon is None:
824            elementwiseEpsilon = self.computeRelError(newTensor)
825        if "skip" in heuristicsToUse:
826            if surrogateThreshold:
827                if sampleEpsilon:
828                    sampleSize=int(newTensor.shape[-1]*samplePercent)
829                    skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False))
830                else:
831                    skipCriterion=np.mean(elementwiseEpsilon)
832                if  skipCriterion<= epsilon:
833                    newTensor = self.projectTensor(newTensor)
834                    self.ttCores[-1] = np.hstack(
835                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
836                    ).reshape(self.ttRanks[-2], -1, 1)
837                    return None
838            else:
839                errorNorm=elementwiseEpsilon*elementwiseNorm
840                errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm)
841                if errorNorm <= epsilon:
842                    newTensor = self.projectTensor(newTensor)
843                    self.ttCores[-1] = np.hstack(
844                        (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor)
845                    ).reshape(self.ttRanks[-2], -1, 1)
846                    return None
847        if tenNorm is None and elementwiseNorm is None:
848            tenNorm = np.linalg.norm(newTensor)
849        elif tenNorm is None:
850            tenNorm = np.linalg.norm(elementwiseNorm)
851
852        select = [True] * newTensor.shape[-1]
853        discard = [False] * newTensor.shape[-1]
854        if "subselect" in heuristicsToUse:
855            select = elementwiseEpsilon > epsilon
856            discard = elementwiseEpsilon <= epsilon
857            if simpleEpsilonUpdate:
858                updEpsilon = (
859                    epsilon * newTensor.shape[-1]
860                    - np.mean(elementwiseEpsilon[discard]) * discard.sum()
861                ) / (select.sum())
862            else:
863                if elementwiseNorm is None:
864                    elementwiseNorm = np.linalg.norm(newTensor, axis=0)
865                    for _ in range(len(self.ttCores) - 1):
866                        elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0)
867                allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2
868                discardedError = np.sum(
869                    (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2
870                )
871                updEpsilon = np.sqrt(
872                    (allowedError - discardedError)
873                    / (np.linalg.norm(elementwiseNorm[select]) ** 2)
874                )
875        self.reshapedShape[-1] = np.array(
876            select
877        ).sum()  # a little trick for ease of coding
878
879        indexString = "["
880        for _ in range(len(self.reshapedShape)):
881            # this heuristic assumes that the last dimension is for observations
882            indexString += ":,"
883        selectString = indexString + "select]"
884        selected = eval("newTensor" + selectString)
885
886        selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :]
887        for coreIdx in range(0, len(self.ttCores) - 1):
888            selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1)
889            if ("occupancy" in heuristicsToUse) and (
890                self.coreOccupancy[coreIdx] >= occupancyThreshold
891            ):
892                pass
893            else:
894                Ui = self.ttCores[coreIdx].reshape(
895                    self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1
896                )
897                Ri = selected - Ui @ (Ui.T @ selected)
898                if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse):
899                    URi, _, _ = deltaSVD(
900                        Ri, np.linalg.norm(selected), newTensorSize, updEpsilon
901                    )
902                else:
903                    URi, _, _ = deltaSVD(
904                        Ri,
905                        np.linalg.norm(elementwiseNorm[select]),
906                        newTensorSize,
907                        updEpsilon,
908                    )
909                self.ttCores[coreIdx] = np.hstack((Ui, URi))
910                self.ttCores[coreIdx + 1] = np.concatenate(
911                    (
912                        self.ttCores[coreIdx + 1],
913                        np.zeros(
914                            (
915                                URi.shape[-1],
916                                self.ttCores[coreIdx + 1].shape[1],
917                                self.ttRanks[coreIdx + 2],
918                            )
919                        ),
920                    ),
921                    axis=0,
922                )
923
924            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
925                np.prod(self.ttCores[coreIdx].shape[:-1]), -1
926            )
927            # #project onto existing core and reshape for next core
928            selected = (self.ttCores[coreIdx].T @ selected).reshape(
929                self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1
930            )
931            # fold back the previous core
932            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
933                -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1]
934            )
935            # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1)
936            # #fold back the previous core
937        self.updateRanks()
938        coreIdx += 1
939        # coreIdx=len(self.ttCores), i.e working on the last core
940        self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
941            self.ttCores[coreIdx].shape[0], -1
942        )
943        self.ttCores[coreIdx] = np.hstack(
944            (self.ttCores[coreIdx], self.projectTensor(newTensor))
945        ).reshape(self.ttCores[coreIdx].shape[0], -1, 1)
946        return None

TT-ICE* algorithmn with heuristic performance upgrades.

Given a set of TT-cores, this function provides incremental updates to the TT-cores to approximate newTensor within a relative error defined in epsilon.

Note

This algorithm/function relies on the fact that TT-cores are columnwise orthonormal in the mode-2 unfolding.

Parameters
  • newTensornp.array: New/streamed tensor that will be used to expand the orthonormal bases defined in TT-cores
  • epsilonfloat, optional: Relative error upper bound for approximating newTensor after incremental updates. If not defined, ttObject.ttEpsilon is used.
  • tenNormfloat, optional: Norm of newTensor.
  • elementwiseNormnp.array, optional: Individual norms of the observations in newTensor.
  • elementwiseEpsilonnp.array, optional: Individual relative projection errors of the observations in newTensor.
  • heuristicsToUselist, optional: List of heuristics to use while updating TT-cores. Currently only accepts 'skip', 'subselect', and 'occupancy'.
  • occupancyThresholdfloat, optional: Threshold determining whether to skip updating a single core or not. Not used if 'occupancy' is not in heuristicsToUse
  • simpleEpsilonUpdatebool, optional: Uses the simple epsilon update equation. Warning: this relies on the assumption that all observations in newTensor have similar norms.
Notes

The following attributes are modified as a result of this function:

def ttRound(self, norm, epsilon=0) -> None:
 948    def ttRound(self, norm, epsilon=0) -> None:
 949        """
 950        Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_.
 951
 952        This is the TT-rounding algorithm described in `TTSVD`_ paper. This
 953        function first reorthogonalizes the cores from last to first using
 954        QR decomposition and then recompresses the reorthogonalized TT-cores
 955        from first to last using `TTSVD`_ algorithm.
 956
 957        Parameters
 958        ----------
 959        norm:obj:`float`
 960            Norm of the TT-approximation. If there is no information on the
 961            uncompressed tensor, `ttNorm` function can be used.
 962        epsilon:obj:`float`, optional
 963            Relative error upper bound for the recompression step. Note that
 964            this relative error upper bound is not with respect to the original
 965            tensor but to the `compressed` tensor. Set to 0 by default.
 966
 967        Notes
 968        -----
 969        **The following attributes are modified as a result of this function:**
 970        - `ttObject.ttCores`
 971        - `ttObject.ttRanks`
 972        - `ttObject.compressionRatio`
 973
 974        .. _Oseledets 2011:
 975            https://epubs.siam.org/doi/epdf/10.1137/090752286
 976            TTSVD:
 977            https://epubs.siam.org/doi/epdf/10.1137/090752286
 978        """
 979        d = [core.shape[1] for core in self.ttCores]
 980        for coreIdx in np.arange(len(self.ttCores))[::-1]:
 981            currCore = self.ttCores[coreIdx]
 982            # Compute mode 1 unfolding of the tt-core
 983            currCore = currCore.reshape(currCore.shape[0], -1)
 984            # Using the transpose results in a row orthogonal Q matrix !!!
 985            q, r = np.linalg.qr(currCore.T)
 986            q, r = q.T, r.T
 987            self.ttCores[coreIdx] = q
 988            # Contract previous tt-core and R matrix
 989            self.ttCores[coreIdx - 1] = np.tensordot(
 990                self.ttCores[coreIdx - 1], r, axes=(-1, 0)
 991            )
 992            if coreIdx == 1:
 993                break
 994                # pass #TODO: write the case for the last core here.
 995        # Compression of the orthogonalized representation #
 996        ranks = [1]
 997        for coreIdx in range(len(self.ttCores) - 1):
 998            core = self.ttCores[coreIdx]
 999            core = core.reshape(np.prod(core.shape[:-1]), -1)
1000            self.ttCores[coreIdx], sigma, v = deltaSVD(
1001                core, norm, len(self.ttCores), epsilon
1002            )
1003            ranks.append(self.ttCores[coreIdx].shape[-1])
1004            # fold matrices into tt-cores
1005            self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape(
1006                ranks[coreIdx], d[coreIdx], -1
1007            )
1008            self.ttCores[coreIdx + 1] = (
1009                (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1]
1010            ).reshape(ranks[-1] * d[coreIdx + 1], -1)
1011        self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1)
1012        self.ttRanks = [1]
1013        for core in self.ttCores:
1014            self.ttRanks.append(core.shape[-1])
1015        return None

Reorthogonalizes and recompresses TT-cores as per Oseledets 2011.

This is the TT-rounding algorithm described in TTSVD_ paper. This function first reorthogonalizes the cores from last to first using QR decomposition and then recompresses the reorthogonalized TT-cores from first to last using TTSVD_ algorithm.

Parameters
  • normfloat: Norm of the TT-approximation. If there is no information on the uncompressed tensor, ttNorm function can be used.
  • epsilonfloat, optional: Relative error upper bound for the recompression step. Note that this relative error upper bound is not with respect to the original tensor but to the compressed tensor. Set to 0 by default.
Notes

The following attributes are modified as a result of this function:

TTSVD:
https://epubs.siam.org/doi/epdf/10.1137/090752286
@staticmethod
def ittd(tt1, tt2, rounding=True, epsilon=0) -> ttObject:
1017    @staticmethod
1018    def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject":
1019        """
1020        Alternative incremental tensor decomposition algorithm used to benchmark.
1021
1022        Note
1023        ----
1024        This method is empirically shown to be inferior to `TT-ICE`_ and
1025        `TT-ICE*`_.
1026
1027        Parameters
1028        ----------
1029        tt1:obj:`ttObject`
1030            Tensor in TT-format.
1031        tt2:obj:`ttObject`
1032            Tensor in TT-format.
1033        rounding:obj:`bool`, optional
1034            Determines if `ttRounding` will be done to the incremented TT-core.
1035            Note that for unbounded streams, rounding is essential. Otherwise
1036            TT-ranks will grow unboundedly.
1037        epsilon:obj:`float`, optional
1038            Relative error upper bound for the recompression step. Note that
1039            this relative error upper bound is not with respect to the original
1040            tensor but to the `compressed` tensor. Set to 0 by default. Not used
1041            if `rounding` is set to `False`.
1042
1043        Notes
1044        -----
1045        **The following attributes are modified as a result of this function:**
1046        - `ttObject.ttCores`
1047        - `ttObject.ttRanks`
1048        - `ttObject.compressionRatio`
1049
1050        .. _TT-ICE:
1051            https://arxiv.org/abs/2211.12487
1052            _TT-ICE*:
1053            https://arxiv.org/abs/2211.12487
1054        """
1055        if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1056            if isinstance(tt1, list):
1057                tt1 = ttObject(tt1)
1058            if isinstance(tt2, list):
1059                tt2 = ttObject(tt2)
1060            if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject):
1061                raise AttributeError("One of the passed objects is not in TT-format!")
1062        tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1)
1063        tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1)
1064        rank1, numObs1 = tt1.ttCores[-1].shape
1065        rank2, numObs2 = tt2.ttCores[-1].shape
1066        tt1.ttCores[-1] = np.concatenate(
1067            (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1
1068        )
1069        tt2.ttCores[-1] = np.concatenate(
1070            (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1
1071        )
1072        # Addition in TT-format #
1073        for coreIdx in range(len(tt1.ttCores)):
1074            if coreIdx == 0:
1075                tt1.ttCores[coreIdx] = np.concatenate(
1076                    (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)),
1077                    axis=1,
1078                )[None, :, :]
1079            elif coreIdx == len(tt1.ttCores) - 1:
1080                tt1.ttCores[coreIdx] = np.concatenate(
1081                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1082                )[:, :, None]
1083            else:
1084                s11, s12, s13 = tt1.ttCores[coreIdx].shape
1085                s21, s22, s23 = tt2.ttCores[coreIdx].shape
1086                tt1.ttCores[coreIdx] = np.concatenate(
1087                    (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2
1088                )
1089                tt2.ttCores[coreIdx] = np.concatenate(
1090                    (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2
1091                )
1092                tt1.ttCores[coreIdx] = np.concatenate(
1093                    (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0
1094                )
1095        tt1.ttRanks = [1]
1096        for core in tt1.ttCores:
1097            tt1.ttRanks.append(core.shape[-1])
1098        if rounding:
1099            tenNorm = ttObject.ttNorm(tt1)
1100            tt1.ttRound(tenNorm, epsilon=epsilon)
1101        return tt1

Alternative incremental tensor decomposition algorithm used to benchmark.

Note

This method is empirically shown to be inferior to TT-ICE and TT-ICE*.

Parameters
  • tt1ttObject: Tensor in TT-format.
  • tt2ttObject: Tensor in TT-format.
  • roundingbool, optional: Determines if ttRounding will be done to the incremented TT-core. Note that for unbounded streams, rounding is essential. Otherwise TT-ranks will grow unboundedly.
  • epsilonfloat, optional: Relative error upper bound for the recompression step. Note that this relative error upper bound is not with respect to the original tensor but to the compressed tensor. Set to 0 by default. Not used if rounding is set to False.
Notes

The following attributes are modified as a result of this function:

@staticmethod
def ttfoa6d(data, ttRanks, ttCores=None, S=None, forgettingFactor=None):
1103    @staticmethod
1104    def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1105        nDims=len(data.shape)
1106        dims=list(data.shape)
1107        coreSwap=False
1108        if forgettingFactor is None:
1109            forgettingFactor=0.7
1110
1111        if len(ttRanks) != nDims-1:
1112            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1113        
1114        if ttCores is None:
1115            # Initialize TT-cores randomly if no core is given.
1116            ttCores=[]
1117            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1118            for dimIdx in range(1,nDims-1):
1119                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1120            ttCores.append(np.empty((ttRanks[-1],1)))
1121            coreSwap=True
1122        # else:
1123        ttCores_old=ttCores.copy()
1124        if S is None:
1125            # Initialize S matrices as identity matrices if no S is given from previous step.
1126            S=[]
1127            S.append(100*np.eye(ttRanks[0]))
1128            for dimIdx in range(1,nDims-1):
1129                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1130
1131        # Update the last core G6
1132        
1133        Ht_1=coreContraction(ttCores_old[:-1])
1134        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1135        Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5
1136        g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1137        if coreSwap:
1138            ttCores[-1]=g6
1139        else:
1140            ttCores[-1]=np.hstack((ttCores[-1],g6))
1141        recData=(Ht_1@g6).reshape(dims,order='F')
1142        delta=data-recData
1143
1144        # Update G1
1145        deltaunfold=mode_n_unfolding(delta,0)
1146        Bk=coreContraction(ttCores_old[1:-1]+[g6])
1147        Wk=mode_n_unfolding(Bk,0)
1148        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1149        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1150        Vk=np.linalg.pinv(S[0])@Wk
1151        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1152
1153        # Update G2
1154        deltaunfold=mode_n_unfolding(delta,1)
1155        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1156        Bk=coreContraction(ttCores_old[2:-1]+[g6])
1157        Bk=mode_n_unfolding(Bk,0)
1158        Wk=np.kron(Bk,Ak)
1159        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1160        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1161        Vk=np.linalg.pinv(S[1])@Wk
1162        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1163        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1164        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1165
1166        # Update G3
1167        deltaunfold=mode_n_unfolding(delta,2)
1168        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1169        Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1170        Bk=mode_n_unfolding(Bk,0)
1171        Wk=np.kron(Bk,Ak)
1172        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1173        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1174        Vk=np.linalg.pinv(S[2])@Wk
1175        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1176        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1177        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1178
1179        # Update G4
1180        deltaunfold=mode_n_unfolding(delta,3)
1181        Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3)
1182        Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary
1183        Wk=np.kron(Bk,Ak)
1184        S[3]=forgettingFactor=S[3]+Wk@Wk.T
1185        # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0]
1186        Vk=np.linalg.pinv(S[3])@Wk
1187        ttCores[3]=mode_n_unfolding(ttCores_old[3],1)
1188        ttCores[3]=ttCores[3]+deltaunfold@Vk.T
1189        ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2)
1190
1191        # Update G5
1192        deltaunfold=mode_n_unfolding(delta,4)
1193        Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4)
1194        # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary
1195        Wk=np.kron(g6,Ak)
1196        S[4]=forgettingFactor=S[4]+Wk@Wk.T
1197        # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0]
1198        Vk=np.linalg.pinv(S[4])@Wk
1199        ttCores[4]=mode_n_unfolding(ttCores_old[4],1)
1200        ttCores[4]=ttCores[4]+deltaunfold@Vk.T
1201        ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2)
1202
1203        return S,ttCores
@staticmethod
def ttfoa4d(data, ttRanks, ttCores=None, S=None, forgettingFactor=None):
1205    @staticmethod
1206    def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None):
1207        nDims=len(data.shape)
1208        dims=list(data.shape)
1209        coreSwap=False
1210        if forgettingFactor is None:
1211            forgettingFactor=0.7
1212
1213        if len(ttRanks) != nDims-1:
1214            raise ValueError("Number of dimensions do not match with number of TT-ranks")
1215        
1216        if ttCores is None:
1217            # Initialize TT-cores randomly if no core is given.
1218            ttCores=[]
1219            ttCores.append(np.random.randn(1,dims[0],ttRanks[0]))
1220            for dimIdx in range(1,nDims-1):
1221                ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx]))
1222            ttCores.append(np.empty((ttRanks[-1],1)))
1223            coreSwap=True
1224        # else:
1225        ttCores_old=ttCores.copy()
1226        if S is None:
1227            # Initialize S matrices as identity matrices if no S is given from previous step.
1228            S=[]
1229            S.append(100*np.eye(ttRanks[0]))
1230            for dimIdx in range(1,nDims-1):
1231                S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx]))
1232
1233        Ht_1=coreContraction(ttCores_old[:-1])
1234        # Ht_1=Ht_1.reshape(-1,ttRanks[-1])
1235        Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4
1236        g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F')
1237        if coreSwap:
1238            ttCores[-1]=g4
1239        else:
1240            ttCores[-1]=np.hstack((ttCores[-1],g4))
1241        recData=(Ht_1@g4).reshape(dims,order='F')
1242        delta=data-recData
1243
1244        # Update G1
1245        deltaunfold=mode_n_unfolding(delta,0)
1246        # Bk=coreContraction(ttCores_old[1:-1]+[g4])
1247        Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0])
1248        Wk=mode_n_unfolding(Bk,0)
1249        S[0]=forgettingFactor*S[0]+Wk@Wk.T
1250        # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0]
1251        try:
1252            Vk=np.linalg.solve(S[0],Wk)
1253        except np.linalg.LinAlgError:
1254            print('Singular S[0] matrix, using Moore-Penrose inverse')
1255            Vk=np.linalg.pinv(S[0])@Wk
1256        ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T
1257
1258        # Update G2
1259        deltaunfold=mode_n_unfolding(delta,1)
1260        Ak=mode_n_unfolding(ttCores_old[0],2)#.T
1261        Bk=coreContraction(ttCores_old[2:-1]+[g4])
1262        Bk=mode_n_unfolding(Bk,0)
1263        Wk=np.kron(Bk,Ak)
1264        S[1]=forgettingFactor*S[1]+Wk@Wk.T
1265        # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0]
1266        try:
1267            Vk=np.linalg.solve(S[1],Wk)
1268        except np.linalg.LinAlgError:
1269            print('Singular S[1] matrix, using Moore-Penrose inverse')
1270            Vk=np.linalg.pinv(S[1])@Wk
1271        ttCores[1]=mode_n_unfolding(ttCores_old[1],1)
1272        ttCores[1]=ttCores[1]+deltaunfold@Vk.T
1273        ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2)
1274
1275        # Update G3
1276        deltaunfold=mode_n_unfolding(delta,2)
1277        Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2)
1278        # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary
1279        # Bk=mode_n_unfolding(Bk,0)
1280        Wk=np.kron(g4,Ak)
1281        S[2]=forgettingFactor*S[2]+Wk@Wk.T
1282        # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0]
1283        try:
1284            Vk=np.linalg.solve(S[2],Wk)
1285        except np.linalg.LinAlgError:
1286            print('Singular S[2] matrix, using Moore-Penrose inverse')
1287            Vk=np.linalg.pinv(S[2])@Wk
1288        ttCores[2]=mode_n_unfolding(ttCores_old[2],1)
1289        ttCores[2]=ttCores[2]+deltaunfold@Vk.T
1290        ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2)
1291
1292        return S,ttCores