• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sandialabs / pyttb / 5182507579

05 Jun 2023 10:30PM UTC coverage: 98.358% (-1.3%) from 99.703%
5182507579

push

github

web-flow
Cleanup regressions (#132)

* Fix error checking for windows triple slash

* Pylint: Add back our module level doc strings, even if slightly redundant currently

* Pylint: Inheritting from object isn't needed in python 3

* Pylint: Opt in to our slower running linting tests.
* Once we have full linting coverage this can be its own stage but for now run the pytest hack with list of covered files.

4313 of 4385 relevant lines covered (98.36%)

2.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.9
/pyttb/ktensor.py
1
"""Classes and functions for working with Kruskal tensors."""
2
# Copyright 2022 National Technology & Engineering Solutions of Sandia,
3
# LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the
4
# U.S. Government retains certain rights in this software.
5

6
from __future__ import annotations
7

8
import logging
9
import warnings
10

11
import numpy as np
12
import scipy.sparse as sparse
13
import scipy.sparse.linalg
14

15
import pyttb as ttb
16
from pyttb.pyttb_utils import *
17

18

19
class ktensor(object):
20
    """
21
    KTENSOR Class for Kruskal tensors (decomposed).
×
22

23
    Contains the following data members:
×
24

25
    ``weights``: :class:`numpy.ndarray` vector containing the weights of the
×
26
    rank-1 tensors defined by the outer products of the column vectors of the
×
27
    factor_matrices.
×
28

29
    ``factor_matrices``: :class:`list` of :class:`numpy.ndarray`. The length
×
30
    of the list is equal to the number of dimensions of the tensor. The shape
×
31
    of the ith element of the list is (n_i, r), where n_i is the length
×
32
    dimension i and r is the rank of the tensor (as well as the length of the
×
33
    weights vector).
×
34

35
    Although the constructor `__init__()` can be used to create an empty
×
36
    :class:`pyttb.ktensor`, there are several class methods that can be used
×
37
    to create an instance of this class:
×
38

39
      * :meth:`from_data`
×
40
      * :meth:`from_tensor_type`
×
41
      * :meth:`from_factor_matrices`
×
42
      * :meth:`from_function`
×
43
      * :meth:`from_vector`
×
44

45
    Examples
×
46
    --------
×
47
    For all examples listed below, the following module imports are assumed:
×
48

49
    >>> import pyttb as ttb
×
50
    >>> import numpy as np
×
51
    """
×
52

53
    __slots__ = ("weights", "factor_matrices")
3✔
54

55
    def __init__(self):
3✔
56
        """
57
        Construct an empty :class:`pyttb.ktensor`
58

59
        The constructor takes no arguments and returns an empty
60
        :class:`pyttb.ktensor`.
61
        """
62
        # Empty constructor
63
        self.weights = np.array([])
3✔
64
        self.factor_matrices = []
3✔
65

66
    @classmethod
3✔
67
    def from_data(cls, weights, *factor_matrices):
3✔
68
        """
69
        Construct a :class:`pyttb.ktensor` from weights and factor matrices.
70

71
        The length of the list or the number of arguments specified by
72
        `factor_matrices` must equal the length of `weights`. See
73
        :class:`pyttb.ktensor` for parameter descriptions.
74

75
        Parameters
76
        ----------
77
        weights: :class:`numpy.ndarray`, required
78
        factor_matrices: :class:`list` of :class:`numpy.ndarray` or variable number of :class:`numpy.ndarray`, required
79

80
        Returns
81
        -------
82
        :class:`pyttb.ktensor`
83

84
        Examples
85
        --------
86
        Create a :class:`pyttb.ktensor` from weights and a list of factor
87
        matrices:
88

89
        >>> weights = np.array([1., 2.])
90
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
91
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
92
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
93
        >>> print(K)
94
        ktensor of shape 2 x 2
95
        weights=[1. 2.]
96
        factor_matrices[0] =
97
        [[1. 2.]
98
         [3. 4.]]
99
        factor_matrices[1] =
100
        [[5. 6.]
101
         [7. 8.]]
102

103
        Create a :class:`pyttb.ktensor` from weights and factor matrices passed as
104
        arguments:
105

106
        >>> K = ttb.ktensor.from_data(weights, fm0, fm1)
107
        >>> print(K)
108
        ktensor of shape 2 x 2
109
        weights=[1. 2.]
110
        factor_matrices[0] =
111
        [[1. 2.]
112
         [3. 4.]]
113
        factor_matrices[1] =
114
        [[5. 6.]
115
         [7. 8.]]
116
        """
117
        # Check individual input parameters
118
        assert isinstance(weights, np.ndarray) and isvector(
3✔
119
            weights
3✔
120
        ), "Input parameter 'weights' must be a numpy.array type vector."
121

122
        # Input can be a list or sequences of factor_matrices
123
        if isinstance(factor_matrices[0], list):
3✔
124
            _factor_matrices = [f.copy() for f in factor_matrices[0]]
3✔
125
        else:
126
            _factor_matrices = [f.copy() for f in factor_matrices[0:]]
3✔
127
        for fm in _factor_matrices:
3✔
128
            assert isinstance(
3✔
129
                fm, np.ndarray
3✔
130
            ), "Input parameter 'factor_matrices' must be a list of numpy.array's."
131

132
        # Check dimensions of weights and factor_matrices
133
        num_weights = len(weights)
3✔
134
        for i, fm in enumerate(_factor_matrices):
3✔
135
            assert (
2✔
136
                num_weights == fm.shape[1]
3✔
137
            ), "Size of factor_matrix {} does not match number of weights ({}).".format(
138
                i, num_weights
139
            )
140

141
        # Create ktensor and populate data members
142
        k = cls()
3✔
143
        k.weights = weights.copy()
3✔
144
        if k.weights.dtype != float:
3✔
145
            print("converting weights from {} to float".format(k.weights.dtype))
3✔
146
            k.weights = k.weights.astype(float)
3✔
147
        k.factor_matrices = _factor_matrices
3✔
148
        for i in range(len(k.factor_matrices)):
3✔
149
            if k.factor_matrices[i].dtype != float:
3✔
150
                print(
3✔
151
                    "converting factor_matrices[{}] from {} to float".format(
3✔
152
                        i, k.factor_matrices[i].dtype
3✔
153
                    )
154
                )
155
                k.factor_matrices[i] = k.factor_matrices[i].astype(float)
3✔
156

157
        return k
3✔
158

159
    @classmethod
3✔
160
    def from_tensor_type(cls, source) -> ktensor:
3✔
161
        """
162
        Construct a :class:`pyttb.ktensor` from another
163
        :class:`pyttb.ktensor`. A deep copy of the data from the input
164
        :class:`pyttb.ktensor` is used for the new :class:`pyttb.ktensor`.
165

166
        Parameters
167
        ----------
168
        source: :class:`pyttb.ktensor`, required
169

170
        Returns
171
        -------
172
        :class:`pyttb.ktensor`
173

174
        Examples
175
        --------
176
        Create an instance of a :class:`pyttb.ktensor`:
177

178
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
179
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
180
        >>> factor_matrices = [fm0, fm1]
181
        >>> K_source = ttb.ktensor.from_factor_matrices(factor_matrices)
182
        >>> print(K_source)
183
        ktensor of shape 2 x 2
184
        weights=[1. 1.]
185
        factor_matrices[0] =
186
        [[1. 2.]
187
         [3. 4.]]
188
        factor_matrices[1] =
189
        [[5. 6.]
190
         [7. 8.]]
191

192
        Create another instance of a :class:`pyttb.ktensor` from the original
193
        one above:
194

195
        >>> K = ttb.ktensor.from_tensor_type(K_source)
196
        >>> print(K)
197
        ktensor of shape 2 x 2
198
        weights=[1. 1.]
199
        factor_matrices[0] =
200
        [[1. 2.]
201
         [3. 4.]]
202
        factor_matrices[1] =
203
        [[5. 6.]
204
         [7. 8.]]
205

206
        See also :func:`pyttb.ktensor.copy`
207
        """
208
        if isinstance(source, ktensor):
3✔
209
            return cls().from_data(
3✔
210
                source.weights.copy(), [f.copy() for f in source.factor_matrices]
3✔
211
            )
212

213
        # TODO impement conversion when symktensor has been implemented
214
        # if isinstance(source, ttb.symktensor):
215
        #     #    self.weights = args[0].weights;
216
        #     #    # MATLAB=> [t.u{1:varargin{1}.m,1}] = deal(varargin{1}.u);
217
        #     #    return
218
        #     raise NotImplementedError
219

220
        assert False, "Cannot convert from {} to ktensor".format(str(source.__class__))
3✔
221

222
    @classmethod
3✔
223
    def from_factor_matrices(cls, *factor_matrices):
3✔
224
        """
225
        Construct a :class:`pyttb.ktensor` from factor matrices. The weights
226
        of the returned :class:`pyttb.ktensor` will all be equal to 1.
227

228
        Parameters
229
        ----------
230
        factor_matrices: :class:`list` of :class:`numpy.ndarray` or variable number of :class:`numpy.ndarray`, required
231
            The number of columns of each of the factor matrices must be the
232
            same.
233

234
        Returns
235
        -------
236
        :class:`pyttb.ktensor`
237

238
        Examples
239
        --------
240
        Create a :class:`pyttb.ktensor` from a :class:`list` of factor
241
        matrices:
242

243
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
244
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
245
        >>> factor_matrices = [fm0, fm1]
246
        >>> K = ttb.ktensor.from_factor_matrices(factor_matrices)
247
        >>> print(K)
248
        ktensor of shape 2 x 2
249
        weights=[1. 1.]
250
        factor_matrices[0] =
251
        [[1. 2.]
252
         [3. 4.]]
253
        factor_matrices[1] =
254
        [[5. 6.]
255
         [7. 8.]]
256

257
        Create a :class:`pyttb.ktensor` from factor matrices passed as
258
        arguments:
259

260
        >>> K = ttb.ktensor.from_factor_matrices(fm0, fm1)
261
        >>> print(K)
262
        ktensor of shape 2 x 2
263
        weights=[1. 1.]
264
        factor_matrices[0] =
265
        [[1. 2.]
266
         [3. 4.]]
267
        factor_matrices[1] =
268
        [[5. 6.]
269
         [7. 8.]]
270
        """
271
        # CONSTRUCTOR FROM LIST OF FACTOR MATRICES
272
        # Input can be a list or sequences of factor_matrices
273
        if isinstance(factor_matrices[0], list):
3✔
274
            _factor_matrices = factor_matrices[0]
3✔
275
        else:
276
            _factor_matrices = [f for f in factor_matrices[0:]]
3✔
277
        for fm in _factor_matrices:
3✔
278
            assert isinstance(
3✔
279
                fm, np.ndarray
3✔
280
            ), "Input parameter 'factor_matrices' must be a list of numpy.array's."
281
        nc = _factor_matrices[0].shape[1]
3✔
282
        return cls().from_data(np.ones(nc), _factor_matrices)
3✔
283

284
    @classmethod
3✔
285
    def from_function(cls, fun, shape, num_components):
3✔
286
        """
287
        Construct a :class:`pyttb.ktensor` whose factor matrix entries are
288
        set using a function. The weights of the returned
289
        :class:`pyttb.ktensor` will all be equal to 1.
290

291
        Parameters
292
        ----------
293
        fun: function, required
294
            A function that can accept a shape (i.e., :class:`tuple` of
295
            dimension sizes) and return a :class:`numpy.ndarray` of that shape.
296
            Example functions include `numpy.random.random_sample`,
297
            `numpy,zeros`, `numpy.ones`.
298
        shape: :class:`tuple`, required
299
        num_components: int, required
300

301
        Returns
302
        -------
303
        :class:`pyttb.ktensor`
304

305
        Examples
306
        --------
307
        Create a :class:`pyttb.ktensor` with entries of the factor matrices
308
        taken from a uniform random distribution:
309

310
        >>> np.random.seed(1)
311
        >>> K = ttb.ktensor.from_function(np.random.random_sample, (2, 3, 4), 2)
312
        >>> print(K)  # doctest: +ELLIPSIS
313
        ktensor of shape 2 x 3 x 4
314
        weights=[1. 1.]
315
        factor_matrices[0] =
316
        [[4.1702...e-01 7.2032...e-01]
317
         [1.1437...e-04 3.0233...e-01]]
318
        factor_matrices[1] =
319
        [[0.1467... 0.0923...]
320
         [0.1862... 0.3455...]
321
         [0.3967... 0.5388...]]
322
        factor_matrices[2] =
323
        [[0.4191... 0.6852...]
324
         [0.2044... 0.8781...]
325
         [0.0273... 0.6704...]
326
         [0.4173...  0.5586...]]
327

328
        Create a :class:`pyttb.ktensor` with entries equal to 1:
329

330
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
331
        >>> print(K)
332
        ktensor of shape 2 x 3 x 4
333
        weights=[1. 1.]
334
        factor_matrices[0] =
335
        [[1. 1.]
336
         [1. 1.]]
337
        factor_matrices[1] =
338
        [[1. 1.]
339
         [1. 1.]
340
         [1. 1.]]
341
        factor_matrices[2] =
342
        [[1. 1.]
343
         [1. 1.]
344
         [1. 1.]
345
         [1. 1.]]
346

347
        Create a :class:`pyttb.ktensor` with entries equal to 0:
348

349
        >>> K = ttb.ktensor.from_function(np.zeros, (2, 3, 4), 2)
350
        >>> print(K)
351
        ktensor of shape 2 x 3 x 4
352
        weights=[1. 1.]
353
        factor_matrices[0] =
354
        [[0. 0.]
355
         [0. 0.]]
356
        factor_matrices[1] =
357
        [[0. 0.]
358
         [0. 0.]
359
         [0. 0.]]
360
        factor_matrices[2] =
361
        [[0. 0.]
362
         [0. 0.]
363
         [0. 0.]
364
         [0. 0.]]
365
        """
366
        # CONSTRUCTOR FROM FUNCTION HANDLE
367
        assert callable(fun), "Input parameter 'fun' must be a function."
3✔
368
        assert isinstance(shape, tuple), "Input parameter 'shape' must be a tuple."
3✔
369
        assert isinstance(
3✔
370
            num_components, int
3✔
371
        ), "Input parameter 'num_components' must be an int."
372
        nd = len(shape)
3✔
373
        weights = np.ones(num_components)
3✔
374
        factor_matrices = []
3✔
375
        for i in range(nd):
3✔
376
            factor_matrices.append(fun((shape[i], num_components)))
3✔
377
        return cls().from_data(weights, factor_matrices)
3✔
378

379
    @classmethod
3✔
380
    def from_vector(cls, data, shape, contains_weights):
3✔
381
        """
382
        Construct a :class:`pyttb.ktensor` from a vector (given as a
383
        :class:`numpy.ndarray`) and shape (given as a
384
        :class:`numpy.ndarray`). The rank of the :class:`pyttb.ktensor`
385
        is inferred from the shape and length of the vector.
386

387
        Parameters
388
        ----------
389
        data: :class:`numpy.ndarray`, required
390
            Vector containing either elements of the factor matrices (when
391
            `contains_weights`==False) or elements of the weights and factor
392
            matrices (when `contains_weights`==True). When both the elements of
393
            the weights and the factor_matrices are present, the weights come
394
            first and the columns of the factor matrices come next.
395
        shape: :class:`numpy.ndarray`, required
396
            Vector containing the shape of the tensor (i.e., lengths of the
397
            dimensions).
398
        contains_weights: bool, required
399
            Flag to specify whether or not `data` contains weights. If False,
400
            all weights are set to 1.
401

402
        Returns
403
        -------
404
        :class:`pyttb.ktensor`
405

406
        Examples
407
        --------
408
        Create a :class:`pyttb.ktensor` from a vector containing only
409
        elements of the factor matrices:
410

411
        >>> rank = 2
412
        >>> shape = np.array([2, 3, 4])
413
        >>> data = np.arange(1, rank*sum(shape)+1).astype(float)
414
        >>> K = ttb.ktensor.from_vector(data[:], shape, False)
415
        >>> print(K)
416
        ktensor of shape 2 x 3 x 4
417
        weights=[1. 1.]
418
        factor_matrices[0] =
419
        [[1. 3.]
420
         [2. 4.]]
421
        factor_matrices[1] =
422
        [[ 5.  8.]
423
         [ 6.  9.]
424
         [ 7. 10.]]
425
        factor_matrices[2] =
426
        [[11. 15.]
427
         [12. 16.]
428
         [13. 17.]
429
         [14. 18.]]
430

431
        Create a :class:`pyttb.ktensor` from a vector containing elements
432
        of both the weights and the factor matrices:
433

434
        >>> weights = 2 * np.ones(rank).astype(float)
435
        >>> weights_and_data = np.concatenate((weights, data), axis=0)
436
        >>> K = ttb.ktensor.from_vector(weights_and_data[:], shape, True)
437
        >>> print(K)
438
        ktensor of shape 2 x 3 x 4
439
        weights=[2. 2.]
440
        factor_matrices[0] =
441
        [[1. 3.]
442
         [2. 4.]]
443
        factor_matrices[1] =
444
        [[ 5.  8.]
445
         [ 6.  9.]
446
         [ 7. 10.]]
447
        factor_matrices[2] =
448
        [[11. 15.]
449
         [12. 16.]
450
         [13. 17.]
451
         [14. 18.]]
452
        """
453
        assert isvector(data), "Input parameter 'data' must be a numpy.array vector."
3✔
454
        assert isinstance(
3✔
455
            shape, np.ndarray
3✔
456
        ), "Input parameter 'shape' must be a numpy.array vector."
457
        assert isinstance(
3✔
458
            contains_weights, bool
3✔
459
        ), "Input parameter 'contains_weights' must be a bool."
460

461
        if isrow(data):
3✔
462
            data = data.T
3✔
463

464
        # compute the number of components from inputs
465
        if contains_weights:
3✔
466
            num_components = len(data) / (sum(shape) + 1)
3✔
467
        else:
468
            num_components = len(data) / sum(shape)
3✔
469

470
        if round(num_components) != num_components:
3✔
471
            assert False, "Input parameter 'data' is not the right length."
3✔
472
        else:
473
            num_components = int(num_components)
3✔
474

475
        # extract weights from input vector if present
476
        if contains_weights:
3✔
477
            weights = data[0:num_components].copy()
3✔
478
            shift = num_components
3✔
479
        else:
480
            weights = np.ones(num_components)
3✔
481
            shift = 0
3✔
482

483
        # extract factor matrices
484
        factor_matrices = []
3✔
485
        for n in range(len(shape)):
3✔
486
            mstart = num_components * sum(shape[0:n]) + shift
3✔
487
            mend = num_components * sum(shape[0 : n + 1]) + shift
3✔
488
            # the following will match MATLAB output
489
            factor_matrix = np.reshape(
3✔
490
                data[mstart:mend].copy(), (shape[n], num_components), order="F"
3✔
491
            )
492
            factor_matrices.append(factor_matrix)
3✔
493

494
        return cls().from_data(weights, factor_matrices)
3✔
495

496
    def arrange(self, weight_factor=None, permutation=None):
3✔
497
        """
498
        Arrange the rank-1 components of a :class:`pyttb.ktensor` in place.
499
        If `permutation` is passed, the columns of `self.factor_matrices` are
500
        arranged using the provided permutation, so you must make a copy
501
        before calling this method if you want to store the original
502
        :class:`pyttb.ktensor`. If `weight_factor` is passed, then the values
503
        in `self.weights` are absorbed into
504
        `self.factor_matrices[weight_factor]`. If no parameters are passed,
505
        then the columns of `self.factor_matrices` are normalized and then
506
        permuted such that the resulting `self.weights` are sorted by
507
        magnitude, greatest to least. Passing both parameters leads to an
508
        error.
509

510
        Parameters
511
        ----------
512
        weight_factor: int, optional
513
            The index of the factor matrix that the weights will be absorbed into.
514
        permutation: :class:`tuple`, :class:`list`, or :class:`numpy.ndarray`, optional
515
            The new order of the components of the :class:`pyttb.ktensor`
516
            into which to permute. The permutation must be of length equal to
517
            the number of components of the :class:`pyttb.ktensor`, `self.ncomponents`
518
            and must be a permutation of [0,...,`self.ncomponents`-1].
519

520
        Examples
521
        --------
522
        Create the initial :class:`pyttb.ktensor`:
523

524
        >>> weights = np.array([1., 2.])
525
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
526
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
527
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
528
        >>> print(K)
529
        ktensor of shape 2 x 2
530
        weights=[1. 2.]
531
        factor_matrices[0] =
532
        [[1. 2.]
533
         [3. 4.]]
534
        factor_matrices[1] =
535
        [[5. 6.]
536
         [7. 8.]]
537

538
        Arrange the columns of the factor matrices using a permutation:
539

540
        >>> p = [1,0]
541
        >>> K.arrange(permutation=p)
542
        >>> print(K)
543
        ktensor of shape 2 x 2
544
        weights=[2. 1.]
545
        factor_matrices[0] =
546
        [[2. 1.]
547
         [4. 3.]]
548
        factor_matrices[1] =
549
        [[6. 5.]
550
         [8. 7.]]
551

552
        Normalize and permute columns such that `weights` are sorted in
553
        decreasing order:
554

555
        >>> K.arrange()
556
        >>> print(K) # doctest: +ELLIPSIS
557
        ktensor of shape 2 x 2
558
        weights=[89.4427... 27.2029...]
559
        factor_matrices[0] =
560
        [[0.4472... 0.3162...]
561
         [0.8944... 0.9486...]]
562
        factor_matrices[1] =
563
        [[0.6... 0.5812...]
564
         [0.8... 0.8137...]]
565

566
        Absorb the weights into the second factor:
567

568
        >>> K.arrange(weight_factor=1)
569
        >>> print(K) # doctest: +ELLIPSIS
570
        ktensor of shape 2 x 2
571
        weights=[1. 1.]
572
        factor_matrices[0] =
573
        [[0.4472... 0.3162...]
574
         [0.8944... 0.9486...]]
575
        factor_matrices[1] =
576
        [[53.6656... 15.8113...]
577
         [71.5541... 22.1359...]]
578
        """
579
        if permutation is not None and weight_factor is not None:
3✔
580
            assert (
3✔
581
                False
3✔
582
            ), "Weighting and permuting the ktensor at the same time is not allowed."
3✔
583

584
        # arrange columns of factor matrices using the permutation provided
585
        if permutation is not None and isinstance(
3✔
586
            permutation, (tuple, list, np.ndarray)
3✔
587
        ):
588
            if len(permutation) == self.ncomponents:
3✔
589
                self.weights = self.weights[permutation]
3✔
590
                for i in range(self.ndims):
3✔
591
                    self.factor_matrices[i] = self.factor_matrices[i][:, permutation]
3✔
592
                return
3✔
593
            else:
594
                assert (
3✔
595
                    False
3✔
596
                ), "Number of elements in permutation does not match number of components in ktensor."
3✔
597

598
        # TODO there is a relationship here between normalize and arrange that repeats tasks.
599
        # Can this be made to be more efficient? ensure that factor matrices are normalized
600
        self.normalize()
3✔
601

602
        # sort
603
        p = np.argsort(self.weights)[::-1]
3✔
604
        self.weights = self.weights[p]
3✔
605
        for i in range(self.ndims):
3✔
606
            self.factor_matrices[i] = self.factor_matrices[i][:, p]
3✔
607

608
        # absorb the weights into one factor, optional
609
        if weight_factor is not None:
3✔
610
            r = len(self.weights)
3✔
611
            self.factor_matrices[weight_factor] *= self.weights
3✔
612
            self.weights = np.ones_like(self.weights)
3✔
613

614
        return
3✔
615

616
    def copy(self):
3✔
617
        """
618
        Make a deep copy of a :class:`pyttb.ktensor`.
619

620
        Returns
621
        -------
622
        :class:`pyttb.ktensor`
623

624
        Examples
625
        --------
626
        Create a random :class:`pyttb.ktensor` with weights of 1:
627

628
        >>> np.random.seed(1)
629
        >>> K = ttb.ktensor.from_function(np.random.random_sample, (2, 3, 4), 2)
630
        >>> print(K)  # doctest: +ELLIPSIS
631
        ktensor of shape 2 x 3 x 4
632
        weights=[1. 1.]
633
        factor_matrices[0] =
634
        [[4.1702...e-01 7.2032...e-01]
635
         [1.1437...e-04 3.0233...e-01]]
636
        factor_matrices[1] =
637
        [[0.1467... 0.0923...]
638
         [0.1862... 0.3455...]
639
         [0.3967... 0.5388...]]
640
        factor_matrices[2] =
641
        [[0.4191... 0.6852...]
642
         [0.2044... 0.8781...]
643
         [0.0273... 0.6704...]
644
         [0.4173... 0.5586...]]
645

646
        Create a copy of the :class:`pyttb.ktensor` and change the weights:
647

648
        >>> K2 = K.copy()
649
        >>> K2.weights = np.array([2., 3.])
650
        >>> print(K2)  # doctest: +ELLIPSIS
651
        ktensor of shape 2 x 3 x 4
652
        weights=[2. 3.]
653
        factor_matrices[0] =
654
        [[4.1702...e-01 7.2032...e-01]
655
         [1.1437...e-04 3.023...e-01]]
656
        factor_matrices[1] =
657
        [[0.1467... 0.0923...]
658
         [0.1862... 0.3455...]
659
         [0.3967... 0.5388...]]
660
        factor_matrices[2] =
661
        [[0.4191... 0.6852...]
662
         [0.2044... 0.8781...]
663
         [0.0273... 0.6704...]
664
         [0.4173... 0.5586...]]
665

666
        Show that the original :class:`pyttb.ktensor` is unchanged:
667

668
        >>> print(K)  # doctest: +ELLIPSIS
669
        ktensor of shape 2 x 3 x 4
670
        weights=[1. 1.]
671
        factor_matrices[0] =
672
        [[4.1702...e-01 7.2032...e-01]
673
         [1.1437...e-04 3.0233...e-01]]
674
        factor_matrices[1] =
675
        [[0.1467... 0.0923...]
676
         [0.1862... 0.3455...]
677
         [0.3967... 0.5388...]]
678
        factor_matrices[2] =
679
        [[0.4191... 0.6852...]
680
         [0.2044... 0.8781...]
681
         [0.0273... 0.6704...]
682
         [0.4173... 0.5586...]]
683
        """
684
        return ttb.ktensor.from_tensor_type(self)
3✔
685

686
    def double(self):
3✔
687
        """
688
        Convert :class:`pyttb.ktensor` to :class:`numpy.ndarray`.
689

690
        Returns
691
        -------
692
        :class:`numpy.ndarray`
693

694
        Examples
695
        --------
696
        >>> weights = np.array([1., 2.])
697
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
698
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
699
        >>> factor_matrices = [fm0, fm1]
700
        >>> K = ttb.ktensor.from_data(weights, factor_matrices)
701
        >>> K.double()
702
        array([[29., 39.],
703
               [63., 85.]])
704
        >>> type(K.double())
705
        <class 'numpy.ndarray'>
706
        """
707
        return self.full().double()
3✔
708

709
    def end(self, k=None):
3✔
710
        """
711
        Last index of indexing expression for :class:`pyttb.ktensor`.
712

713
        Parameters
714
        ----------
715
        k: int, optional
716
          dimension for subscripted indexing
717

718
        Returns
719
        -------
720
        int: index
721

722
        Examples
723
        --------
724
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
725
        >>> print(K.end(2))
726
        3
727
        """
728

729
        if k is not None:  # Subscripted indexing
3✔
730
            return self.shape[k] - 1
3✔
731
        else:  # For linear indexing
732
            return np.prod(self.shape) - 1
3✔
733

734
    def extract(self, idx=None):
3✔
735
        """
736
        Creates a new :class:`pyttb.ktensor` with only the specified
737
        components.
738

739
        Parameters
740
        ----------
741
        idx: int, :class:`tuple`, :class:`list`, :class:`numpy.ndarray`, optional
742
            Index set of components to extract. It should be the case that
743
            `idx` is a subset of [0,...,`self.ncomponents`]. If this
744
            parameter is None or is empty, a copy of the
745
            :class:`pyttb.ktensor` is returned.
746

747
        Returns
748
        -------
749
        :class:`pyttb.ktensor`
750

751
        Examples
752
        --------
753
        Create a :class:`pyttb.ktensor`:
754

755
        >>> weights = np.array([1., 2.])
756
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
757
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
758
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
759
        >>> print(K)
760
        ktensor of shape 2 x 2
761
        weights=[1. 2.]
762
        factor_matrices[0] =
763
        [[1. 2.]
764
         [3. 4.]]
765
        factor_matrices[1] =
766
        [[5. 6.]
767
         [7. 8.]]
768

769
        Create a new :class:`pyttb.ktensor`, extracting only the second
770
        component from each factor of the original :class:`pyttb.ktensor`:
771

772
        >>> K.extract([1])
773
        ktensor of shape 2 x 2
774
        weights=[2.]
775
        factor_matrices[0] =
776
        [[2.]
777
         [4.]]
778
        factor_matrices[1] =
779
        [[6.]
780
         [8.]]
781
        """
782
        # return a copy if no components have been specified
783
        if idx is None:
3✔
784
            return self.from_tensor_type(self)
3✔
785

786
        if isinstance(idx, (int, tuple, list, np.ndarray)):
3✔
787
            if isinstance(idx, int):
3✔
788
                components = [idx]
3✔
789
            else:
790
                components = idx
3✔
791
            if len(components) == 0 or len(components) > self.ncomponents:
3✔
792
                assert (
3✔
793
                    False
3✔
794
                ), "Number of components requested is not valid: {} (should be in [1,...,{}]).".format(
3✔
795
                    len(components), self.ncomponents
3✔
796
                )
797
            else:
798
                # check that all requested component indices are valid
799
                invalid_entries = []
3✔
800
                for i in range(len(components)):
3✔
801
                    if components[i] not in range(self.ncomponents):
3✔
802
                        invalid_entries.append(components[i])
3✔
803
                if len(invalid_entries) > 0:
3✔
804
                    assert (
3✔
805
                        False
3✔
806
                    ), "Invalid component indices to be extracted: {} not in range({})".format(
3✔
807
                        str(invalid_entries), self.ncomponents
3✔
808
                    )
809
                new_weights = self.weights[components]
3✔
810
                new_factor_matrices = []
3✔
811
                for i in range(self.ndims):
3✔
812
                    new_factor_matrices.append(self.factor_matrices[i][:, components])
3✔
813
                return self.from_data(new_weights, new_factor_matrices)
3✔
814
        else:
815
            assert False, "Input parameter must be an int, tuple, list or numpy.ndarray"
3✔
816

817
    def fixsigns(self, other=None):
3✔
818
        """
819
        Change the elements of a :class:`pyttb.ktensor` in place so that the
820
        largest magnitude entries for each column vector in each factor
821
        matrix are positive, provided that the sign on pairs of vectors in a
822
        rank-1 component can be flipped.
823

824
        Parameters
825
        ----------
826
        other: :class:`pyttb.ktensor`, optional
827
            If not None, returns a version of the :class:`pyttb.ktensor`
828
            where some of the signs of the columns of the factor matrices have
829
            been flipped to better align with `other`. In not None, both
830
            :class:`pyttb.ktensor` objects are first normalized (using
831
            :func:`~pyttb.ktensor.normalize`).
832

833
        Returns
834
        -------
835
        :class:`pyttb.ktensor`
836
            The changes are made in place and a reference to the updated
837
            tensor is returned
838

839
        Examples
840
        --------
841
        Create a :class:`pyttb.ktensor` with negative large magnitude entries:
842

843
        >>> weights = np.array([1., 2.])
844
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
845
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
846
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
847
        >>> K.factor_matrices[0][1, 1] = -K.factor_matrices[0][1, 1]
848
        >>> K.factor_matrices[1][1, 1] = -K.factor_matrices[1][1, 1]
849
        >>> print(K)
850
        ktensor of shape 2 x 2
851
        weights=[1. 2.]
852
        factor_matrices[0] =
853
        [[ 1.  2.]
854
         [ 3. -4.]]
855
        factor_matrices[1] =
856
        [[ 5.  6.]
857
         [ 7. -8.]]
858

859
        Fix the signs of the largest magnitude entries:
860

861
        >>> print(K.fixsigns())
862
        ktensor of shape 2 x 2
863
        weights=[1. 2.]
864
        factor_matrices[0] =
865
        [[ 1. -2.]
866
         [ 3.  4.]]
867
        factor_matrices[1] =
868
        [[ 5. -6.]
869
         [ 7.  8.]]
870

871
        Fix the signs using another :class:`pyttb.ktensor`:
872

873
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
874
        >>> K2 = K.copy()
875
        >>> K2.factor_matrices[0][1, 1] = -K2.factor_matrices[0][1, 1]
876
        >>> K2.factor_matrices[1][1, 1] = -K2.factor_matrices[1][1, 1]
877
        >>> K = K.fixsigns(K2)
878
        >>> print(K)  # doctest: +ELLIPSIS
879
        ktensor of shape 2 x 2
880
        weights=[27.2029... 89.4427...]
881
        factor_matrices[0] =
882
        [[ 0.3162... -0.4472...]
883
         [ 0.9486... -0.8944...]]
884
        factor_matrices[1] =
885
        [[ 0.5812... -0.6...]
886
         [ 0.8137... -0.8...]]
887
        """
888
        if other == None:
3✔
889
            for r in range(self.ncomponents):
3✔
890
                sgn = np.zeros(self.ndims)
3✔
891
                for n in range(self.ndims):
3✔
892
                    index_max = np.argmax(abs(self.factor_matrices[n][:, r]))
3✔
893
                    sgn[n] = np.sign(self.factor_matrices[n][index_max, r])
3✔
894

895
                negidx = np.nonzero(sgn == -1)[0]
3✔
896
                nflip = int(2 * np.floor(np.size(negidx) / 2))
3✔
897

898
                for i in range(nflip):
3✔
899
                    n = negidx[i]
3✔
900
                    self.factor_matrices[n][:, r] = -self.factor_matrices[n][:, r]
3✔
901

902
            return self
3✔
903
        else:
904
            if not isinstance(other, ktensor):
3✔
905
                assert False, "other must be a ktensor"
3✔
906

907
            self.normalize()
3✔
908
            other = other.normalize()
3✔
909

910
            N = self.ndims
3✔
911
            RA = self.ncomponents
3✔
912
            RB = other.ncomponents
3✔
913

914
            # Try to fix the signs for each component
915
            best_sign = np.zeros((N, RA))
3✔
916
            for r in range(RB):
3✔
917
                # Compute the inner products. They should mostly be O(1) if there is a
918
                # good match because the factors have prevsiouly been normalized. If
919
                # the signs are correct, then the score should be +1. Otherwise we need
920
                # to flip the sign and the score should be -1.
921
                sgn_score = np.zeros(N)
3✔
922
                for n in range(N):
3✔
923
                    sgn_score[n] = (
3✔
924
                        self.factor_matrices[n][:, r].T @ other.factor_matrices[n][:, r]
3✔
925
                    )
926

927
                # Sort the sign scores.
928
                sort_idx = np.argsort(sgn_score)
3✔
929
                sort_sgn_score = sgn_score.copy()[sort_idx]
3✔
930

931
                # Determine the number of scores that should be flipped.
932
                breakpt = np.nonzero(sort_sgn_score < 0)[-1]
3✔
933

934
                # If nothing needs to be flipped, then move on the the next component.
935
                if len(breakpt) == 0:
3✔
936
                    continue
3✔
937
                else:
938
                    breakpt = breakpt[-1]
3✔
939

940
                # Need to flip signs in pairs. If we don't have an even number of
941
                # negative sign scores, then we need to decide to do one fewer or one
942
                # more.
943
                if np.mod(breakpt + 1, 2) == 0:
3✔
944
                    endpt = breakpt + 1
3✔
945
                else:
946
                    warnings.warn("Trouble fixing signs for mode {}".format(r))
3✔
947
                    if (breakpt < RB) and (
3✔
948
                        -sort_sgn_score[breakpt] > sort_sgn_score[breakpt + 1]
3✔
949
                    ):
950
                        endpt = breakpt + 1
3✔
951
                    else:
952
                        endpt = breakpt - 1
3✔
953

954
                # Flip the signs
955
                for i in range(endpt):
3✔
956
                    self.factor_matrices[sort_idx[i]][:, r] = (
3✔
957
                        -1 * self.factor_matrices[sort_idx[i]][:, r]
3✔
958
                    )
959
                    best_sign[sort_idx[i], r] = -1
3✔
960

961
            return self
3✔
962

963
    def full(self):
3✔
964
        """
965
        Convert a :class:`pyttb.ktensor` to a :class:`pyttb.tensor`.
966

967
        Returns
968
        -------
969
        :class:`pyttb.tensor`
970

971
        Examples
972
        --------
973
        >>> weights = np.array([1., 2.])
974
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
975
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
976
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
977
        >>> print(K)
978
        ktensor of shape 2 x 2
979
        weights=[1. 2.]
980
        factor_matrices[0] =
981
        [[1. 2.]
982
         [3. 4.]]
983
        factor_matrices[1] =
984
        [[5. 6.]
985
         [7. 8.]]
986
        >>> print(K.full()) # doctest: +NORMALIZE_WHITESPACE
987
        tensor of shape 2 x 2
988
        data[:, :] =
989
        [[29. 39.]
990
         [63. 85.]]
991
        <BLANKLINE>
992
        """
993
        data = self.weights @ ttb.khatrirao(*self.factor_matrices, reverse=True).T
3✔
994
        return ttb.tensor.from_data(data, self.shape)
3✔
995

996
    def innerprod(self, other):
3✔
997
        """
998
        Efficient inner product with a :class:`pyttb.ktensor`.
999

1000
        Efficiently computes the inner product between two tensors, `self`
1001
            and `other`.  If other is a :class:`pyttb.ktensor`, the inner
1002
            product is computed using inner products of the factor matrices.
1003
            Otherwise, the inner product is computed using the `ttv` (tensor
1004
            times vector) of `other` with all of the columns of
1005
            `self.factor_matrices`.
1006

1007
        Parameters
1008
        ----------
1009
        other: :class:`pyttb.ktensor`, :class:`pyttb.sptensor`, :class:`pyttb.tensor`, or :class:`pyttb.ttensor`, required
1010
            Tensor with which to compute the inner product.
1011

1012
        Returns
1013
        -------
1014
        :float
1015

1016
        Examples
1017
        --------
1018
        >>> K = ttb.ktensor.from_function(np.ones, (2,3,4), 2)
1019
        >>> print(K.innerprod(K))
1020
        96.0
1021
        """
1022
        if not (self.shape == other.shape):
3✔
1023
            assert False, "Innerprod can only be computed for tensors of the same size"
3✔
1024

1025
        if isinstance(other, ktensor):
3✔
1026
            M = np.outer(self.weights, other.weights)
3✔
1027
            for i in range(self.ndims):
3✔
1028
                M = M * (self.factor_matrices[i].T @ other.factor_matrices[i])
3✔
1029
            return np.sum(np.sum(M))
3✔
1030

1031
        if isinstance(other, (ttb.sptensor, ttb.tensor, ttb.ttensor)):
3✔
1032
            res = 0.0
3✔
1033
            for r in range(self.ncomponents):
3✔
1034
                vecs = []
3✔
1035
                for n in range(self.ndims):
3✔
1036
                    vecs.append(self.factor_matrices[n][:, r])
3✔
1037
                res = res + self.weights[r] * other.ttv(vecs)
3✔
1038
            return res
3✔
1039

1040
    def isequal(self, other):
3✔
1041
        """
1042
        Equal comparator for :class:`pyttb.ktensor` objects.
1043

1044
        Parameters
1045
        ----------
1046
        other: :class:`pyttb.ktensor`, required
1047
            :class:`pyttb.ktensor` with which to compare.
1048

1049
        Returns
1050
        -------
1051
        :bool
1052

1053
        Examples
1054
        --------
1055
        >>> K1 = ttb.ktensor.from_function(np.ones, (2,3,4), 2)
1056
        >>> weights = np.ones((2, 1))
1057
        >>> factor_matrices = [np.ones((2, 2)), np.ones((3, 2)), np.ones((4, 2))]
1058
        >>> K2 = ttb.ktensor.from_data(weights, factor_matrices)
1059
        >>> print(K1.isequal(K2))
1060
        True
1061
        """
1062
        if not isinstance(other, ktensor):
3✔
1063
            return False
3✔
1064
        if not (self.ncomponents == other.ncomponents):
3✔
1065
            return False
3✔
1066
        if not (self.weights == other.weights).all():
3✔
1067
            return False
3✔
1068
        for k in range(self.ndims):
3✔
1069
            if not (self.factor_matrices[k] == other.factor_matrices[k]).all():
3✔
1070
                return False
3✔
1071
        return True
3✔
1072

1073
    def issymmetric(self, return_diffs=False):
3✔
1074
        """
1075
        Returns True if the :class:`pyttb.ktensor` is exactly symmetric for
1076
        every permutation.
1077

1078
        Parameters
1079
        ----------
1080
        return_diffs: bool, optional
1081
            If True, returns the matrix of the norm of the differences between
1082
            the factor matrices.
1083

1084
        Returns
1085
        -------
1086
        :bool
1087
        :class:`numpy.ndarray`, optional
1088
            Matrix of the norm of the differences between the factor matrices
1089

1090
        Examples
1091
        --------
1092
        Create a :class:`pyttb.ktensor` that is symmetric and test if it is
1093
        symmetric:
1094

1095
        >>> K = ttb.ktensor.from_function(np.ones, (3, 3, 3), 2)
1096
        >>> print(K.issymmetric())
1097
        True
1098

1099
        Create a :class:`pyttb.ktensor` that is not symmetric and return the
1100
        differences:
1101

1102
        >>> weights = np.array([1., 2.])
1103
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1104
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1105
        >>> K2 = ttb.ktensor.from_data(weights, [fm0, fm1])
1106
        >>> issym, diffs = K2.issymmetric(return_diffs=True)
1107
        >>> print(diffs)
1108
        [[0. 8.]
1109
         [0. 0.]]
1110
        """
1111
        diffs = np.zeros((self.ndims, self.ndims))
3✔
1112
        for i in range(self.ndims):
3✔
1113
            for j in range(i + 1, self.ndims):
3✔
1114
                if not (self.factor_matrices[i].shape == self.factor_matrices[j].shape):
3✔
1115
                    diffs[i, j] = np.inf
3✔
1116
                elif (self.factor_matrices[i] == self.factor_matrices[j]).all():
3✔
1117
                    diffs[i, j] = 0
3✔
1118
                else:
1119
                    diffs[i, j] = np.linalg.norm(
3✔
1120
                        self.factor_matrices[i] - self.factor_matrices[j]
3✔
1121
                    )
1122
        issym = (diffs == 0).all()
3✔
1123

1124
        if return_diffs:
3✔
1125
            return issym, diffs
3✔
1126
        else:
1127
            return issym
3✔
1128

1129
    def mask(self, W):
3✔
1130
        """
1131
        Extract :class:`pyttb.ktensor` values as specified by `W`, a
1132
        :class:`pyttb.tensor` or :class:`pyttb.sptensor` containing
1133
        only values of zeros (0) and ones (1). The values in the
1134
        :class:`pyttb.ktensor` corresponding to the indices for the
1135
        ones (1) in `W` will be returned as a column vector.
1136

1137
        Parameters
1138
        ----------
1139
        W: :class:`pyttb.tensor` or :class:`pyttb.sptensor`, required
1140

1141
        Returns
1142
        -------
1143
        :class:`numpy.ndarray`
1144

1145
        Examples
1146
        --------
1147
        Create a :class:`pyttb.ktensor`:
1148

1149
        >>> weights = np.array([1., 2.])
1150
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1151
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1152
        >>> K = ttb.ktensor.from_data(weights, [fm0, fm1])
1153

1154
        Create a mask :class:`pyttb.tensor` and extract the elements of the
1155
        :class:`pyttb.ktensor` using the mask:
1156

1157
        >>> W = ttb.tensor.from_data(np.array([[0, 1], [1, 0]]))
1158
        >>> print(K.mask(W))
1159
        [[63.]
1160
         [39.]]
1161
        """
1162
        # Error check
1163
        if len(W.shape) != len(self.shape) or np.any(
3✔
1164
            np.array(W.shape) > np.array(self.shape)
3✔
1165
        ):
1166
            assert False, "Mask cannot be bigger than the data tensor"
3✔
1167

1168
        # Extract locations of nonzeros in W
1169
        wsubs, _ = W.find()
3✔
1170

1171
        # Assemble return array
1172
        nvals = wsubs.shape[0]
3✔
1173
        vals = np.zeros((nvals, 1))
3✔
1174
        for j in range(self.ncomponents):
3✔
1175
            tmpvals = self.weights[j] * np.ones((nvals, 1))
3✔
1176
            for k in range(self.ndims):
3✔
1177
                akvals = self.factor_matrices[k][wsubs[:, [k]], j]
3✔
1178
                tmpvals = tmpvals * akvals
3✔
1179
            vals = vals + tmpvals
3✔
1180
        return vals
3✔
1181

1182
    def mttkrp(self, U, n):
3✔
1183
        """
1184
        Matricized tensor times Khatri-Rao product for :class:`pyttb.ktensor`.
1185

1186
        Efficiently calculates the matrix product of the n-mode matricization
1187
        of the `ktensor` with the Khatri-Rao product of all entries in U,
1188
        a :class:`list` of factor matrices, except the nth.
1189

1190
        Parameters
1191
        ----------
1192
        U: :class:`list` of factor matrices, required
1193
        n: int, required
1194
            Multiply by all modes except n.
1195

1196
        Returns
1197
        -------
1198
        :class:`numpy.ndarray`
1199

1200
        Examples
1201
        --------
1202
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1203
        >>> U = [np.ones((2, 2)), np.ones((3, 2)), np.ones(((4, 2)))]
1204
        >>> print(K.mttkrp(U, 0))
1205
        [[24. 24.]
1206
         [24. 24.]]
1207
        """
1208
        if not isinstance(U, list):
3✔
1209
            assert False, "Second argument must be list of numpy.ndarray's"
3✔
1210

1211
        if len(U) != self.ndims:
3✔
1212
            assert False, "List of factor matrices is the wrong length"
3✔
1213

1214
        # Number of columns in input matrices
1215
        if n == 0:
3✔
1216
            R = U[1].shape[1]
3✔
1217
        else:
1218
            R = U[0].shape[1]
3✔
1219

1220
        # Compute matrix of weights
1221
        W = np.tile(self.weights[:, None], (1, R))
3✔
1222
        for i in range(self.ndims):
3✔
1223
            if i != n:
3✔
1224
                W = W * (self.factor_matrices[i].T @ U[i])
3✔
1225

1226
        # Find each column of answer by multiplying columns of X.u{n} with weights
1227
        return self.factor_matrices[n] @ W
3✔
1228

1229
    @property
3✔
1230
    def ncomponents(self):
3✔
1231
        """
1232
        Number of components in the :class:`pyttb.ktensor` (i.e., number of
1233
        columns in each factor matrix) of the :class:`pyttb.ktensor`.
1234

1235
        Returns
1236
        -------
1237
        :int
1238

1239
        Examples
1240
        --------
1241
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1242
        >>> print(K.ncomponents)
1243
        2
1244
        """
1245
        return len(self.weights)
3✔
1246

1247
    @property
3✔
1248
    def ndims(self):
3✔
1249
        """
1250
        Number of dimensions (i.e., number of factor matrices) of the
1251
        :class:`pyttb.ktensor`.
1252

1253
        Returns
1254
        -------
1255
        :int
1256

1257
        Examples
1258
        --------
1259
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1260
        >>> print(K.ndims)
1261
        3
1262
        """
1263
        return len(self.factor_matrices)
3✔
1264

1265
    def norm(self):
3✔
1266
        """
1267
        Compute the norm (i.e., square root of the sum of squares of entries)
1268
        of a :class:`pyttb.ktensor`.
1269

1270
        Returns
1271
        --------
1272
        :int
1273

1274
        Examples
1275
        --------
1276
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1277
        >>> K.norm()
1278
        9.797958971132712
1279
        """
1280
        # Compute the matrix of correlation coefficients
1281
        coefMatrix = self.weights[:, None] @ self.weights[None, :]
3✔
1282
        for f in self.factor_matrices:
3✔
1283
            coefMatrix = coefMatrix * (f.T @ f)
3✔
1284
        return np.sqrt(np.abs(np.sum(coefMatrix)))
3✔
1285

1286
    def normalize(self, weight_factor=None, sort=False, normtype=2, mode=None):
3✔
1287
        """
1288
        Normalize the columns of the factor matrices of a
1289
        :class:`pyttb.ktensor` in place.
1290

1291
        Parameters
1292
        ----------
1293
        weight_factor: {"all", int}, optional
1294
            Absorb the weights into one or more factors. If "all", absorb
1295
            weight equally across all factors. If `int`, absorb weight into a
1296
            single dimension (value must be in range(self.ndims)).
1297
        sort: bool, optional
1298
            Sort the columns in descending order of the weights.
1299
        normtype: {non-negative int, -1, -2, np.inf, -np.inf}, optional
1300
            Order of the norm (see :func:`numpy.linalg.norm` for possible
1301
            values).
1302
        mode: int, optional
1303
            Index of factor matrix to normalize. A value of `None` means
1304
            normalize all factor matrices.
1305

1306
        Returns
1307
        -------
1308
        :class:`pyttb.ktensor`
1309

1310
        Examples
1311
        --------
1312
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1313
        >>> print(K.normalize()) # doctest: +ELLIPSIS
1314
        ktensor of shape 2 x 3 x 4
1315
        weights=[4.898... 4.898...]
1316
        factor_matrices[0] =
1317
        [[0.7071... 0.7071...]
1318
         [0.7071... 0.7071...]]
1319
        factor_matrices[1] =
1320
        [[0.5773... 0.5773...]
1321
         [0.5773... 0.5773...]
1322
         [0.5773... 0.5773...]]
1323
        factor_matrices[2] =
1324
        [[0.5 0.5]
1325
         [0.5 0.5]
1326
         [0.5 0.5]
1327
         [0.5 0.5]]
1328
        """
1329
        # when mode is specified, just normalize self.factor_matrices[mode]
1330
        if mode is not None:
3✔
1331
            if mode in range(self.ndims):
3✔
1332
                for r in range(self.ncomponents):
3✔
1333
                    tmp = np.linalg.norm(self.factor_matrices[mode][:, r], ord=normtype)
3✔
1334
                    if tmp > 0:
3✔
1335
                        self.factor_matrices[mode][:, r] = (
3✔
1336
                            1.0 / tmp * self.factor_matrices[mode][:, r]
3✔
1337
                        )
1338
                    self.weights[r] = self.weights[r] * tmp
3✔
1339
                return self
3✔
1340
            else:
1341
                assert (
3✔
1342
                    False
3✔
1343
                ), "Parameter single_factor is invalid; index must be an int in range of number of dimensions"
3✔
1344

1345
        # ensure that all factor_matrices are normalized
1346
        for mode in range(self.ndims):
3✔
1347
            for r in range(self.ncomponents):
3✔
1348
                tmp = np.linalg.norm(self.factor_matrices[mode][:, r], ord=normtype)
3✔
1349
                if tmp > 0:
3✔
1350
                    self.factor_matrices[mode][:, r] = (
3✔
1351
                        1.0 / tmp * self.factor_matrices[mode][:, r]
3✔
1352
                    )
1353
                self.weights[r] = self.weights[r] * tmp
3✔
1354

1355
        # check that all weights are positive, flip sign of columns in first factor matrix if negative weight found
1356
        idx = np.where(self.weights < 0)
3✔
1357
        self.factor_matrices[0][:, idx] = -self.factor_matrices[0][:, idx]
3✔
1358
        self.weights[idx] = -self.weights[idx]
3✔
1359

1360
        # absorb weight into factors
1361
        if weight_factor == "all":
3✔
1362
            # all factors
1363
            D = np.diag(np.power(self.weights, 1.0 / self.ndims))
3✔
1364
            for i in range(self.ndims):
3✔
1365
                self.factor_matrices[i] = self.factor_matrices[i] @ D
3✔
1366
            self.weights[:] = 1.0
3✔
1367
        elif weight_factor in range(self.ndims):
3✔
1368
            # single factor
1369
            self.factor_matrices[weight_factor] = self.factor_matrices[
3✔
1370
                weight_factor
3✔
1371
            ] @ np.diag(self.weights)
3✔
1372
            self.weights = np.ones((self.weights.shape))
3✔
1373

1374
        if sort:
3✔
1375
            if self.ncomponents > 1:
3✔
1376
                # indices of srting in descending order
1377
                p = np.argsort(self.weights)[::-1]
3✔
1378
                self.arrange(permutation=p)
3✔
1379

1380
        return self
3✔
1381

1382
    def nvecs(self, n, r, flipsign=True):
3✔
1383
        """
1384
        Compute the leading mode-n vectors for a :class:`pyttb.ktensor`.
1385

1386
        Computes the `r` leading eigenvectors of Xn*Xn.T (where Xn is the
1387
        mode-`n` matricization/unfolding of self), which provides information
1388
        about the mode-N fibers. In two-dimensions, the `r` leading mode-1
1389
        vectors are the same as the `r` left singular vectors and the `r`
1390
        leading mode-2 vectors are the same as the `r` right singular
1391
        vectors. By default, this method computes the top `r` eigenvectors
1392
        of Xn*Xn.T.
1393

1394
        Parameters
1395
        ----------
1396
        n: int, required
1397
            Mode for tensor matricization.
1398
        r: int, required
1399
            Number of eigenvectors to compute and use.
1400
        flipsign: bool, optional
1401
            If True, make each column's largest element positive.
1402

1403
        Returns
1404
        -------
1405
        :class:`numpy.ndarray`
1406

1407
        Examples
1408
        --------
1409
        Compute single eigenvector for dimension 0:
1410

1411
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
1412
        >>> nvecs1 = K.nvecs(0, 1)
1413
        >>> print(nvecs1) # doctest: +ELLIPSIS
1414
        [[0.70710678...]
1415
         [0.70710678...]]
1416

1417
        Compute first 2 leading eigenvectors for dimension 0:
1418

1419
        >>> nvecs2 = K.nvecs(0, 2)
1420
        >>> print(nvecs2) # doctest: +ELLIPSIS
1421
        [[ 0.70710678...  0.70710678...]
1422
         [ 0.70710678... -0.70710678...]]
1423
        """
1424
        M = self.weights[:, None] @ self.weights[:, None].T
3✔
1425
        for i in range(self.ndims):
3✔
1426
            if i != n:
3✔
1427
                M = M * (self.factor_matrices[i].T @ self.factor_matrices[i])
3✔
1428

1429
        y = self.factor_matrices[n] @ M @ self.factor_matrices[n].T
3✔
1430

1431
        if r < y.shape[0] - 1:
3✔
1432
            w, v = scipy.sparse.linalg.eigsh(y, r)
3✔
1433
            v = v[:, (-np.abs(w)).argsort()]
3✔
1434
            v = v[:, :r]
3✔
1435
        else:
1436
            logging.debug(
3✔
1437
                "Greater than or equal to ktensor.shape[n] - 1 eigenvectors requires cast to dense to solve"
3✔
1438
            )
1439
            w, v = scipy.linalg.eigh(y)
3✔
1440
            v = v[:, (-np.abs(w)).argsort()]
3✔
1441
            v = v[:, :r]
3✔
1442

1443
        if flipsign:
3✔
1444
            idx = np.argmax(np.abs(v), axis=0)
3✔
1445
            for i in range(v.shape[1]):
3✔
1446
                if v[idx[i], i] < 0:
3✔
1447
                    v[:, i] *= -1
3✔
1448
        return v
3✔
1449

1450
    def permute(self, order):
3✔
1451
        """
1452
        Permute :class:`pyttb.ktensor` dimensions.
1453

1454
        Rearranges the dimensions of a :class:`pyttb.ktensor` so that they are
1455
        in the order specified by `order`. The corresponding tensor has the
1456
        same components as `self` but the order of the subscripts needed to
1457
        access any particular element is rearranged as specified by `order`.
1458

1459
        Parameters
1460
        ----------
1461
        order: :class:`numpy.ndarray`
1462
            Permutation of [0,...,self.ndimensions].
1463

1464
        Returns
1465
        -------
1466
        :class:`pyttb.ktensor`
1467

1468
        Examples
1469
        --------
1470
        >>> weights = np.array([1., 2.])
1471
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1472
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1473
        >>> factor_matrices = [fm0, fm1]
1474
        >>> K = ttb.ktensor.from_data(weights, factor_matrices)
1475
        >>> print(K)
1476
        ktensor of shape 2 x 2
1477
        weights=[1. 2.]
1478
        factor_matrices[0] =
1479
        [[1. 2.]
1480
         [3. 4.]]
1481
        factor_matrices[1] =
1482
        [[5. 6.]
1483
         [7. 8.]]
1484

1485
        Permute the order of the dimension so they are in reverse order:
1486

1487
        >>> K1 = K.permute(np.array([1, 0]))
1488
        >>> print(K1)
1489
        ktensor of shape 2 x 2
1490
        weights=[1. 2.]
1491
        factor_matrices[0] =
1492
        [[5. 6.]
1493
         [7. 8.]]
1494
        factor_matrices[1] =
1495
        [[1. 2.]
1496
         [3. 4.]]
1497
        """
1498
        # Check that the permutation is valid
1499
        if tuple(range(self.ndims)) != tuple(sorted(order.tolist())):
3✔
1500
            assert False, "Invalid permutation"
3✔
1501

1502
        return ktensor.from_data(self.weights, [self.factor_matrices[i] for i in order])
3✔
1503

1504
    def redistribute(self, mode):
3✔
1505
        """
1506
        Distribute weights of a :class:`pyttb.ktensor` to the specified mode.
1507
        The redistribution is performed in place.
1508

1509
        Parameters
1510
        ----------
1511
        mode: int
1512
            Must be value in [0,...self.ndims].
1513

1514
        Example
1515
        -------
1516
        Create a :class:`pyttb.ktensor`:
1517

1518
        >>> weights = np.array([1., 2.])
1519
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1520
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1521
        >>> factor_matrices = [fm0, fm1]
1522
        >>> K = ttb.ktensor.from_data(weights, factor_matrices)
1523
        >>> print(K)
1524
        ktensor of shape 2 x 2
1525
        weights=[1. 2.]
1526
        factor_matrices[0] =
1527
        [[1. 2.]
1528
         [3. 4.]]
1529
        factor_matrices[1] =
1530
        [[5. 6.]
1531
         [7. 8.]]
1532

1533
        Distribute weights of that :class:`pyttb.ktensor` to mode 0:
1534

1535
        >>> K.redistribute(0)
1536
        >>> print(K)
1537
        ktensor of shape 2 x 2
1538
        weights=[1. 1.]
1539
        factor_matrices[0] =
1540
        [[1. 4.]
1541
         [3. 8.]]
1542
        factor_matrices[1] =
1543
        [[5. 6.]
1544
         [7. 8.]]
1545
        """
1546
        for r in range(self.ncomponents):
3✔
1547
            self.factor_matrices[mode][:, [r]] = (
3✔
1548
                self.factor_matrices[mode][:, [r]] * self.weights[r]
3✔
1549
            )
1550
            self.weights[r] = 1
3✔
1551

1552
    @property
3✔
1553
    def shape(self):
3✔
1554
        """Shape of a :class:`pyttb.ktensor`.
1555

1556
        Returns the lengths of all dimensions of the :class:`pyttb.ktensor`.
1557

1558
        Returns
1559
        -------
1560
        :class:`tuple`
1561
        """
1562
        return tuple([f.shape[0] for f in self.factor_matrices])
3✔
1563

1564
    def score(self, other, weight_penalty=True, threshold=0.99, greedy=True):
3✔
1565
        """
1566
        Checks if two :class:`pyttb.ktensor` instances with the same shapes
1567
        but potentially different number of components match except for
1568
        permutation.
1569

1570
        Matching is defined as follows. If `self` and `other` are single-
1571
        component :class:`pyttb.ktensor` instances that have been normalized
1572
        so that their weights are `self.weights` and `other.weights`, and their
1573
        factor matrices are single column vectors containing [a1,a2,...,an] and
1574
        [b1,b2,...bn], rescpetively, then the score is defined as
1575

1576
            score = penalty * (a1.T*b1) * (a2.T*b2) * ... * (an.T*bn),
1577

1578
        where the penalty is defined by the weights such that
1579

1580
            penalty = 1 - abs(self.weights - other.weights) / max(self.weights, other.weights).
1581

1582
        The score of multi-component :class:`pyttb.ktensor` instances is a
1583
        normalized sum of the scores across the best permutation of the
1584
        components of `self`. `self` can have more components than `other`;
1585
        any extra components are ignored in terms of the matching score.
1586

1587
        Parameters
1588
        ----------
1589
        other: :class:`pyttb.ktensor`, required
1590
            :class:`pyttb.ktensor` with which to match.
1591
        weight_penalty: bool, optional
1592
            Flag indicating whether or not to consider the weights in the
1593
            calculations.
1594
        threshold: float, optional
1595
            Threshold specified in the formula above for determining a match.
1596
        greedy: bool, optional
1597
            Flag indicating whether or not to consider all possible matchings
1598
            (exponentially expensive) or just do a greedy matching.
1599

1600
        Returns
1601
        -------
1602
        int
1603
            Score (between 0 and 1).
1604
        :class:`pyttb.ktensor`
1605
            Copy of `self`, which has been normalized and permuted to best match
1606
            `other`.
1607
        bool
1608
            Flag indicating a match according to a user-specified threshold.
1609
        :class:`numpy.ndarray`
1610
            Permutation (i.e. array of indices of the modes of self) of the
1611
            components of `self` that was used to best match `other`.
1612

1613
        Examples
1614
        --------
1615
        Create two :class:`pyttb.ktensor` instances and compute the score
1616
        between them:
1617

1618
        >>> K = ttb.ktensor.from_data(np.array([2., 1., 3.]), np.ones((3,3)), np.ones((4,3)), np.ones((5,3)))
1619
        >>> K2 = ttb.ktensor.from_data(np.array([2., 4.]), np.ones((3,2)), np.ones((4,2)), np.ones((5,2)))
1620
        >>> score,Kperm,flag,perm = K.score(K2)
1621
        >>> print(score)
1622
        0.875
1623
        >>> print(perm)
1624
        [0 2 1]
1625

1626
        Compute score without using weights:
1627

1628
        >>> score,Kperm,flag,perm = K.score(K2,weight_penalty=False)
1629
        >>> print(score)
1630
        1.0
1631
        >>> print(perm)
1632
        [0 1 2]
1633
        """
1634

1635
        if not greedy:
3✔
1636
            assert (
3✔
1637
                False
3✔
1638
            ), "Not yet implemented. Only greedy method is implemented currently."
3✔
1639

1640
        if not isinstance(other, ktensor):
3✔
1641
            assert False, "The first input should be a ktensor"
3✔
1642

1643
        if not (self.shape == other.shape):
3✔
1644
            assert False, "Size mismatch"
3✔
1645

1646
        # Set-up
1647
        N = self.ndims
3✔
1648
        RA = self.ncomponents
3✔
1649
        RB = other.ncomponents
3✔
1650

1651
        # We're matching components in A to B
1652
        if RA < RB:
3✔
1653
            assert False, "Tensor A must have at least as many components as tensor B"
3✔
1654

1655
        # Make sure columns of factor matrices are normalized
1656
        A = ttb.ktensor.from_tensor_type(self).normalize()
3✔
1657
        B = ttb.ktensor.from_tensor_type(other).normalize()
3✔
1658

1659
        # Compute all possible vector-vector congruences.
1660

1661
        # Compute every pair for each mode
1662
        Cbig = ttb.tensor.from_function(np.zeros, (RA, RB, N))
3✔
1663
        for n in range(N):
3✔
1664
            Cbig[:, :, n] = np.abs(A.factor_matrices[n].T @ B.factor_matrices[n])
3✔
1665

1666
        # Collapse across all modes using the product
1667
        C = Cbig.collapse(np.array([2]), np.prod).double()
3✔
1668

1669
        # Calculate penalty based on differences in the weights
1670
        # Note that we are assuming the the weights are positive because the
1671
        # ktensor's were previously normalized.
1672
        if weight_penalty:
3✔
1673
            P = np.zeros((RA, RB))
3✔
1674
            for ra in range(RA):
3✔
1675
                la = A.weights[ra]
3✔
1676
                for rb in range(RB):
3✔
1677
                    lb = B.weights[rb]
3✔
1678
                    if (la == 0) and (lb == 0):
3✔
1679
                        # if both lambda values are zero (0), they match
1680
                        P[ra, rb] = 1
3✔
1681
                    else:
1682
                        P[ra, rb] = 1 - (
3✔
1683
                            np.abs(la - lb) / np.max([np.abs(la), np.abs(lb)])
3✔
1684
                        )
1685
            C = P * C
3✔
1686

1687
        # Option to do greedy matching
1688
        if greedy:
3✔
1689
            best_perm = -1 * np.ones((RA), dtype=int)
3✔
1690
            best_score = 0
3✔
1691
            for r in range(RB):
3✔
1692
                idx = np.argmax(C.reshape(np.prod(C.shape), order="F"))
3✔
1693
                ij = tt_ind2sub((RA, RB), idx)
3✔
1694
                best_score = best_score + C[ij[0], ij[1]]
3✔
1695
                C[ij[0], :] = -10
3✔
1696
                C[:, ij[1]] = -10
3✔
1697
                best_perm[ij[1]] = ij[0]
3✔
1698
            best_score = best_score / RB
3✔
1699
            flag = 1
3✔
1700

1701
            # Rearrange the components of A according to the best matching
1702
            foo = np.arange(RA)
3✔
1703
            tf = np.in1d(foo, best_perm)
3✔
1704
            best_perm[RB : RA + 1] = foo[~tf]
3✔
1705
            A.arrange(permutation=best_perm)
3✔
1706
            return best_score, A, flag, best_perm
3✔
1707

1708
    def symmetrize(self):
3✔
1709
        """
1710
        Symmetrize a :class:`pyttb.ktensor` in all modes.
1711

1712
        Symmetrize a :class:`pyttb.ktensor` with respect to all modes so that
1713
        the resulting :class:`pyttb.ktensor` is symmetric with respect to any
1714
        permutation of indices.
1715

1716
        Returns
1717
        -------
1718
        :class:`pyttb.ktensor`
1719

1720
        Examples
1721
        --------
1722
        Create a :class:`pyttb.ktensor`:
1723

1724
        >>> weights = np.array([1., 2.])
1725
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1726
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1727
        >>> factor_matrices = [fm0, fm1]
1728
        >>> K = ttb.ktensor.from_data(weights, factor_matrices)
1729
        >>> print(K)
1730
        ktensor of shape 2 x 2
1731
        weights=[1. 2.]
1732
        factor_matrices[0] =
1733
        [[1. 2.]
1734
         [3. 4.]]
1735
        factor_matrices[1] =
1736
        [[5. 6.]
1737
         [7. 8.]]
1738

1739
        Make the factor matrices of the :class:`pyttb.ktensor` symmetric with
1740
        respect to any permutation of the factor matrices:
1741

1742
        >>> K1 = K.symmetrize()
1743
        >>> print(K1) # doctest: +ELLIPSIS
1744
        ktensor of shape 2 x 2
1745
        weights=[1. 1.]
1746
        factor_matrices[0] =
1747
        [[2.3404... 4.9519...]
1748
         [4.5960... 8.0124...]]
1749
        factor_matrices[1] =
1750
        [[2.3404... 4.9519...]
1751
         [4.5960... 8.0124...]]
1752
        """
1753
        # Check tensor dimensions for compatibility with symmetrization
1754
        assert (
3✔
1755
            self.shape == self.shape[0] * np.ones(self.ndims)
3✔
1756
        ).all(), "Tensor is not cubic -- cannot be symmetrized"
1✔
1757

1758
        # Distribute lambda evenly into factors
1759
        K = self.from_tensor_type(self)
3✔
1760
        K.normalize("all")
3✔
1761

1762
        weights = K.weights
3✔
1763
        factor_matrices = K.factor_matrices
3✔
1764
        fm0 = factor_matrices[0]
3✔
1765

1766
        V = fm0
3✔
1767
        for i in range(1, K.ndims):
3✔
1768
            fmi = factor_matrices[i]
3✔
1769
            for j in range(fm0.shape[1]):
3✔
1770
                if fm0[:, [j]].T @ fmi[:, [j]] < 0:
3✔
1771
                    fmi[:, [j]] = -fmi[:, [j]]
3✔
1772
                    weights[j] = -weights[j]
3✔
1773
            V = V + fmi
3✔
1774
        V = V / K.ndims
3✔
1775

1776
        # Odd-ordered tensors should not have any negative weights
1777
        if np.mod(K.ndims, 2) == 1:
3✔
1778
            for j in range(K.ncomponents):
3✔
1779
                if weights[j] < 0:
3✔
1780
                    weights[j] = -weights[j]
3✔
1781
                    V[:, [j]] = -V[:, [j]]
3✔
1782

1783
        return self.from_data(weights, [V.copy() for i in range(K.ndims)])
3✔
1784

1785
    def tolist(self, mode=None):
3✔
1786
        """
1787
        Convert :class:`pyttb.ktensor` to a list of factor matrices, evenly
1788
        distributing the weights across factors. Optionally absorb the
1789
        weights into a single mode.
1790

1791
        Parameters
1792
        ----------
1793
        mode: int, optional
1794
            Index of factor matrix to absorb all of the weights.
1795

1796
        Returns
1797
        -------
1798
        :class:`list` of :class:`numpy.ndarray`
1799

1800
        Examples
1801
        --------
1802
        Create a :class:`pyttb.ktensor` of all ones:
1803

1804
        >>> weights = np.array([1., 2.])
1805
        >>> fm0 = np.array([[1., 2.], [3., 4.]])
1806
        >>> fm1 = np.array([[5., 6.], [7., 8.]])
1807
        >>> factor_matrices = [fm0, fm1]
1808
        >>> K = ttb.ktensor.from_data(weights, factor_matrices)
1809
        >>> print(K)
1810
        ktensor of shape 2 x 2
1811
        weights=[1. 2.]
1812
        factor_matrices[0] =
1813
        [[1. 2.]
1814
         [3. 4.]]
1815
        factor_matrices[1] =
1816
        [[5. 6.]
1817
         [7. 8.]]
1818

1819
        Spread weights equally to all factors and return list of factor
1820
        matrices:
1821

1822
        >>> fm_list = K.tolist()
1823
        >>> for fm in fm_list: print(fm) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
1824
        [[1. 2.8284...]
1825
         [3. 5.6568...]]
1826
        [[ 5. 8.4852...]
1827
         [ 7. 11.313...]]
1828

1829
        Shift weight to single factor matrix and return list of factor
1830
        matrices:
1831

1832
        >>> fm_list = K.tolist(0)
1833
        >>> for fm in fm_list: print(fm)  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
1834
        [[ 8.6023... 40. ]
1835
         [25.8069... 80. ]]
1836
        [[0.5812... 0.6...]
1837
         [0.8137... 0.8...]]
1838
        """
1839
        if mode is not None:
3✔
1840
            if isinstance(mode, int) and mode in range(self.ndims):
3✔
1841
                self.normalize(mode)
3✔
1842
                return self.factor_matrices.copy()
3✔
1843
            else:
1844
                assert False, "Input parameter'mode' must be in the range of self.ndims"
3✔
1845

1846
        # all weights are equal to 1
1847
        if (self.weights == np.ones(self.weights.shape)).all():
3✔
1848
            return self.factor_matrices.copy()
3✔
1849

1850
        lsgn = np.sign(self.weights)
3✔
1851
        D = np.diag(np.power(np.fabs(self.weights), 1.0 / self.ndims))
3✔
1852
        factor_matrices = self.factor_matrices.copy()
3✔
1853
        factor_matrices[0] = factor_matrices[0] @ np.diag(lsgn)
3✔
1854
        for n in range(self.ndims):
3✔
1855
            factor_matrices[n] = factor_matrices[n] @ D
3✔
1856
        return factor_matrices
3✔
1857

1858
    def tovec(self, include_weights=True):
3✔
1859
        """
1860
        Convert :class:`pyttb.ktensor` to column vector. Optionally include
1861
        or exclude the weights.
1862

1863
        Parameters
1864
        ----------
1865
        include_weights: bool, optional
1866
            Flag to specify whether or not to include weights in output.
1867

1868
        Returns
1869
        -------
1870
        :class:`numpy.ndarray`
1871
            The length of the column vector is
1872
            (sum(self.shape)+1)*self.ncomponents. The vector contains the
1873
            weights (if requested) stacked on top of each of the columns of
1874
            the factor_matrices in order.
1875

1876
        Examples
1877
        --------
1878
        Create a :class:`pyttb.ktensor` from a vector:
1879

1880
        >>> rank = 2
1881
        >>> shape = np.array([2, 3, 4])
1882
        >>> data = np.arange(1, rank*sum(shape)+1)
1883
        >>> weights = 2 * np.ones(rank)
1884
        >>> weights_and_data = np.concatenate((weights, data), axis=0)
1885
        >>> K = ttb.ktensor.from_vector(weights_and_data[:], shape, True)
1886
        >>> print(K)
1887
        ktensor of shape 2 x 3 x 4
1888
        weights=[2. 2.]
1889
        factor_matrices[0] =
1890
        [[1. 3.]
1891
         [2. 4.]]
1892
        factor_matrices[1] =
1893
        [[ 5.  8.]
1894
         [ 6.  9.]
1895
         [ 7. 10.]]
1896
        factor_matrices[2] =
1897
        [[11. 15.]
1898
         [12. 16.]
1899
         [13. 17.]
1900
         [14. 18.]]
1901

1902
        Create a :class:`pyttb.ktensor` from a vector of data extracted from
1903
        another :class:`pyttb.ktensor`:
1904

1905
        >>> K2 = ttb.ktensor.from_vector(K.tovec(), shape, True)
1906
        >>> print(K2)
1907
        ktensor of shape 2 x 3 x 4
1908
        weights=[2. 2.]
1909
        factor_matrices[0] =
1910
        [[1. 3.]
1911
         [2. 4.]]
1912
        factor_matrices[1] =
1913
        [[ 5.  8.]
1914
         [ 6.  9.]
1915
         [ 7. 10.]]
1916
        factor_matrices[2] =
1917
        [[11. 15.]
1918
         [12. 16.]
1919
         [13. 17.]
1920
         [14. 18.]]
1921
        """
1922
        if include_weights:
3✔
1923
            x = np.zeros(self.ncomponents * int(sum(self.shape) + 1))
3✔
1924
            x[0 : self.ncomponents] = self.weights
3✔
1925
            offset = self.ncomponents
3✔
1926
        else:
1927
            x = np.zeros(self.ncomponents * int(sum(self.shape)))
3✔
1928
            offset = 0
3✔
1929

1930
        for f in self.factor_matrices:
3✔
1931
            for r in range(self.ncomponents):
3✔
1932
                x[offset : offset + f.shape[0]] = f[:, r].reshape(f.shape[0])
3✔
1933
                offset += f.shape[0]
3✔
1934

1935
        return x
3✔
1936

1937
    def ttv(self, vector, dims=None, exclude_dims=None):
3✔
1938
        """
1939
        Tensor times vector for a :class:`pyttb.ktensor`.
1940

1941
        Computes the product of a :class:`pyttb.ktensor` with a vector (i.e.,
1942
        np.array).  If `dims` is an integer, it specifies the dimension in the
1943
        :class:`pyttb.ktensor` along which the vector is multiplied.
1944
        If the shape of the vector is = (I,1), then the length of dimension
1945
        `dims` of the :class:`pyttb.ktensor` must be  I.  Note that the number
1946
        of dimensions of the returned :class:`pyttb.ktensor` is 1 less than
1947
        the dimension of the :class:`pyttb.ktensor` used in the
1948
        multiplication because dimension `dims` is removed.
1949

1950
        If `vector` is a :class:`list` of np.array instances, the
1951
        :class:`pyttb.ktensor` is multiplied with each vector in the list. The
1952
        products are computed sequentially along all dimensions (or modes) of
1953
        the :class:`pyttb.ktensor`, and thus the list must contain `self.ndims`
1954
        vectors.
1955

1956
        When `dims` is not None, compute the products along the dimensions
1957
        specified by `dims`. In this case, the number of products can be less
1958
        than `self.ndims` and the order of the sequence does not need to match
1959
        the order of the dimensions in the :class:`pyttb.ktensor`. Note that
1960
        the number of vectors must match the number of dimensions provided,
1961
        and the length of each vector must match the size of each dimension
1962
        of the :class:`pyttb.ktensor` specified in `dims`.
1963

1964
        Parameters
1965
        ----------
1966
        vector: :class:`numpy.ndarray` or list[:class:`numpy.ndarray`], required
1967
        dims: int, :class:`numpy.ndarray`, optional
1968
        exclude_dims:
1969

1970
        Returns
1971
        -------
1972
        float or :class:`pyttb.ktensor`
1973
            The number of dimensions of the returned :class:`pyttb.ktensor` is
1974
            n-k, where n = self.ndims and k = number of vectors provided as
1975
            input. If k == n, a scalar is returned.
1976

1977
        Examples
1978
        -------
1979
        Compute the product of a :class:`pyttb.ktensor` and a single vector
1980
        (results in a :class:`pyttb.ktensor`):
1981

1982
        >>> rank = 2
1983
        >>> shape = np.array([2, 3, 4])
1984
        >>> data = np.arange(1, rank*sum(shape)+1)
1985
        >>> weights = 2 * np.ones(rank)
1986
        >>> weights_and_data = np.concatenate((weights, data), axis=0)
1987
        >>> K = ttb.ktensor.from_vector(weights_and_data[:], shape, True)
1988
        >>> K0 = K.ttv(np.array([1, 1, 1]),dims=1) # compute along a single dimension
1989
        >>> print(K0)
1990
        ktensor of shape 2 x 4
1991
        weights=[36. 54.]
1992
        factor_matrices[0] =
1993
        [[1. 3.]
1994
         [2. 4.]]
1995
        factor_matrices[1] =
1996
        [[11. 15.]
1997
         [12. 16.]
1998
         [13. 17.]
1999
         [14. 18.]]
2000

2001
        Compute the product of a :class:`pyttb.ktensor` and a vector for each
2002
        dimension (results in a `float`):
2003

2004
        >>> vec2 = np.array([1, 1])
2005
        >>> vec3 = np.array([1, 1, 1])
2006
        >>> vec4 = np.array([1, 1, 1, 1])
2007
        >>> K1 = K.ttv([vec2, vec3, vec4])
2008
        >>> print(K1)
2009
        30348.0
2010

2011
        Compute the product of a :class:`pyttb.ktensor` and multiple vectors
2012
        out of order (results in a :class:`pyttb.ktensor`):
2013

2014
        >>> K2 = K.ttv([vec4, vec3],np.array([2, 1]))
2015
        >>> print(K2)
2016
        ktensor of shape 2
2017
        weights=[1800. 3564.]
2018
        factor_matrices[0] =
2019
        [[1. 3.]
2020
         [2. 4.]]
2021
        """
2022

2023
        if dims is None and exclude_dims is None:
3✔
2024
            dims = np.array([])
3✔
2025
        elif isinstance(dims, (float, int)):
3✔
2026
            dims = np.array([dims])
3✔
2027

2028
        if isinstance(exclude_dims, (float, int)):
3✔
2029
            exclude_dims = np.array([exclude_dims])
3✔
2030

2031
        # Check that vector is a list of vectors, if not place single vector as element in list
2032
        if len(vector) > 0 and isinstance(vector[0], (int, float, np.int_, np.float_)):
3✔
2033
            return self.ttv([vector], dims)
3✔
2034

2035
        # Get sorted dims and index for multiplicands
2036
        dims, vidx = ttb.tt_dimscheck(self.ndims, len(vector), dims, exclude_dims)
3✔
2037

2038
        # Check that each multiplicand is the right size.
2039
        for i in range(dims.size):
3✔
2040
            if vector[vidx[i]].shape != (self.shape[dims[i]],):
3✔
2041
                assert False, "Multiplicand is wrong size"
3✔
2042

2043
        # Figure out which dimensions will be left when we're done
2044
        remdims = np.setdiff1d(range(self.ndims), dims)
3✔
2045

2046
        # Collapse dimensions that are being multiplied out
2047
        new_weights = self.weights.copy()
3✔
2048
        for i in range(len(dims)):
3✔
2049
            new_weights = new_weights * (
3✔
2050
                self.factor_matrices[dims[i]].T @ vector[vidx[i]]
3✔
2051
            )
2052

2053
        # Create final result
2054
        if len(remdims) == 0:
3✔
2055
            return sum(new_weights)
3✔
2056
        else:
2057
            factor_matrices = []
3✔
2058
            for i in remdims:
3✔
2059
                factor_matrices.append(self.factor_matrices[i])
3✔
2060
            return ktensor.from_data(new_weights, factor_matrices)
3✔
2061

2062
    def update(self, modes, data):
3✔
2063
        """
2064
        Updates a :class:`pyttb.ktensor` in the specific dimensions with the
2065
        values in `data` (in vector or matrix form). The value of `modes` must
2066
        be a value in [-1,...,self.ndoms]. If the Further, the number of elements in
2067
        `data` must equal self.shape[modes] * self.ncomponents. The update is
2068
        performed in place.
2069

2070
        Parameters
2071
        ----------
2072
        modes: int or :class:`list` of int, required
2073
            List of dimensions to update; values must be in ascending order. If
2074
            the first element of the list is -1, then update the weights. All
2075
            other integer values values must be sorted and in
2076
            [0,...,self.ndims].
2077
        data: :class:`numpy.ndarray`, required
2078
            Data values to use in the update.
2079

2080
        Results
2081
        -------
2082
        :class:`pyttb.ktensor`
2083

2084
        Examples
2085
        --------
2086
        Create a :class:`pyttb.ktensor` of all ones:
2087

2088
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
2089

2090
        Create vectors for updating various factor matrices of the
2091
        :class:`pyttb.ktensor`:
2092

2093
        >>> vec0 = 2 * np.ones(K.shape[0] * K.ncomponents)
2094
        >>> vec1 = 3 * np.ones(K.shape[1] * K.ncomponents)
2095
        >>> vec2 = 4 * np.ones(K.shape[2] * K.ncomponents)
2096

2097
        Update a single factor matrix:
2098

2099
        >>> K1 = K.copy()
2100
        >>> K1 = K1.update(0, vec0)
2101
        >>> print(K1)
2102
        ktensor of shape 2 x 3 x 4
2103
        weights=[1. 1.]
2104
        factor_matrices[0] =
2105
        [[2. 2.]
2106
         [2. 2.]]
2107
        factor_matrices[1] =
2108
        [[1. 1.]
2109
         [1. 1.]
2110
         [1. 1.]]
2111
        factor_matrices[2] =
2112
        [[1. 1.]
2113
         [1. 1.]
2114
         [1. 1.]
2115
         [1. 1.]]
2116

2117
        Update all factor matrices:
2118

2119
        >>> K2 = K.copy()
2120
        >>> vec_all = np.concatenate((vec0, vec1, vec2))
2121
        >>> K2 = K2.update([0, 1, 2], vec_all)
2122
        >>> print(K2)
2123
        ktensor of shape 2 x 3 x 4
2124
        weights=[1. 1.]
2125
        factor_matrices[0] =
2126
        [[2. 2.]
2127
         [2. 2.]]
2128
        factor_matrices[1] =
2129
        [[3. 3.]
2130
         [3. 3.]
2131
         [3. 3.]]
2132
        factor_matrices[2] =
2133
        [[4. 4.]
2134
         [4. 4.]
2135
         [4. 4.]
2136
         [4. 4.]]
2137

2138
        Update some but not all factor matrices:
2139

2140
        >>> K3 = K.copy()
2141
        >>> vec_some = np.concatenate((vec0, vec2))
2142
        >>> K3 = K3.update([0, 2], vec_some)
2143
        >>> print(K3)
2144
        ktensor of shape 2 x 3 x 4
2145
        weights=[1. 1.]
2146
        factor_matrices[0] =
2147
        [[2. 2.]
2148
         [2. 2.]]
2149
        factor_matrices[1] =
2150
        [[1. 1.]
2151
         [1. 1.]
2152
         [1. 1.]]
2153
        factor_matrices[2] =
2154
        [[4. 4.]
2155
         [4. 4.]
2156
         [4. 4.]
2157
         [4. 4.]]
2158

2159
        """
2160
        if not isinstance(modes, int):
3✔
2161
            assert modes == sorted(modes), "Modes must be sorted in ascending order"
3✔
2162
        else:
2163
            modes = [modes]
3✔
2164

2165
        loc = 0  # Location in data array
3✔
2166
        for k in modes:
3✔
2167
            if k == -1:
3✔
2168
                # update weights
2169
                endloc = loc + self.ncomponents
3✔
2170
                if len(data) < endloc:
3✔
2171
                    assert False, "Data is too short"
3✔
2172
                self.weights = data[loc:endloc].copy()
3✔
2173
                loc = endloc
3✔
2174
            elif k < self.ndims:
3✔
2175
                # update factor matrix
2176
                endloc = loc + self.shape[k] * self.ncomponents
3✔
2177
                if len(data) < endloc:
3✔
2178
                    assert False, "Data is too short"
3✔
2179
                self.factor_matrices[k] = np.reshape(
3✔
2180
                    data[loc:endloc].copy(), (self.shape[k], self.ncomponents)
3✔
2181
                )
2182
                loc = endloc
3✔
2183
            else:
2184
                assert False, "Invalid mode: {}".format(k)
3✔
2185

2186
        ## Check that we used all the data
2187
        if not (loc == len(data)):
3✔
2188
            warnings.warn("Failed to consume all of the input data")
3✔
2189

2190
        return self
3✔
2191

2192
    def __add__(self, other):
3✔
2193
        """
2194
        Binary addition for :class:`pyttb.ktensor`.
2195

2196
        Parameters
2197
        ----------
2198
        other: :class:`pyttb.ktensor`, required
2199
            :class:`pyttb.ktensor` to add to `self`.
2200

2201
        Returns
2202
        -------
2203
        :class:`pyttb.ktensor`
2204
        """
2205
        # TODO include test of other as sumtensor and call sumtensor.__add__
2206
        if not isinstance(other, ktensor):
3✔
2207
            assert False, "Cannot add instance of this type to a ktensor"
3✔
2208

2209
        if self.shape != other.shape:
3✔
2210
            assert False, "Must be two ktensors of the same shape"
3✔
2211

2212
        weights = np.concatenate((self.weights, other.weights))
3✔
2213
        factor_matrices = []
3✔
2214
        for k in range(self.ndims):
3✔
2215
            factor_matrices.append(
3✔
2216
                np.concatenate(
3✔
2217
                    (self.factor_matrices[k], other.factor_matrices[k]), axis=1
3✔
2218
                )
2219
            )
2220
        return ktensor.from_data(weights, factor_matrices)
3✔
2221

2222
    def __getitem__(self, item):
3✔
2223
        """
2224
        Subscripted reference for a :class:`pyttb.ktensor`.
2225

2226
        Subscripted reference is used to query the components of a
2227
        :class:`pyttb.ktensor`.
2228

2229
        Parameters
2230
        ----------
2231
        item: tuple(int) or int, required
2232

2233
        Examples
2234
        --------
2235
        >>> K = ttb.ktensor.from_function(np.ones, (2, 3, 4), 2)
2236
        >>> K.weights
2237
        array([1., 1.])
2238
        >>> K.factor_matrices
2239
        [array([[1., 1.],
2240
               [1., 1.]]), array([[1., 1.],
2241
               [1., 1.],
2242
               [1., 1.]]), array([[1., 1.],
2243
               [1., 1.],
2244
               [1., 1.],
2245
               [1., 1.]])]
2246
        >>> K.factor_matrices[0]
2247
        array([[1., 1.],
2248
               [1., 1.]])
2249
        >>> K[0]
2250
        array([[1., 1.],
2251
               [1., 1.]])
2252
        >>> K[1, 2, 0]
2253
        2.0
2254
        >>> K[0][:, [0]]
2255
        array([[1.],
2256
               [1.]])
2257
        """
2258
        if isinstance(item, tuple):
3✔
2259
            if len(item) == self.ndims:
3✔
2260
                # Extract single element
2261
                a = 0
3✔
2262
                for k in range(self.ncomponents):
3✔
2263
                    b = self.weights[k]
3✔
2264
                    for i in range(self.ndims):
3✔
2265
                        b = b * self.factor_matrices[i][item[i], k]
3✔
2266
                    a = a + b
3✔
2267
                return a
3✔
2268
            else:
2269
                assert (
3✔
2270
                    False
3✔
2271
                ), "ktensor.__getitem__ requires tuples with {} elements".format(
3✔
2272
                    self.ndims
3✔
2273
                )
2274
        elif isinstance(item, (int, np.int_)) and item in range(self.ndims):
3✔
2275
            # Extract factor matrix
2276
            return self.factor_matrices[item].copy()
3✔
2277
        else:
2278
            assert (
3✔
2279
                False
3✔
2280
            ), "ktensor.__getitem__() can only extract single elements (tuple of indices) or factor matrices (single index)"
3✔
2281

2282
    def __neg__(self):
3✔
2283
        """
2284
        Unary minus (negative) for :class:`pyttb.ktensor` instances.
2285

2286
        Returns
2287
        -------
2288
        :class:`pyttb.ktensor`
2289
        """
2290
        return ktensor.from_data(-self.weights, self.factor_matrices)
3✔
2291

2292
    def __pos__(self):
3✔
2293
        """
2294
        Unary plus (positive) for :class:`pyttb.ktensor` instances.
2295

2296
        Returns
2297
        -------
2298
        :class:`pyttb.ktensor`
2299
        """
2300
        return ktensor.from_tensor_type(self)
3✔
2301

2302
    def __setitem__(self, key, value):
3✔
2303
        """
2304
        Subscripted assignment for :class:`pyttb.ktensor`.
2305

2306
        Subscripted assignment cannot be used to update individual elements of
2307
        a :class:`pyttb.ktensor`. You can update the weights vector or the
2308
        factor matrices of a :class:`pyttb.ktensor`.
2309

2310
        Example
2311
        -------
2312
        >>> K = ttb.ktensor.from_data(np.ones((4,1)), [np.random.random((2,4)), np.random.random((3,4)), np.random.random((4,4))])
2313
        >>> K.weights = 2 * np.ones((4,1))
2314
        >>> K.factor_matrices[0] = np.zeros((2, 4))
2315
        >>> K.factor_matrices = [np.zeros((2, 4)), np.zeros((3, 4)), np.zeros((4, 4))]
2316
        >>> print(K)
2317
        ktensor of shape 2 x 3 x 4
2318
        weights=[[2.]
2319
         [2.]
2320
         [2.]
2321
         [2.]]
2322
        factor_matrices[0] =
2323
        [[0. 0. 0. 0.]
2324
         [0. 0. 0. 0.]]
2325
        factor_matrices[1] =
2326
        [[0. 0. 0. 0.]
2327
         [0. 0. 0. 0.]
2328
         [0. 0. 0. 0.]]
2329
        factor_matrices[2] =
2330
        [[0. 0. 0. 0.]
2331
         [0. 0. 0. 0.]
2332
         [0. 0. 0. 0.]
2333
         [0. 0. 0. 0.]]
2334
        """
2335
        assert (
3✔
2336
            False
3✔
2337
        ), "Subscripted assignment cannot be used to update individual elements of a ktensor. However, you can update the weights vector or the factor matrices of a ktensor. The entire factor matrix or weight vector must be provided."
3✔
2338

2339
    def __sub__(self, other):
3✔
2340
        """
2341
        Binary subtraction for :class:`pyttb.ktensor`.
2342

2343
        Parameters
2344
        ----------
2345
        other: :class:`pyttb.ktensor`
2346

2347
        Returns
2348
        -------
2349
        :class:`pyttb.ktensor`
2350
        """
2351
        if not isinstance(other, ktensor):
3✔
2352
            assert False, "Cannot subtract instance of this type from a ktensor"
3✔
2353

2354
        if self.shape != other.shape:
3✔
2355
            assert False, "Must be two ktensors of the same shape"
3✔
2356

2357
        weights = np.concatenate((self.weights, -other.weights))
3✔
2358
        factor_matrices = []
3✔
2359
        for k in range(self.ndims):
3✔
2360
            factor_matrices.append(
3✔
2361
                np.concatenate(
3✔
2362
                    (self.factor_matrices[k], other.factor_matrices[k]), axis=1
3✔
2363
                )
2364
            )
2365
        return ktensor.from_data(weights, factor_matrices)
3✔
2366

2367
    def __mul__(self, other):
3✔
2368
        """
2369
        Elementwise (including scalar) multiplication for
2370
        :class:`pyttb.ktensor` instances.
2371

2372
        Parameters
2373
        ----------
2374
        other: :class:`pyttb.tensor`, :class:`pyttb.sptensor`, float, int
2375

2376
        Returns
2377
        -------
2378
        :class:`pyttb.ktensor`
2379
        """
2380
        if isinstance(other, (ttb.sptensor, ttb.tensor)):
3✔
2381
            return other.__mul__(self)
3✔
2382

2383
        if isinstance(other, (float, int)):
3✔
2384
            return ktensor.from_data(other * self.weights, self.factor_matrices)
3✔
2385

2386
        assert (
3✔
2387
            False
3✔
2388
        ), "Multiplication by ktensors only allowed for scalars, tensors, or sptensors"
3✔
2389

2390
    def __rmul__(self, other):
3✔
2391
        """
2392
        Elementwise (including scalar) multiplication for
2393
        :class:`pyttb.ktensor` instances.
2394

2395
        Parameters
2396
        ----------
2397
        other: :class:`pyttb.tensor`, :class:`pyttb.sptensor`, float, int
2398

2399
        Returns
2400
        -------
2401
        :class:`pyttb.ktensor`
2402
        """
2403
        return self.__mul__(other)
3✔
2404

2405
    def __repr__(self):
3✔
2406
        """
2407
        String representation of a :class:`pyttb.ktensor`.
2408

2409
        Returns
2410
        -------
2411
        str:
2412
        """
2413
        s = ""
3✔
2414
        s += "ktensor of shape "
3✔
2415
        s += (" x ").join([str(int(d)) for d in self.shape])
3✔
2416
        s += "\n"
3✔
2417
        s += "weights="
3✔
2418
        s += str(self.weights)
3✔
2419
        for i in range(len(self.factor_matrices)):
3✔
2420
            s += "\nfactor_matrices[{}] =\n".format(i)
3✔
2421
            s += str(self.factor_matrices[i])
3✔
2422
        return s
3✔
2423

2424
    __str__ = __repr__
3✔
2425

2426

2427
if __name__ == "__main__":
3✔
2428
    import doctest  # pragma: no cover
2429

2430
    doctest.testmod()  # pragma: no cover
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc