359 lines
10 KiB
Python
359 lines
10 KiB
Python
import pytest
|
|
import numpy as np
|
|
from numpy.testing import assert_allclose
|
|
from pytest import raises as assert_raises
|
|
from scipy import sparse
|
|
|
|
from scipy.sparse import csgraph
|
|
|
|
|
|
def check_int_type(mat):
|
|
return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype(
|
|
mat.dtype, np.uint
|
|
)
|
|
|
|
|
|
def test_laplacian_value_error():
|
|
for t in int, float, complex:
|
|
for m in ([1, 1],
|
|
[[[1]]],
|
|
[[1, 2, 3], [4, 5, 6]],
|
|
[[1, 2], [3, 4], [5, 5]]):
|
|
A = np.array(m, dtype=t)
|
|
assert_raises(ValueError, csgraph.laplacian, A)
|
|
|
|
|
|
def _explicit_laplacian(x, normed=False):
|
|
if sparse.issparse(x):
|
|
x = x.toarray()
|
|
x = np.asarray(x)
|
|
y = -1.0 * x
|
|
for j in range(y.shape[0]):
|
|
y[j,j] = x[j,j+1:].sum() + x[j,:j].sum()
|
|
if normed:
|
|
d = np.diag(y).copy()
|
|
d[d == 0] = 1.0
|
|
y /= d[:,None]**.5
|
|
y /= d[None,:]**.5
|
|
return y
|
|
|
|
|
|
def _check_symmetric_graph_laplacian(mat, normed, copy=True):
|
|
if not hasattr(mat, 'shape'):
|
|
mat = eval(mat, dict(np=np, sparse=sparse))
|
|
|
|
if sparse.issparse(mat):
|
|
sp_mat = mat
|
|
mat = sp_mat.toarray()
|
|
else:
|
|
sp_mat = sparse.csr_matrix(mat)
|
|
|
|
mat_copy = np.copy(mat)
|
|
sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True)
|
|
|
|
n_nodes = mat.shape[0]
|
|
explicit_laplacian = _explicit_laplacian(mat, normed=normed)
|
|
laplacian = csgraph.laplacian(mat, normed=normed, copy=copy)
|
|
sp_laplacian = csgraph.laplacian(sp_mat, normed=normed,
|
|
copy=copy)
|
|
|
|
if copy:
|
|
assert_allclose(mat, mat_copy)
|
|
_assert_allclose_sparse(sp_mat, sp_mat_copy)
|
|
else:
|
|
if not (normed and check_int_type(mat)):
|
|
assert_allclose(laplacian, mat)
|
|
if sp_mat.format == 'coo':
|
|
_assert_allclose_sparse(sp_laplacian, sp_mat)
|
|
|
|
assert_allclose(laplacian, sp_laplacian.toarray())
|
|
|
|
for tested in [laplacian, sp_laplacian.toarray()]:
|
|
if not normed:
|
|
assert_allclose(tested.sum(axis=0), np.zeros(n_nodes))
|
|
assert_allclose(tested.T, tested)
|
|
assert_allclose(tested, explicit_laplacian)
|
|
|
|
|
|
def test_symmetric_graph_laplacian():
|
|
symmetric_mats = (
|
|
'np.arange(10) * np.arange(10)[:, np.newaxis]',
|
|
'np.ones((7, 7))',
|
|
'np.eye(19)',
|
|
'sparse.diags([1, 1], [-1, 1], shape=(4, 4))',
|
|
'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()',
|
|
'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()',
|
|
'np.vander(np.arange(4)) + np.vander(np.arange(4)).T'
|
|
)
|
|
for mat in symmetric_mats:
|
|
for normed in True, False:
|
|
for copy in True, False:
|
|
_check_symmetric_graph_laplacian(mat, normed, copy)
|
|
|
|
|
|
def _assert_allclose_sparse(a, b, **kwargs):
|
|
# helper function that can deal with sparse matrices
|
|
if sparse.issparse(a):
|
|
a = a.toarray()
|
|
if sparse.issparse(b):
|
|
b = b.toarray()
|
|
assert_allclose(a, b, **kwargs)
|
|
|
|
|
|
def _check_laplacian_dtype_none(
|
|
A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
|
|
):
|
|
mat = arr_type(A, dtype=dtype)
|
|
L, d = csgraph.laplacian(
|
|
mat,
|
|
normed=normed,
|
|
return_diag=True,
|
|
use_out_degree=use_out_degree,
|
|
copy=copy,
|
|
dtype=None,
|
|
)
|
|
if normed and check_int_type(mat):
|
|
assert L.dtype == np.float64
|
|
assert d.dtype == np.float64
|
|
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
|
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
|
else:
|
|
assert L.dtype == dtype
|
|
assert d.dtype == dtype
|
|
desired_L = np.asarray(desired_L).astype(dtype)
|
|
desired_d = np.asarray(desired_d).astype(dtype)
|
|
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
|
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
|
|
|
if not copy:
|
|
if not (normed and check_int_type(mat)):
|
|
if type(mat) is np.ndarray:
|
|
assert_allclose(L, mat)
|
|
elif mat.format == "coo":
|
|
_assert_allclose_sparse(L, mat)
|
|
|
|
|
|
def _check_laplacian_dtype(
|
|
A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
|
|
):
|
|
mat = arr_type(A, dtype=dtype)
|
|
L, d = csgraph.laplacian(
|
|
mat,
|
|
normed=normed,
|
|
return_diag=True,
|
|
use_out_degree=use_out_degree,
|
|
copy=copy,
|
|
dtype=dtype,
|
|
)
|
|
assert L.dtype == dtype
|
|
assert d.dtype == dtype
|
|
desired_L = np.asarray(desired_L).astype(dtype)
|
|
desired_d = np.asarray(desired_d).astype(dtype)
|
|
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
|
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
|
|
|
if not copy:
|
|
if not (normed and check_int_type(mat)):
|
|
if type(mat) is np.ndarray:
|
|
assert_allclose(L, mat)
|
|
elif mat.format == 'coo':
|
|
_assert_allclose_sparse(L, mat)
|
|
|
|
|
|
INT_DTYPES = {np.intc, np.int_, np.longlong}
|
|
REAL_DTYPES = {np.single, np.double, np.longdouble}
|
|
COMPLEX_DTYPES = {np.csingle, np.cdouble, np.clongdouble}
|
|
# use sorted tuple to ensure fixed order of tests
|
|
DTYPES = tuple(sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str))
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@pytest.mark.parametrize("arr_type", [np.array,
|
|
sparse.csr_matrix,
|
|
sparse.coo_matrix])
|
|
@pytest.mark.parametrize("copy", [True, False])
|
|
@pytest.mark.parametrize("normed", [True, False])
|
|
@pytest.mark.parametrize("use_out_degree", [True, False])
|
|
def test_asymmetric_laplacian(use_out_degree, normed,
|
|
copy, dtype, arr_type):
|
|
# adjacency matrix
|
|
A = [[0, 1, 0],
|
|
[4, 2, 0],
|
|
[0, 0, 0]]
|
|
A = arr_type(np.array(A), dtype=dtype)
|
|
A_copy = A.copy()
|
|
|
|
if not normed and use_out_degree:
|
|
# Laplacian matrix using out-degree
|
|
L = [[1, -1, 0],
|
|
[-4, 4, 0],
|
|
[0, 0, 0]]
|
|
d = [1, 4, 0]
|
|
|
|
if normed and use_out_degree:
|
|
# normalized Laplacian matrix using out-degree
|
|
L = [[1, -0.5, 0],
|
|
[-2, 1, 0],
|
|
[0, 0, 0]]
|
|
d = [1, 2, 1]
|
|
|
|
if not normed and not use_out_degree:
|
|
# Laplacian matrix using in-degree
|
|
L = [[4, -1, 0],
|
|
[-4, 1, 0],
|
|
[0, 0, 0]]
|
|
d = [4, 1, 0]
|
|
|
|
if normed and not use_out_degree:
|
|
# normalized Laplacian matrix using in-degree
|
|
L = [[1, -0.5, 0],
|
|
[-2, 1, 0],
|
|
[0, 0, 0]]
|
|
d = [2, 1, 1]
|
|
|
|
_check_laplacian_dtype_none(
|
|
A,
|
|
L,
|
|
d,
|
|
normed=normed,
|
|
use_out_degree=use_out_degree,
|
|
copy=copy,
|
|
dtype=dtype,
|
|
arr_type=arr_type,
|
|
)
|
|
|
|
_check_laplacian_dtype(
|
|
A_copy,
|
|
L,
|
|
d,
|
|
normed=normed,
|
|
use_out_degree=use_out_degree,
|
|
copy=copy,
|
|
dtype=dtype,
|
|
arr_type=arr_type,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil',
|
|
'dok', 'dia', 'bsr'])
|
|
@pytest.mark.parametrize("normed", [True, False])
|
|
@pytest.mark.parametrize("copy", [True, False])
|
|
def test_sparse_formats(fmt, normed, copy):
|
|
mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt)
|
|
_check_symmetric_graph_laplacian(mat, normed, copy)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
|
|
)
|
|
@pytest.mark.parametrize("form", ["array", "function", "lo"])
|
|
def test_laplacian_symmetrized(arr_type, form):
|
|
# adjacency matrix
|
|
n = 3
|
|
mat = arr_type(np.arange(n * n).reshape(n, n))
|
|
L_in, d_in = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
form=form,
|
|
)
|
|
L_out, d_out = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
use_out_degree=True,
|
|
form=form,
|
|
)
|
|
Ls, ds = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
symmetrized=True,
|
|
form=form,
|
|
)
|
|
Ls_normed, ds_normed = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
symmetrized=True,
|
|
normed=True,
|
|
form=form,
|
|
)
|
|
mat += mat.T
|
|
Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form)
|
|
Lss_normed, dss_normed = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
normed=True,
|
|
form=form,
|
|
)
|
|
|
|
assert_allclose(ds, d_in + d_out)
|
|
assert_allclose(ds, dss)
|
|
assert_allclose(ds_normed, dss_normed)
|
|
|
|
d = {}
|
|
for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]:
|
|
if form == "array":
|
|
d[L] = eval(L)
|
|
else:
|
|
d[L] = eval(L)(np.eye(n, dtype=mat.dtype))
|
|
|
|
_assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T)
|
|
_assert_allclose_sparse(d["Ls"], d["Lss"])
|
|
_assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
|
|
)
|
|
@pytest.mark.parametrize("dtype", DTYPES)
|
|
@pytest.mark.parametrize("normed", [True, False])
|
|
@pytest.mark.parametrize("symmetrized", [True, False])
|
|
@pytest.mark.parametrize("use_out_degree", [True, False])
|
|
@pytest.mark.parametrize("form", ["function", "lo"])
|
|
def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form):
|
|
n = 3
|
|
mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]]
|
|
mat = arr_type(np.array(mat), dtype=dtype)
|
|
Lo, do = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
normed=normed,
|
|
symmetrized=symmetrized,
|
|
use_out_degree=use_out_degree,
|
|
dtype=dtype,
|
|
)
|
|
La, da = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
normed=normed,
|
|
symmetrized=symmetrized,
|
|
use_out_degree=use_out_degree,
|
|
dtype=dtype,
|
|
form="array",
|
|
)
|
|
assert_allclose(do, da)
|
|
_assert_allclose_sparse(Lo, La)
|
|
|
|
L, d = csgraph.laplacian(
|
|
mat,
|
|
return_diag=True,
|
|
normed=normed,
|
|
symmetrized=symmetrized,
|
|
use_out_degree=use_out_degree,
|
|
dtype=dtype,
|
|
form=form,
|
|
)
|
|
assert_allclose(d, do)
|
|
assert d.dtype == dtype
|
|
Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype)
|
|
_assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7)
|
|
x = np.arange(6).reshape(3, 2)
|
|
if not (normed and dtype in INT_DTYPES):
|
|
assert_allclose(L(x), Lo @ x)
|
|
else:
|
|
# Normalized Lo is casted to integer, but L() is not
|
|
pass
|
|
|
|
|
|
def test_format_error_message():
|
|
with pytest.raises(ValueError, match="Invalid form: 'toto'"):
|
|
_ = csgraph.laplacian(np.eye(1), form='toto')
|