#
# This file is part of TensorToolbox.
#
# TensorToolbox is free software: you can redistribute it and/or modify
# it under the terms of the LGNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# TensorToolbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# LGNU Lesser General Public License for more details.
#
# You should have received a copy of the LGNU Lesser General Public License
# along with TensorToolbox. If not, see <http://www.gnu.org/licenses/>.
#
# DTU UQ Library
# Copyright (C) 2014 The Technical University of Denmark
# Scientific Computing Section
# Department of Applied Mathematics and Computer Science
#
# Author: Daniele Bigoni
#
__all__ = ['NumericsError','ConvergenceError','TTcrossLoopError','idxunfold','idxfold','expand_idxs','matkron_to_mattensor','mat_to_tt_idxs','tt_to_mat_idxs','maxvol','lowrankapprox','reort']
import operator
import itertools
import random
import numpy as np
import numpy.linalg as npla
import scipy.linalg as scla
class NumericsError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class ConvergenceError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class TTcrossLoopError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
[docs]def matkron_to_mattensor(A,nrows,ncols):
""" This function reshapes a 2D-matrix obtained as kron product of len(nrows)==len(ncols) matrices, to a len(nrows)-tensor that can be used as input for the TTmat constructor. Applies the Van Loan-Pitsianis reordering of the matrix elements.
:param ndarray A: 2-dimensional matrix
:param list,int nrows,ncols: number of rows and number of columns of the original matrices used for the kron product
"""
if A.ndim != 2: raise NameError("TensorToolbox.core.matkron_to_mattensor: The input ndarray has the wrong number of dimension. 2d-ndarray required.")
if A.shape[0] != np.prod(nrows) or A.shape[1] != np.prod(ncols): raise NameError("TensorToolbox.core.matkron_to_mattensor: dimension of A not consistent with nrows or ncols")
if isinstance(nrows,int) and isinstance(ncols,int):
nrows = [nrows]
ncols = [ncols]
if len(nrows) != len(ncols): raise NameError("TensorToolbox.core.matkron_to_mattensor: len(nrows)!=len(ncols)")
d = len(nrows)
dims = list(nrows)
dims.extend(ncols)
# Prepare interleaved idxs
idxs = [[i,d+i] for i in range(d)]
idxs = list(itertools.chain(*idxs))
# Reshape and re-order
A = A.reshape(dims)
A = np.transpose(A,axes=idxs)
A = A.reshape([nrows[i]*ncols[i] for i in range(d)])
return A
[docs]def idxunfold(dlist,idxs):
""" Find the index corresponding to the unfolded (flat) version of a tensor
:param list,int dlist: list of integers containing the dimensions of the tensor
:param list,int idxs: tensor index
:returns: index for the flatten tensor
"""
# Check whether index out of bounds
import operator
if any(map(operator.ge,idxs,dlist)):
raise NameError("TensorToolbox.core.idxunfold: Index out of bounds")
n = len(dlist)
plist = [1]
for i in range(n-1,0,-1):
plist.append( plist[-1] * dlist[i] )
ii = 0
for i,p in enumerate(reversed(plist)):
ii += p * idxs[i]
return ii
[docs]def idxfold(dlist,idx):
""" Find the index corresponding to the folded version of a tensor from the flatten version
:param list,int dlist: list of integers containing the dimensions of the tensor
:param int idx: tensor flatten index
:returns: list of int -- the index for the folded version
:note: this routine can be used to get the indexes of a TTmat from indices of a matkron (matrix obtained using np.kron): (i,j) \in N^d x N^d -> ((i_1,..,i_d),(j_1,..,j_d)) \in (N x .. x N) x (N x .. x N)
"""
n = len(dlist)
cc = [1]
for val in reversed(dlist): cc.append( cc[-1] * val )
if idx >= cc[-1]: raise NameError("TensorToolbox.core.idxunfold: Index out of bounds")
ii = []
tmp = idx
for i in range(n):
ii.append( tmp//cc[n-i-1] )
tmp = tmp % cc[n-i-1]
return tuple(ii)
[docs]def expand_idxs(idxs_in,shape,full_shape,fix_dims=[],fix_idxs=None):
""" From a tuple of indicies, apply all the unslicing transformations necessary in order to extract values from a tensor.
:param tuple idxs_in: indexing tuple. The admissible slicing format is the same used in np.ndarray.
:param tuple shape: shape of the tensor
:param tuple full_shape: full shape of the tensor
:param list fix_dims: whether there are dimensions which had been fixed and need to be added.
:param list fix_idxs: fixed idxs for each fixed dimension.
:return: tuple ``(lidxs,list_idx,slice_idx,out_shape,transpose_list_shape)``. ``lidxs`` is an iterator of the indices. ``list_idx`` and ``slice_idx`` are the list of dimensions containing lists and the dimensions containing slices. ``out_shape`` is a tuple containing the shape of the output tensor. ``transpose_list_shape`` is a flag indicating whether the output format need to be transposed (behaving accordingly to np.ndarray).
"""
# Transform the tuple to a list for convinience
idxs_in = list(idxs_in)
# Slice notation can be used. Remember: slice(start:stop:step)
if len(idxs_in) != len(shape):
raise IndexError('wrong number of indices')
# Check that all the lists are of the same length
int_idx = []
llen = None
for i,idx in enumerate(idxs_in):
if isinstance(idx, int):
int_idx.append(i)
if isinstance(idx, list) or isinstance(idx,tuple):
idxs_in[i] = list(idx)
if llen == None:
llen = len(idx)
elif llen != len(idx):
raise IndexError('List of indices must have the same length.')
if llen == None: llen = 1
# Expand single indices in idxs_in to llen
for i in int_idx: idxs_in[i] = [idxs_in[i]] * llen
# Update input indices of slices and lists
list_idx_in = []
slice_idx_in = []
for i,idx in enumerate(idxs_in):
if isinstance(idx, list) or isinstance(idx,tuple):
list_idx_in.append(i)
if isinstance(idx, slice):
slice_idx_in.append(i)
# Insert fixed indices
for i in fix_dims:
idxs_in.insert(i, [fix_idxs[fix_dims.index(i)]] * llen)
# Construct list of indices which are lists and slices
list_idx = []
list_IDXs = []
slice_idx = []
slice_IDXs = []
out_shape = []
for i,idx in enumerate(idxs_in):
if isinstance(idx, list) or isinstance(idx,tuple):
list_idx.append(i)
list_IDXs.append( idx )
if isinstance(idx, slice):
slice_idx.append(i)
IDXs = range(idx.start if idx.start != None else 0,
idx.stop if idx.stop != None else full_shape[i],
idx.step if idx.step != None else 1)
slice_IDXs.append( IDXs )
out_shape.append(len(IDXs))
if len(list_idx) == 0: list_IDXs.append( [-1] ) # Ghost element added to make the full slicing work
unlistIdxs = itertools.izip(*list_IDXs)
transpose_list_shape = False
if llen > 1:
out_shape.insert(0,llen)
if len(slice_idx_in) > 0 and len(list_idx_in) > 0 and min(list_idx_in) > max(slice_idx_in):
transpose_list_shape = True
# Un-slice sliced idxs
unslicedIdxs = itertools.product(*slice_IDXs)
# Final list of indices (iterator)
lidxs = itertools.product(unlistIdxs, unslicedIdxs)
return (lidxs,list_idx,slice_idx,out_shape,transpose_list_shape)
[docs]def mat_to_tt_idxs(rowidxs,colidxs,nrows,ncols):
""" Mapping from the multidimensional matrix indexing to the tt matrix indexing
(rowidxs,colidxs) = ((i_1,...,i_d),(j_1,...,j_d)) -> (l_1,...,l_d)
:param tuple,int rowidxs,colidxs: list of row and column indicies. len(rowidxs) == len(colidxs)
:param tuple,int nrows,ncols: dimensions of matrices
:returns: tuple,int indices in the tt format
"""
if isinstance(rowidxs,int): rowidxs = (rowidxs,)
if isinstance(colidxs,int): colidxs = (colidxs,)
if isinstance(nrows,int): nrows = (nrows,)
if isinstance(ncols,int): ncols = (ncols,)
if not (len(rowidxs) == len(colidxs) == len(nrows) == len(ncols)):
raise NameError("TensorToolbox.core.mat_to_tt_idxs: not consistent dimensions in the input arguments")
import operator
if any(map(operator.ge,rowidxs,nrows)) or any(map(operator.ge,colidxs,ncols)):
raise NameError("TensorToolbox.core.mat_to_tt_idxs: Index out of bound")
return tuple(np.asarray(rowidxs) * np.asarray(ncols) + np.asarray(colidxs))
[docs]def tt_to_mat_idxs(idxs,nrows,ncols):
""" Mapping from the tt matrix indexing to the multidimensional matrix indexing
(l_1,...,l_d) -> (rowidxs,colidxs) = ((i_1,...,i_d),(j_1,...,j_d))
:param tuple,int idxs: list of tt indicies. len(idxs) == len(nrows) == len(ncols)
:param tuple,int nrows,ncols: dimensions of matrices
:returns: (rowidxs,colidxs) = ((i_1,..,i_d),(j_1,..,j_d)) indices in the matrix indexing
"""
if isinstance(idxs,int): idxs = (idxs,)
if isinstance(nrows,int): nrows = (nrows,)
if isinstance(ncols,int): ncols = (ncols,)
if not (len(idxs) == len(nrows) == len(ncols)):
raise NameError("TensorToolbox.core.tt_to_mat_idxs: not consistent dimensions in the input arguments")
import operator
if any(map(operator.ge,idxs,np.asarray(nrows)*np.asarray(ncols))):
raise NameError("TensorToolbox.core.tt_to_mat_idxs: Index out of bound")
return (tuple(np.asarray(idxs) // np.asarray(ncols)),tuple(np.asarray(idxs) % np.asarray(ncols)))
[docs]def maxvol(A,delta=1e-2,maxit=100):
""" Find the rxr submatrix of maximal volume in A(nxr), n>=r
:param ndarray A: two dimensional array with (n,r)=shape(A) where r<=n
:param float delta: stopping cirterion [default=1e-2]
:param int maxit: maximum number of iterations [default=100]
:returns: ``(I,AsqInv,it)`` where ``I`` is the list or rows of A forming the matrix with maximal volume, ``AsqInv`` is the inverse of the matrix with maximal volume and ``it`` is the number of iterations to convergence
:raises: raise exception if the dimension of A is r>n or if A is singular
:raises: ConvergenceError if convergence is not reached in maxit iterations
"""
(n,r) = A.shape
if r>n :
raise TypeError("TensorToolbox.core.maxvol: A(nxr) must be a thin matrix, i.e. n>=r")
# Find an arbitrary non-singular rxr matrix in A
(P,L,U) = scla.lu(A)
# Check singularity
if np.min(np.abs(np.diag(U))) < np.spacing(1):
raise NumericsError("TensorToolbox.core.maxvol: Matrix A is singular")
# Reorder A so that the non-singular matrix is on top
I = np.arange(n,dtype=int) # set of swapping indices
I = np.dot(P.astype(int).T,I)
# Select Asq the top square matrix
Asq = A[I[:r],:]
# Compute inverse of Asq: Asq^-1 = (PLU)^-1
LU = L[:r,:r] - np.eye(r) + U
AsqInv = scla.lu_solve((LU,np.arange(r)), np.eye(r))
# Compute B
B = np.dot(A[I,:],AsqInv)
# Find maximum and row
maxidx = np.argmax(np.abs(B))
maxB = np.abs(B).flatten()[maxidx]
(maxrow,maxcol) = (maxidx // r, maxidx % r)
it = 0
eps = 1.+ delta
while it < maxit and maxB > eps:
it += 1
# Update AsqInv
q = np.zeros((r,1),dtype=np.float64)
q[maxcol] = 1.
vT = A[[I[maxrow],],:] - A[[I[maxcol],],:]
AsqInv -= np.dot(np.dot(AsqInv,q), np.dot(vT,AsqInv)) / (1. + np.dot(vT,np.dot(AsqInv,q))) # Eq (8) in "How to find a good submatrix"
# Update B using Sherman-Woodbury-Morrison formula
Bj = B[:,[maxcol,]]
# Bj[maxcol,0] -= 1.
Bj[maxrow,0] += 1.
Bi = B[[maxrow,],:]
Bi[0,maxcol] -= 1.
B[r:,:] -= np.dot(Bj[r:],Bi)/B[maxrow,maxcol]
# Update index of maxvol matrix I
tmp = I[maxcol]
I[maxcol] = I[maxrow]
I[maxrow] = tmp
# # Manual update TO BE REMOVED
# AsqInv = npla.inv(A[I[:r],:])
# B = np.dot(A[I,:], AsqInv)
# Find new maximum in B
maxidx = np.argmax(np.abs(B))
maxB = np.abs(B).flatten()[maxidx]
(maxrow,maxcol) = (maxidx // r, maxidx % r)
if maxB > eps:
raise ConvergenceError('Maxvol algorithm did not converge.')
# Return max-vol submatrix Asq
return (list(I[:r]),AsqInv,it)
[docs]def lowrankapprox(A, r, Jinit=None, delta=1e-5, maxit=100, maxvoleps=1e-2, maxvolit=100):
""" Given a matrix A nxm, find the maximum volume submatrix with rank r<n,m.
:param ndarray A: two dimensional array with dimension nxm
:param int r: rank of the maxvol submatrix
:param list Jinit: list of integers containing the r starting columns. If ``None`` then pick them randomly.
:param float delta: accuracy parameter
:param int maxit: maximum number of iterations in the lowrankapprox routine
:param float maxvoleps: accuracy parameter for each usage of the maxvol algorithm
:parma int maxvolit: maximum number of iterations in the maxvol routine
:return: ``(I,J,AsqInv,it)`` where ``I`` and ``J`` are the list of rows and columns of A that compose the submatrix of maximal volume, ``AsqInv`` is the inverse of such matrix and ``it`` is the number of iteration to convergence
"""
import random
(n,m) = A.shape
if r>n or r>m:
raise AttributeError('Rank r bigger than shape of A')
# Pick first column indices J at random
if Jinit == None:
J = random.sample(range(m),r)
else:
J = Jinit
if len(J) != r:
raise AttributeError('Invalid number of init columns: len(J) != r')
J.sort()
Aold = np.ones((n,m))
Anew = np.zeros((n,m))
it = 0
while it < maxit and npla.norm( Anew-Aold, 'fro' ) > delta * npla.norm(Anew,'fro'):
it += 1
# Row cycle
R = A[:,J]
if r == 1: R = np.reshape(R,(len(R),1))
# QR decomposition
(Q,R) = scla.qr(R,mode='economic')
# Maxvol
(I,QsqInv,it) = maxvol(Q,maxvoleps,maxvolit)
# Column cycle
C = A[I,:].T
if r == 1: C = np.reshape(C,(len(C),1))
# QR decomposition
(Q,R) = scla.qr(C,mode='economic')
# Maxvol
(J,QsqInv,it) = maxvol(Q,maxvoleps,maxvolit)
# New approximation
Aold = Anew
Atmp = A[:,J]
if r == 1: Atmp = np.reshape(Atmp,(len(Atmp),1))
Anew = np.dot(Atmp, np.dot(Q, QsqInv).T)
if npla.norm( Anew-Aold, 'fro' ) > delta * npla.norm(Anew,'fro'):
raise ConvergenceError('Low Rank Approximation algorithm did not converge.')
if np.min(np.abs(np.diag(R))) <= np.spacing(1):
raise ConvergenceError('Low Rank Approximation algorithm converged to a singular solution. Rank r over-estimated.')
# Compute AsqInv
AsqInv = scla.solve_triangular(R,QsqInv).T
return (I,J,AsqInv,it)
[docs]def reort(u,uadd):
""" Golub-Kahan reorthogonalization
.. note: See Oseledets' TT-Toolbox
"""
if uadd.shape[1] == 0 or u.shape[0] == u.shape[1]:
return u
if u.shape[1] + uadd.shape[1] >= u.shape[0]:
uadd = uadd[:, :u.shape[0]-u.shape[1]]
radd = uadd.shape[1]
mvr = np.dot( u.T, uadd)
unew = uadd - np.dot( u, mvr )
reort_flag = True
while reort_flag:
reort_flag = False
j = 1
norm_unew = np.sum(unew**2., axis=0)
norm_uadd = np.sum(uadd**2., axis=0)
reort_flag = (0 < len([ True for (nn,na) in itertools.product(norm_unew,norm_uadd) if nn <= .25 * na]))
[unew,_] = scla.qr(unew,mode='economic')
if reort_flag:
su = np.dot( u.T, unew )
uadd = unew.copy()
unew -= np.dot(u, su)
return np.hstack( (u,unew) )