# Source code for sknetwork.linalg.sparse_lowrank

```#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Apr 19 2019
@author: Nathan de Lara <nathan.delara@polytechnique.org>
"""
from typing import Union, Tuple

import numpy as np
from scipy import sparse
from scipy.sparse.linalg import LinearOperator

[docs]class SparseLR(LinearOperator):
"""Class for matrices with "sparse + low rank" structure.
Example:

:math:`A + xy^T`

Parameters
----------
sparse_mat: scipy.spmatrix
Sparse component. Is converted to csr format automatically.
low_rank_tuples: list
Single tuple of arrays of list of tuples, representing the low rank components [(x1, y1), (x2, y2),...].
Each low rank component is of the form :math:`xy^T`.

Examples
--------
>>> from scipy import sparse
>>> from sknetwork.linalg import SparseLR
>>> adjacency = sparse.eye(2, format='csr')
>>> slr = SparseLR(adjacency, (np.ones(2), np.ones(2)))
>>> x = np.ones(2)
>>> slr.dot(x)
array([3., 3.])
>>> slr.sum(axis=0)
array([3., 3.])
>>> slr.sum(axis=1)
array([3., 3.])
>>> slr.sum()
6.0

References
----------
De Lara (2019). `The Sparse + Low Rank trick for Matrix Factorization-Based Graph Algorithms.
<http://www.mlgworkshop.org/2019/papers/MLG2019_paper_1.pdf>`_
Proceedings of the 15th International Workshop on Mining and Learning with Graphs (MLG).
"""
def __init__(self, sparse_mat: Union[sparse.csr_matrix, sparse.csc_matrix], low_rank_tuples: Union[list, Tuple],
dtype=float):
n_row, n_col = sparse_mat.shape
self.sparse_mat = sparse_mat.tocsr().astype(dtype)
super(SparseLR, self).__init__(dtype=dtype, shape=(n_row, n_col))

if isinstance(low_rank_tuples, Tuple):
low_rank_tuples = [low_rank_tuples]
self.low_rank_tuples = []
for x, y in low_rank_tuples:
if x.shape == (n_row,) and y.shape == (n_col,):
self.low_rank_tuples.append((x.astype(self.dtype), y.astype(self.dtype)))
else:
raise ValueError('For each low rank tuple, x (resp. y) should be a vector of length {} (resp. {})'
.format(n_row, n_col))

def __neg__(self):
return SparseLR(-self.sparse_mat, [(-x, y) for (x, y) in self.low_rank_tuples])

def __add__(self, other: 'SparseLR'):
if type(other) == sparse.csr_matrix:
return SparseLR(self.sparse_mat + other, self.low_rank_tuples)
else:
return SparseLR(self.sparse_mat + other.sparse_mat, self.low_rank_tuples + other.low_rank_tuples)

def __sub__(self, other):

def __mul__(self, other):
return SparseLR(other * self.sparse_mat, [(other * x, y) for (x, y) in self.low_rank_tuples])

def _matvec(self, matrix: np.ndarray):
"""Right dot product with a dense matrix.

Parameters
----------
matrix:
Matrix.

Returns
-------
Dot product as a dense array
"""
prod = self.sparse_mat.dot(matrix)
if len(matrix.shape) == 1:
for (x, y) in self.low_rank_tuples:
prod += x * matrix.dot(y)
else:
transposed = matrix.T
for (x, y) in self.low_rank_tuples:
prod += x[:, np.newaxis].dot(transposed.dot(y)[:, np.newaxis].T)
return prod

def _transpose(self):
"""Transposed operator."""
transposed_sparse = sparse.csr_matrix(self.sparse_mat.T)
transposed_tuples = [(y, x) for (x, y) in self.low_rank_tuples]
return SparseLR(transposed_sparse, transposed_tuples)

return self.transpose()

[docs]    def left_sparse_dot(self, matrix: sparse.csr_matrix):
"""Left dot product with a sparse matrix."""
return SparseLR(matrix.dot(self.sparse_mat), [(matrix.dot(x), y) for (x, y) in self.low_rank_tuples])

[docs]    def right_sparse_dot(self, matrix: sparse.csr_matrix):
"""Right dot product with a sparse matrix."""
return SparseLR(self.sparse_mat.dot(matrix), [(x, matrix.T.dot(y)) for (x, y) in self.low_rank_tuples])

[docs]    def sum(self, axis=None):
"""Row-wise, column-wise or total sum of operator's coefficients.

Parameters
----------
axis :
If 0, return column-wise sum. If 1, return row-wise sum. Otherwise, return total sum.
"""
if axis == 0:
s = self.T.dot(np.ones(self.shape[0]))
elif axis == 1:
s = self.dot(np.ones(self.shape[1]))
else:
s = self.dot(np.ones(self.shape[1])).sum()
return s

[docs]    def astype(self, dtype: Union[str, np.dtype]):
"""Change dtype of the object."""
self.sparse_mat = self.sparse_mat.astype(dtype)
self.low_rank_tuples = [(x.astype(dtype), y.astype(dtype)) for (x, y) in self.low_rank_tuples]
self.dtype = np.dtype(dtype)

return self
```