Unverified Commit bb7b3c6f authored by czkkkkkk's avatar czkkkkkk Committed by GitHub
Browse files

[Sparse] Add SparseMatrix unittest and fix docstring problem (#4627)



* [Sparse] Add SparseMatrix unittest and fix docstring problem

* Minor fix

* Update

* check permission

* rm future annonations

* Skip create_from_csr and create_from_csc tests because Pytorch 1.9.0 does not have torch.sparse_csr_tensor
Co-authored-by: default avatarIsrat Nisa <nisisrat@amazon.com>
parent 2accfc5d
"""dgl diagonal matrix module.""" """DGL diagonal matrix module."""
from typing import Optional, Tuple from typing import Optional, Tuple
import torch import torch
from .sp_matrix import SparseMatrix, create_from_coo from .sp_matrix import SparseMatrix, create_from_coo
class DiagMatrix: class DiagMatrix:
"""Diagonal Matrix Class """Diagonal Matrix Class
...@@ -23,21 +24,27 @@ class DiagMatrix: ...@@ -23,21 +24,27 @@ class DiagMatrix:
shape : tuple[int, int] shape : tuple[int, int]
Shape of the matrix. Shape of the matrix.
""" """
def __init__(self, val: torch.Tensor, shape: Optional[Tuple[int, int]] = None):
def __init__(
self, val: torch.Tensor, shape: Optional[Tuple[int, int]] = None
):
len_val = len(val) len_val = len(val)
if shape is not None: if shape is not None:
assert len_val == min(shape), \ assert len_val == min(shape), (
f'Expect len(val) to be min(shape), got {len_val} for len(val) and {shape} for shape.' f"Expect len(val) to be min(shape), got {len_val} for len(val)"
"and {shape} for shape."
)
else: else:
shape = (len_val, len_val) shape = (len_val, len_val)
self.val = val self.val = val
self.shape = shape self.shape = shape
def __repr__(self): def __repr__(self):
return f'DiagMatrix(val={self.val}, \nshape={self.shape})' return f"DiagMatrix(val={self.val}, \nshape={self.shape})"
def __call__(self, x: torch.Tensor): def __call__(self, x: torch.Tensor):
"""Create a new diagonal matrix with the same shape as self but different values. """Create a new diagonal matrix with the same shape as self
but different values.
Parameters Parameters
---------- ----------
...@@ -128,7 +135,7 @@ class DiagMatrix: ...@@ -128,7 +135,7 @@ class DiagMatrix:
return self.transpose() return self.transpose()
@property @property
def T(self): # pylint: disable=C0103 def T(self): # pylint: disable=C0103
"""Alias of :meth:`transpose()`""" """Alias of :meth:`transpose()`"""
return self.transpose() return self.transpose()
...@@ -152,7 +159,10 @@ class DiagMatrix: ...@@ -152,7 +159,10 @@ class DiagMatrix:
""" """
return DiagMatrix(self.val, self.shape[::-1]) return DiagMatrix(self.val, self.shape[::-1])
def diag(val: torch.Tensor, shape: Optional[Tuple[int, int]] = None) -> DiagMatrix:
def diag(
val: torch.Tensor, shape: Optional[Tuple[int, int]] = None
) -> DiagMatrix:
"""Create a diagonal matrix based on the diagonal values """Create a diagonal matrix based on the diagonal values
Parameters Parameters
...@@ -200,10 +210,13 @@ def diag(val: torch.Tensor, shape: Optional[Tuple[int, int]] = None) -> DiagMatr ...@@ -200,10 +210,13 @@ def diag(val: torch.Tensor, shape: Optional[Tuple[int, int]] = None) -> DiagMatr
# NOTE(Mufei): this may not be needed if DiagMatrix is simple enough # NOTE(Mufei): this may not be needed if DiagMatrix is simple enough
return DiagMatrix(val, shape) return DiagMatrix(val, shape)
def identity(shape: Tuple[int, int],
d: Optional[int] = None, def identity(
dtype: Optional[torch.dtype] = None, shape: Tuple[int, int],
device: Optional[torch.device] = None) -> DiagMatrix: d: Optional[int] = None,
dtype: Optional[torch.dtype] = None,
device: Optional[torch.device] = None,
) -> DiagMatrix:
"""Create a diagonal matrix with ones on the diagonal and zeros elsewhere """Create a diagonal matrix with ones on the diagonal and zeros elsewhere
Parameters Parameters
...@@ -251,14 +264,11 @@ def identity(shape: Tuple[int, int], ...@@ -251,14 +264,11 @@ def identity(shape: Tuple[int, int],
Case3: 3-by-3 matrix with tensor diagonal values Case3: 3-by-3 matrix with tensor diagonal values
>>> mat = identity(shape=(3, 3), d=2) >>> mat = identity(shape=(3, 3), d=2)
>>> mat.val >>> print(mat)
tensor([[1., 1.], DiagMatrix(val=tensor([[1., 1.],
[1., 1.], [1., 1.],
[1., 1.]]) [1., 1.]]),
>>> mat.shape shape=(3, 3))
(3, 3)
>>> mat.nnz
3
""" """
len_val = min(shape) len_val = min(shape)
if d is None: if d is None:
......
"""dgl sparse matrix module.""" """DGL sparse matrix module."""
from typing import Optional, Tuple from typing import Optional, Tuple
import torch import torch
__all__ = ['SparseMatrix', 'create_from_coo', 'create_from_csr', 'create_from_csc'] __all__ = [
"SparseMatrix",
"create_from_coo",
"create_from_csr",
"create_from_csc",
]
class SparseMatrix: class SparseMatrix:
r'''Class for sparse matrix. r"""Class for sparse matrix.
Parameters Parameters
---------- ----------
...@@ -28,30 +34,31 @@ class SparseMatrix: ...@@ -28,30 +34,31 @@ class SparseMatrix:
>>> dst = torch.tensor([2, 4, 3]) >>> dst = torch.tensor([2, 4, 3])
>>> val = torch.tensor([1, 1, 1]) >>> val = torch.tensor([1, 1, 1])
>>> A = SparseMatrix(src, dst, val) >>> A = SparseMatrix(src, dst, val)
>>> A.shape >>> print(A)
(3,5) SparseMatrix(indices=tensor([[1, 1, 2],
>>> A.row [2, 4, 3]]),
tensor([1, 1, 2]) values=tensor([1, 1, 1]),
>>> A.val shape=(3, 5), nnz=3)
tensor([1., 1., 1.])
>>> A.nnz
3
Case2: Sparse matrix with row indices, col indices and values (vector). Case2: Sparse matrix with row indices, col indices and values (vector).
>>> ...
>>> val = torch.tensor([[1, 1], [2, 2], [3, 3]]) >>> val = torch.tensor([[1, 1], [2, 2], [3, 3]])
>>> A = SparseMatrix(src, dst, val) >>> A = SparseMatrix(src, dst, val)
>>> A.val >>> print(A)
tensor([[1, 1], SparseMatrix(indices=tensor([[1, 1, 2],
[2, 4, 3]]),
values=tensor([[1, 1],
[2, 2], [2, 2],
[3, 3]]) [3, 3]]),
''' shape=(3, 5), nnz=3)
def __init__(self, """
def __init__(
self,
row: torch.Tensor, row: torch.Tensor,
col: torch.Tensor, col: torch.Tensor,
val: Optional[torch.Tensor] = None, val: Optional[torch.Tensor] = None,
shape : Optional[Tuple[int, int]] = None shape: Optional[Tuple[int, int]] = None,
): ):
if val is None: if val is None:
val = torch.ones(row.shape[0]) val = torch.ones(row.shape[0])
...@@ -112,7 +119,7 @@ class SparseMatrix: ...@@ -112,7 +119,7 @@ class SparseMatrix:
return self.adj.device return self.adj.device
@property @property
def row(self) -> torch.tensor: def row(self) -> torch.Tensor:
"""Get the row indices of the nonzero elements. """Get the row indices of the nonzero elements.
Returns Returns
...@@ -123,7 +130,7 @@ class SparseMatrix: ...@@ -123,7 +130,7 @@ class SparseMatrix:
return self.adj.indices()[0] return self.adj.indices()[0]
@property @property
def col(self) -> torch.tensor: def col(self) -> torch.Tensor:
"""Get the column indices of the nonzero elements. """Get the column indices of the nonzero elements.
Returns Returns
...@@ -134,7 +141,7 @@ class SparseMatrix: ...@@ -134,7 +141,7 @@ class SparseMatrix:
return self.adj.indices()[1] return self.adj.indices()[1]
@property @property
def val(self) -> torch.tensor: def val(self) -> torch.Tensor:
"""Get the values of the nonzero elements. """Get the values of the nonzero elements.
Returns Returns
...@@ -145,16 +152,18 @@ class SparseMatrix: ...@@ -145,16 +152,18 @@ class SparseMatrix:
return self.adj.values() return self.adj.values()
@val.setter @val.setter
def val(self, x) -> torch.tensor: def val(self, x: torch.Tensor) -> torch.Tensor:
"""Set the values of the nonzero elements.""" """Set the values of the nonzero elements."""
assert len(x) == self.nnz assert len(x) == self.nnz
if len(x.shape) == 1: if len(x.shape) == 1:
shape = self.shape shape = self.shape
else: else:
shape = self.shape + (x.shape[-1],) shape = self.shape + (x.shape[-1],)
self.adj = torch.sparse_coo_tensor(self.adj.indices(), x, shape).coalesce() self.adj = torch.sparse_coo_tensor(
self.adj.indices(), x, shape
).coalesce()
def __call__(self, x): def __call__(self, x: torch.Tensor):
"""Create a new sparse matrix with the same sparsity as self but different values. """Create a new sparse matrix with the same sparsity as self but different values.
Parameters Parameters
...@@ -171,7 +180,9 @@ class SparseMatrix: ...@@ -171,7 +180,9 @@ class SparseMatrix:
assert len(x) == self.nnz assert len(x) == self.nnz
return SparseMatrix(self.row, self.col, x, shape=self.shape) return SparseMatrix(self.row, self.col, x, shape=self.shape)
def indices(self, fmt, return_shuffle=False) -> Tuple[torch.tensor, ...]: def indices(
self, fmt: str, return_shuffle=False
) -> Tuple[torch.Tensor, ...]:
"""Get the indices of the nonzero elements. """Get the indices of the nonzero elements.
Parameters Parameters
...@@ -186,12 +197,12 @@ class SparseMatrix: ...@@ -186,12 +197,12 @@ class SparseMatrix:
tensor tensor
Indices of the nonzero elements Indices of the nonzero elements
""" """
if fmt == 'COO' and not return_shuffle: if fmt == "COO" and not return_shuffle:
return self.adj.indices() return self.adj.indices()
else: else:
raise NotImplementedError raise NotImplementedError
def coo(self) -> Tuple[torch.tensor, ...]: def coo(self) -> Tuple[torch.Tensor, ...]:
"""Get the coordinate (COO) representation of the sparse matrix. """Get the coordinate (COO) representation of the sparse matrix.
Returns Returns
...@@ -201,7 +212,7 @@ class SparseMatrix: ...@@ -201,7 +212,7 @@ class SparseMatrix:
""" """
return self return self
def csr(self) -> Tuple[torch.tensor, ...]: def csr(self) -> Tuple[torch.Tensor, ...]:
"""Get the CSR (Compressed Sparse Row) representation of the sparse matrix. """Get the CSR (Compressed Sparse Row) representation of the sparse matrix.
Returns Returns
...@@ -211,7 +222,7 @@ class SparseMatrix: ...@@ -211,7 +222,7 @@ class SparseMatrix:
""" """
return self return self
def csc(self) -> Tuple[torch.tensor, ...]: def csc(self) -> Tuple[torch.Tensor, ...]:
"""Get the CSC (Compressed Sparse Column) representation of the sparse matrix. """Get the CSC (Compressed Sparse Column) representation of the sparse matrix.
Returns Returns
...@@ -221,7 +232,7 @@ class SparseMatrix: ...@@ -221,7 +232,7 @@ class SparseMatrix:
""" """
return self return self
def dense(self) -> torch.tensor: def dense(self) -> torch.Tensor:
"""Get the dense representation of the sparse matrix. """Get the dense representation of the sparse matrix.
Returns Returns
...@@ -236,7 +247,7 @@ class SparseMatrix: ...@@ -236,7 +247,7 @@ class SparseMatrix:
return self.transpose() return self.transpose()
@property @property
def T(self): # pylint: disable=C0103 def T(self): # pylint: disable=C0103
"""Alias of :meth:`transpose()`""" """Alias of :meth:`transpose()`"""
return self.transpose() return self.transpose()
...@@ -264,10 +275,13 @@ class SparseMatrix: ...@@ -264,10 +275,13 @@ class SparseMatrix:
""" """
return SparseMatrix(self.col, self.row, self.val, self.shape[::-1]) return SparseMatrix(self.col, self.row, self.val, self.shape[::-1])
def create_from_coo(row: torch.Tensor,
col: torch.Tensor, def create_from_coo(
val: Optional[torch.Tensor] = None, row: torch.Tensor,
shape: Optional[Tuple[int, int]] = None) -> SparseMatrix: col: torch.Tensor,
val: Optional[torch.Tensor] = None,
shape: Optional[Tuple[int, int]] = None,
) -> SparseMatrix:
"""Create a sparse matrix from row and column coordinates. """Create a sparse matrix from row and column coordinates.
Parameters Parameters
...@@ -324,10 +338,13 @@ def create_from_coo(row: torch.Tensor, ...@@ -324,10 +338,13 @@ def create_from_coo(row: torch.Tensor,
""" """
return SparseMatrix(row=row, col=col, val=val, shape=shape) return SparseMatrix(row=row, col=col, val=val, shape=shape)
def create_from_csr(indptr: torch.Tensor,
indices: torch.Tensor, def create_from_csr(
val: Optional[torch.Tensor] = None, indptr: torch.Tensor,
shape: Optional[Tuple[int, int]] = None) -> SparseMatrix: indices: torch.Tensor,
val: Optional[torch.Tensor] = None,
shape: Optional[Tuple[int, int]] = None,
) -> SparseMatrix:
"""Create a sparse matrix from CSR indices. """Create a sparse matrix from CSR indices.
For row i of the sparse matrix For row i of the sparse matrix
...@@ -366,38 +383,49 @@ def create_from_csr(indptr: torch.Tensor, ...@@ -366,38 +383,49 @@ def create_from_csr(indptr: torch.Tensor,
>>> indptr = torch.tensor([0, 1, 2, 5]) >>> indptr = torch.tensor([0, 1, 2, 5])
>>> indices = torch.tensor([1, 2, 0, 1, 2]) >>> indices = torch.tensor([1, 2, 0, 1, 2])
>>> A = create_from_csr(indptr, indices) >>> A = create_from_csr(indptr, indices)
>>> A >>> print(A)
SparseMatrix(indices=tensor([[0, 1, 2, 2, 2], SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[1, 2, 0, 1, 2]]), [1, 2, 0, 1, 2]]),
values=tensor([1., 1., 1., 1., 1.]), values=tensor([1., 1., 1., 1., 1.]),
shape=(3, 3), nnz=5) shape=(3, 3), nnz=5)
>>> # Specify shape >>> # Specify shape
>>> A = create_from_csr(indptr, indices, shape=(5, 3)) >>> A = create_from_csr(indptr, indices, shape=(5, 3))
>>> A.shape >>> print(A)
(5, 3) SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[1, 2, 0, 1, 2]]),
values=tensor([1., 1., 1., 1., 1.]),
shape=(5, 3), nnz=5)
Case2: Sparse matrix with scalar/vector values. Following example is with Case2: Sparse matrix with scalar/vector values. Following example is with
vector data. vector data.
>>> val = torch.tensor([[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]) >>> val = torch.tensor([[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> A = create_from_csr(indptr, indices, val) >>> A = create_from_csr(indptr, indices, val)
>>> A.val >>> print(A)
tensor([[1, 1], SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[1, 2, 0, 1, 2]]),
values=tensor([[1, 1],
[2, 2], [2, 2],
[3, 3], [3, 3],
[4, 4], [4, 4],
[5, 5]]) [5, 5]]),
shape=(3, 3), nnz=5)
""" """
adj_csr = torch.sparse_csr_tensor(indptr, indices, torch.ones(indices.shape[0])) adj_csr = torch.sparse_csr_tensor(
indptr, indices, torch.ones(indices.shape[0])
)
adj_coo = adj_csr.to_sparse_coo().coalesce() adj_coo = adj_csr.to_sparse_coo().coalesce()
row, col = adj_coo.indices() row, col = adj_coo.indices()
return SparseMatrix(row=row, col=col, val=val, shape=shape) return SparseMatrix(row=row, col=col, val=val, shape=shape)
def create_from_csc(indptr: torch.Tensor,
indices: torch.Tensor, def create_from_csc(
val: Optional[torch.Tensor] = None, indptr: torch.Tensor,
shape: Optional[Tuple[int, int]] = None) -> SparseMatrix: indices: torch.Tensor,
val: Optional[torch.Tensor] = None,
shape: Optional[Tuple[int, int]] = None,
) -> SparseMatrix:
"""Create a sparse matrix from CSC indices. """Create a sparse matrix from CSC indices.
For column i of the sparse matrix For column i of the sparse matrix
...@@ -436,29 +464,37 @@ def create_from_csc(indptr: torch.Tensor, ...@@ -436,29 +464,37 @@ def create_from_csc(indptr: torch.Tensor,
>>> indptr = torch.tensor([0, 1, 3, 5]) >>> indptr = torch.tensor([0, 1, 3, 5])
>>> indices = torch.tensor([2, 0, 2, 1, 2]) >>> indices = torch.tensor([2, 0, 2, 1, 2])
>>> A = create_from_csc(indptr, indices) >>> A = create_from_csc(indptr, indices)
>>> A >>> print(A)
SparseMatrix(indices=tensor([[0, 1, 2, 2, 2], SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[1, 2, 0, 1, 2]]), [1, 2, 0, 1, 2]]),
values=tensor([1., 1., 1., 1., 1.]), values=tensor([1., 1., 1., 1., 1.]),
shape=(3, 3), nnz=5) shape=(3, 3), nnz=5)
>>> # Specify shape >>> # Specify shape
>>> A = create_from_csc(indptr, indices, shape=(5, 3)) >>> A = create_from_csc(indptr, indices, shape=(5, 3))
>>> A.shape >>> print(A)
(5, 3) SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[1, 2, 0, 1, 2]]),
values=tensor([1., 1., 1., 1., 1.]),
shape=(5, 3), nnz=5)
Case2: Sparse matrix with scalar/vector values. Following example is with Case2: Sparse matrix with scalar/vector values. Following example is with
vector data. vector data.
>>> val = torch.tensor([[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]) >>> val = torch.tensor([[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> A = create_from_csc(indptr, indices, val) >>> A = create_from_csc(indptr, indices, val)
>>> A.val >>> print(A)
tensor([[1, 1], SparseMatrix(indices=tensor([[0, 1, 2, 2, 2],
[2, 2], [1, 2, 0, 1, 2]]),
[3, 3], values=tensor([[2, 2],
[4, 4], [4, 4],
[5, 5]]) [1, 1],
[3, 3],
[5, 5]]),
shape=(3, 3), nnz=5)
""" """
adj_csr = torch.sparse_csr_tensor(indptr, indices, torch.ones(indices.shape[0])) adj_csr = torch.sparse_csr_tensor(
indptr, indices, torch.ones(indices.shape[0])
)
adj_coo = adj_csr.to_sparse_coo().coalesce() adj_coo = adj_csr.to_sparse_coo().coalesce()
col, row = adj_coo.indices() col, row = adj_coo.indices()
......
import pytest
import torch
from dgl.mock_sparse import create_from_coo, create_from_csr, create_from_csc
@pytest.mark.parametrize("dense_dim", [None, 4])
@pytest.mark.parametrize("row", [[0, 0, 1, 2], (0, 1, 2, 4)])
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
@pytest.mark.parametrize("mat_shape", [None, (3, 5), (5, 3)])
def test_create_from_coo(dense_dim, row, col, mat_shape):
# Skip invalid matrices
if mat_shape is not None and (
max(row) >= mat_shape[0] or max(col) >= mat_shape[1]
):
return
val_shape = (len(row),)
if dense_dim is not None:
val_shape += (dense_dim,)
val = torch.randn(val_shape)
row = torch.tensor(row)
col = torch.tensor(col)
mat = create_from_coo(row, col, val, mat_shape)
if mat_shape is None:
mat_shape = (torch.max(row).item() + 1, torch.max(col).item() + 1)
assert mat.shape == mat_shape
assert mat.nnz == row.numel()
assert mat.dtype == val.dtype
assert torch.allclose(mat.val, val)
assert torch.allclose(mat.row, row)
assert torch.allclose(mat.col, col)
@pytest.mark.skip(reason="no way of currently testing this")
@pytest.mark.parametrize("dense_dim", [None, 4])
@pytest.mark.parametrize("indptr", [[0, 0, 1, 4], (0, 1, 2, 4)])
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
@pytest.mark.parametrize("mat_shape", [None, (3, 5)])
def test_create_from_csr(dense_dim, indptr, indices, mat_shape):
val_shape = (len(indices),)
if dense_dim is not None:
val_shape += (dense_dim,)
val = torch.randn(val_shape)
indptr = torch.tensor(indptr)
indices = torch.tensor(indices)
mat = create_from_csr(indptr, indices, val, mat_shape)
if mat_shape is None:
mat_shape = (indptr.numel() - 1, torch.max(indices).item() + 1)
assert mat.device == val.device
assert mat.shape == mat_shape
assert mat.nnz == indices.numel()
assert mat.dtype == val.dtype
assert torch.allclose(mat.val, val)
deg = torch.diff(indptr)
row = torch.repeat_interleave(torch.arange(deg.numel()), deg)
assert torch.allclose(mat.row, row)
col = indices
assert torch.allclose(mat.col, col)
@pytest.mark.skip(reason="no way of currently testing this")
@pytest.mark.parametrize("dense_dim", [None, 4])
@pytest.mark.parametrize("indptr", [[0, 0, 1, 4], (0, 1, 2, 4)])
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
@pytest.mark.parametrize("mat_shape", [None, (5, 3)])
def test_create_from_csc(dense_dim, indptr, indices, mat_shape):
val_shape = (len(indices),)
if dense_dim is not None:
val_shape += (dense_dim,)
val = torch.randn(val_shape)
indptr = torch.tensor(indptr)
indices = torch.tensor(indices)
mat = create_from_csc(indptr, indices, val, mat_shape)
if mat_shape is None:
mat_shape = (torch.max(indices).item() + 1, indptr.numel() - 1)
assert mat.device == val.device
assert mat.shape == mat_shape
assert mat.nnz == indices.numel()
assert mat.dtype == val.dtype
assert torch.allclose(mat.val, val)
row = indices
assert torch.allclose(mat.row, row)
deg = torch.diff(indptr)
col = torch.repeat_interleave(torch.arange(deg.numel()), deg)
assert torch.allclose(mat.col, col)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment