# -*- coding: utf-8 -*-

"""
Test suite for the graphs module of the pygsp2 package.

"""

from __future__ import division

import os
import random
import sys
import unittest

import numpy as np
import scipy.linalg
from scipy import sparse
import networkx as nx
import graph_tool as gt
import graph_tool.generation
from skimage import data, img_as_float

from pygsp2 import graphs


class TestCase(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls._G = graphs.Logo()
        cls._G.compute_fourier_basis()
        cls._G.compute_differential_operator()

        cls._rng = np.random.default_rng(42)
        cls._signal = cls._rng.uniform(size=cls._G.N)

        cls._img = img_as_float(data.camera()[::16, ::16])

    @classmethod
    def tearDownClass(cls):
        pass

    def test_graph(self):
        adjacency = [
            [0., 3., 0., 2.],
            [3., 0., 4., 0.],
            [0., 4., 0., 5.],
            [2., 0., 5., 0.],
        ]

        # Input types.
        G = graphs.Graph(adjacency)
        self.assertIs(type(G.W), sparse.csr_matrix)
        adjacency = np.array(adjacency)
        G = graphs.Graph(adjacency)
        self.assertIs(type(G.W), sparse.csr_matrix)
        adjacency = sparse.coo_matrix(adjacency)
        G = graphs.Graph(adjacency)
        self.assertIs(type(G.W), sparse.csr_matrix)
        adjacency = sparse.csr_matrix(adjacency)
        # G = graphs.Graph(adjacency)
        # self.assertIs(G.W, adjacency)  # Not copied if already CSR.

        # Attributes.
        np.testing.assert_allclose(G.W.toarray(), adjacency.toarray())
        np.testing.assert_allclose(G.A.toarray(), G.W.toarray() > 0)
        np.testing.assert_allclose(G.d, np.array([2, 2, 2, 2]))
        np.testing.assert_allclose(G.dw, np.array([5, 7, 9, 7]))
        self.assertEqual(G.n_vertices, 4)
        self.assertIs(G.N, G.n_vertices)
        self.assertEqual(G.n_edges, 4)
        self.assertIs(G.Ne, G.n_edges)

        # Errors and warnings.
        self.assertRaises(ValueError, graphs.Graph, np.ones((3, 4)))
        self.assertRaises(ValueError, graphs.Graph, np.ones((3, 3, 4)))
        self.assertRaises(ValueError, graphs.Graph, [[0, np.nan], [0, 0]])
        self.assertRaises(ValueError, graphs.Graph, [[0, np.inf], [0, 0]])
        with self.assertLogs(level='WARNING'):
            graphs.Graph([[0, -1], [-1, 0]])
        with self.assertLogs(level='WARNING'):
            graphs.Graph([[1, 1], [1, 0]])
        for attr in ['A', 'd', 'dw', 'lmax', 'U', 'e', 'coherence', 'D']:
            # FIXME: The Laplacian L should be there as well.
            self.assertRaises(AttributeError, setattr, G, attr, None)
            self.assertRaises(AttributeError, delattr, G, attr)

    def test_degree(self):
        graph = graphs.Graph([
            [0, 1, 0],
            [1, 0, 2],
            [0, 2, 0],
        ])
        self.assertEqual(graph.is_directed(), False)
        np.testing.assert_allclose(graph.d, [1, 2, 1])
        np.testing.assert_allclose(graph.dw, [1, 3, 2])
        graph = graphs.Graph([
            [0, 1, 0],
            [0, 0, 2],
            [0, 2, 0],
        ])
        self.assertEqual(graph.is_directed(), True)
        np.testing.assert_allclose(graph.d, [0.5, 1.5, 1])
        np.testing.assert_allclose(graph.dw, [0.5, 2.5, 2])

    def test_is_connected(self):
        graph = graphs.Graph([
            [0, 1, 0],
            [1, 0, 2],
            [0, 2, 0],
        ])
        self.assertEqual(graph.is_directed(), False)
        self.assertEqual(graph.is_connected(), True)
        graph = graphs.Graph([
            [0, 1, 0],
            [1, 0, 0],
            [0, 2, 0],
        ])
        self.assertEqual(graph.is_directed(), True)
        self.assertEqual(graph.is_connected(), False)
        graph = graphs.Graph([
            [0, 1, 0],
            [1, 0, 0],
            [0, 0, 0],
        ])
        self.assertEqual(graph.is_directed(), False)
        self.assertEqual(graph.is_connected(), False)
        graph = graphs.Graph([
            [0, 1, 0],
            [0, 0, 2],
            [3, 0, 0],
        ])
        self.assertEqual(graph.is_directed(), True)
        self.assertEqual(graph.is_connected(), True)

    def test_is_directed(self):
        graph = graphs.Graph([
            [0, 3, 0, 0],
            [3, 0, 4, 0],
            [0, 4, 0, 2],
            [0, 0, 2, 0],
        ])
        assert graph.W.nnz == 6
        self.assertEqual(graph.is_directed(), False)
        # In-place modification is not allowed anymore.
        # graph.W[0, 1] = 0
        # assert graph.W.nnz == 6
        # self.assertEqual(graph.is_directed(recompute=True), True)
        # graph.W[1, 0] = 0
        # assert graph.W.nnz == 6
        # self.assertEqual(graph.is_directed(recompute=True), False)

    def test_laplacian(self):

        G = graphs.Graph([
            [0, 3, 0, 1],
            [3, 0, 1, 0],
            [0, 1, 0, 3],
            [1, 0, 3, 0],
        ])
        laplacian = np.array([
            [+4, -3, +0, -1],
            [-3, +4, -1, +0],
            [+0, -1, +4, -3],
            [-1, +0, -3, +4],
        ])
        self.assertFalse(G.is_directed())
        G.compute_laplacian('combinatorial')
        np.testing.assert_allclose(G.L.toarray(), laplacian)
        G.compute_laplacian('normalized')
        np.testing.assert_allclose(G.L.toarray(), laplacian/4)

        G = graphs.Graph([
            [0, 6, 0, 1],
            [0, 0, 0, 0],
            [0, 2, 0, 3],
            [1, 0, 3, 0],
        ])
        self.assertTrue(G.is_directed())
        G.compute_laplacian('combinatorial')
        np.testing.assert_allclose(G.L.toarray(), laplacian)
        G.compute_laplacian('normalized')
        np.testing.assert_allclose(G.L.toarray(), laplacian/4)

        def test_combinatorial(G):
            np.testing.assert_equal(G.L.toarray(), G.L.T.toarray())
            np.testing.assert_equal(G.L.sum(axis=0), 0)
            np.testing.assert_equal(G.L.sum(axis=1), 0)
            np.testing.assert_equal(G.L.diagonal(), G.dw)

        def test_normalized(G):
            np.testing.assert_equal(G.L.toarray(), G.L.T.toarray())
            np.testing.assert_equal(G.L.diagonal(), 1)

        G = graphs.ErdosRenyi(100, directed=False)
        self.assertFalse(G.is_directed())
        G.compute_laplacian(lap_type='combinatorial')
        test_combinatorial(G)
        G.compute_laplacian(lap_type='normalized')
        test_normalized(G)

        G = graphs.ErdosRenyi(100, directed=True)
        self.assertTrue(G.is_directed())
        G.compute_laplacian(lap_type='combinatorial')
        test_combinatorial(G)
        G.compute_laplacian(lap_type='normalized')
        test_normalized(G)

    def test_estimate_lmax(self):

        graph = graphs.Sensor()
        self.assertRaises(ValueError, graph.estimate_lmax, method='unk')

        def check_lmax(graph, lmax):
            graph.estimate_lmax(method='bounds')
            np.testing.assert_allclose(graph.lmax, lmax)
            graph.estimate_lmax(method='lanczos')
            np.testing.assert_allclose(graph.lmax, lmax*1.01)
            graph.compute_fourier_basis()
            np.testing.assert_allclose(graph.lmax, lmax)

        # Full graph (bound is tight).
        n_nodes, value = 10, 2
        adjacency = np.full((n_nodes, n_nodes), value)
        graph = graphs.Graph(adjacency, lap_type='combinatorial')
        check_lmax(graph, lmax=value*n_nodes)

        # Regular bipartite graph (bound is tight).
        adjacency = [
            [0, 0, 1, 1],
            [0, 0, 1, 1],
            [1, 1, 0, 0],
            [1, 1, 0, 0],
        ]
        graph = graphs.Graph(adjacency, lap_type='combinatorial')
        check_lmax(graph, lmax=4)

        # Bipartite graph (bound is tight).
        adjacency = [
            [0, 0, 1, 1],
            [0, 0, 1, 0],
            [1, 1, 0, 0],
            [1, 0, 0, 0],
        ]
        graph = graphs.Graph(adjacency, lap_type='normalized')
        check_lmax(graph, lmax=2)

    def test_fourier_basis(self):
        # Smallest eigenvalue close to zero.
        np.testing.assert_allclose(self._G.e[0], 0, atol=1e-12)
        # First eigenvector is constant.
        N = self._G.N
        np.testing.assert_allclose(self._G.U[:, 0], np.sqrt(N) / N)
        # Control eigenvector direction.
        # assert (self._G.U[0, :] > 0).all()
        # Spectrum bounded by [0, 2] for the normalized Laplacian.
        G = graphs.Logo(lap_type='normalized')
        n = G.N // 2
        # check partial eigendecomposition
        G.compute_fourier_basis(n_eigenvectors=n)
        assert len(G.e) == n
        assert G.U.shape[1] == n
        assert G.e[-1] < 2
        U = G.U
        e = G.e
        # check full eigendecomposition
        G.compute_fourier_basis()
        assert len(G.e) == G.N
        assert G.U.shape[1] == G.N
        assert G.e[-1] < 2
        # eigsh might flip a sign
        np.testing.assert_allclose(np.abs(U), np.abs(G.U[:, :n]),
                                   atol=1e-12)
        np.testing.assert_allclose(e, G.e[:n])

    def test_eigendecompositions(self):
        G = graphs.Logo()
        U1, e1, V1 = scipy.linalg.svd(G.L.toarray())
        U2, e2, V2 = np.linalg.svd(G.L.toarray())
        e3, U3 = np.linalg.eig(G.L.toarray())
        e4, U4 = scipy.linalg.eig(G.L.toarray())
        e5, U5 = np.linalg.eigh(G.L.toarray())
        e6, U6 = scipy.linalg.eigh(G.L.toarray())

        def correct_sign(U):
            signs = np.sign(U[0, :])
            signs[signs == 0] = 1
            return U * signs
        U1 = correct_sign(U1)
        U2 = correct_sign(U2)
        U3 = correct_sign(U3)
        U4 = correct_sign(U4)
        U5 = correct_sign(U5)
        U6 = correct_sign(U6)
        V1 = correct_sign(V1.T)
        V2 = correct_sign(V2.T)

        inds3 = np.argsort(e3)[::-1]
        inds4 = np.argsort(e4)[::-1]
        np.testing.assert_allclose(e2, e1)
        np.testing.assert_allclose(e3[inds3], e1, atol=1e-12)
        np.testing.assert_allclose(e4[inds4], e1, atol=1e-12)
        np.testing.assert_allclose(e5[::-1], e1, atol=1e-12)
        np.testing.assert_allclose(e6[::-1], e1, atol=1e-12)
        np.testing.assert_allclose(U2, U1, atol=1e-12)
        np.testing.assert_allclose(V1, U1, atol=1e-12)
        np.testing.assert_allclose(V2, U1, atol=1e-12)
        np.testing.assert_allclose(U3[:, inds3], U1, atol=1e-10)
        np.testing.assert_allclose(U4[:, inds4], U1, atol=1e-10)
        np.testing.assert_allclose(U5[:, ::-1], U1, atol=1e-10)
        np.testing.assert_allclose(U6[:, ::-1], U1, atol=1e-10)

    def test_fourier_transform(self):
        s = self._rng.uniform(size=(self._G.N, 99, 21))
        s_hat = self._G.gft(s)
        s_star = self._G.igft(s_hat)
        np.testing.assert_allclose(s, s_star, rtol=1e-6)

    def test_edge_list(self):
        for directed in [False, True]:
            G = graphs.ErdosRenyi(100, directed=directed)
            sources, targets, weights = G.get_edge_list()
            if not directed:
                self.assertTrue(np.all(sources <= targets))
            edges = np.arange(G.n_edges)
            np.testing.assert_equal(G.W[sources[edges], targets[edges]],
                                    weights[edges][np.newaxis, :])

    def test_differential_operator(self, n_vertices=98):
        r"""The Laplacian must always be the divergence of the gradient,
        whether the Laplacian is combinatorial or normalized, and whether the
        graph is directed or weighted."""
        def test_incidence_nx(graph):
            r"""Test that the incidence matrix corresponds to NetworkX."""
            incidence_pg = np.sign(graph.D.toarray())
            G = nx.DiGraph if graph.is_directed() else nx.Graph
            graph_nx = nx.from_scipy_sparse_matrix(graph.W, create_using=G)
            incidence_nx = nx.incidence_matrix(graph_nx, oriented=True)
            np.testing.assert_equal(incidence_pg, incidence_nx.toarray())
        for graph in [graphs.Graph(np.zeros((n_vertices, n_vertices))),
                      graphs.Graph(np.identity(n_vertices)),
                      graphs.Graph([[0, 0.8], [0.8, 0]]),
                      graphs.Graph([[1.3, 0], [0.4, 0.5]]),
                      graphs.ErdosRenyi(n_vertices, directed=False, seed=42),
                      graphs.ErdosRenyi(n_vertices, directed=True, seed=42)]:
            for lap_type in ['combinatorial', 'normalized']:
                graph.compute_laplacian(lap_type)
                graph.compute_differential_operator()
                L = graph.D.dot(graph.D.T)
                np.testing.assert_allclose(L.toarray(), graph.L.toarray())
                test_incidence_nx(graph)

    def test_difference(self):
        for lap_type in ['combinatorial', 'normalized']:
            G = graphs.Logo(lap_type=lap_type)
            y = G.grad(self._signal)
            self.assertEqual(len(y), G.n_edges)
            z = G.div(y)
            self.assertEqual(len(z), G.n_vertices)
            np.testing.assert_allclose(z, G.L.dot(self._signal))

    def test_dirichlet_energy(self, n_vertices=100):
        r"""The Dirichlet energy is defined as the norm of the gradient."""
        signal = np.random.default_rng(42).uniform(size=n_vertices)
        for lap_type in ['combinatorial', 'normalized']:
            graph = graphs.BarabasiAlbert(n_vertices, lap_type=lap_type)
            graph.compute_differential_operator()
            energy = graph.dirichlet_energy(signal)
            grad_norm = np.sum(graph.grad(signal)**2)
            np.testing.assert_allclose(energy, grad_norm)

    def test_empty_graph(self, n_vertices=11):
        """Empty graphs have either no edge, or self-loops only. The Laplacian
        doesn't see self-loops, as the gradient on those edges is always zero.
        """
        adjacencies = [
            np.zeros((n_vertices, n_vertices)),
            np.identity(n_vertices),
        ]
        for adjacency, n_edges in zip(adjacencies, [0, n_vertices]):
            graph = graphs.Graph(adjacency)
            self.assertEqual(graph.n_vertices, n_vertices)
            self.assertEqual(graph.n_edges, n_edges)
            self.assertEqual(graph.W.nnz, n_edges)
            for laplacian in ['combinatorial', 'normalized']:
                graph.compute_laplacian(laplacian)
                self.assertEqual(graph.L.nnz, 0)
                sources, targets, weights = graph.get_edge_list()
                self.assertEqual(len(sources), n_edges)
                self.assertEqual(len(targets), n_edges)
                self.assertEqual(len(weights), n_edges)
                graph.compute_differential_operator()
                self.assertEqual(graph.D.nnz, 0)
                graph.compute_fourier_basis()
                np.testing.assert_allclose(graph.U, np.identity(n_vertices))
                np.testing.assert_allclose(graph.e, np.zeros(n_vertices))
            # NetworkX uses the same conventions.
            G = nx.from_scipy_sparse_matrix(graph.W)
            self.assertEqual(nx.laplacian_matrix(G).nnz, 0)
            self.assertEqual(nx.normalized_laplacian_matrix(G).nnz, 0)
            self.assertEqual(nx.incidence_matrix(G).nnz, 0)

    def test_adjacency_types(self, n_vertices=10):

        rng = np.random.default_rng(42)
        W = 10 * np.abs(rng.normal(size=(n_vertices, n_vertices)))
        W = W + W.T
        W = W - np.diag(np.diag(W))

        def test(adjacency):
            G = graphs.Graph(adjacency)
            G.compute_laplacian('combinatorial')
            G.compute_laplacian('normalized')
            G.estimate_lmax()
            G.compute_fourier_basis()
            G.compute_differential_operator()

        test(W)
        test(W.astype(np.float32))
        test(W.astype(int))
        test(sparse.csr_matrix(W))
        test(sparse.csr_matrix(W, dtype=np.float32))
        test(sparse.csr_matrix(W, dtype=int))
        test(sparse.csc_matrix(W))
        test(sparse.coo_matrix(W))

    def test_set_signal(self, name='test'):
        signal = np.zeros(self._G.n_vertices)
        self._G.set_signal(signal, name)
        self.assertIs(self._G.signals[name], signal)
        signal = np.zeros(self._G.n_vertices // 2)
        self.assertRaises(ValueError, self._G.set_signal, signal, name)

    def test_set_coordinates(self):
        G = graphs.FullConnected()
        coords = self._rng.uniform(size=(G.N, 2))
        G.set_coordinates(coords)
        G.set_coordinates('ring2D')
        G.set_coordinates('random2D')
        G.set_coordinates('random3D')
        G.set_coordinates('spring')
        G.set_coordinates('spring', dim=3)
        G.set_coordinates('spring', dim=3, pos=G.coords)
        G.set_coordinates('laplacian_eigenmap2D')
        G.set_coordinates('laplacian_eigenmap3D')
        self.assertRaises(AttributeError, G.set_coordinates, 'community2D')
        G = graphs.Community()
        G.set_coordinates('community2D')
        self.assertRaises(ValueError, G.set_coordinates, 'invalid')

    def test_line_graph(self):
        adjacency = [
            [0, 1, 1, 3],
            [1, 0, 1, 0],
            [1, 1, 0, 1],
            [3, 0, 1, 0],
        ]
        coords = [
            [0, 0],
            [4, 0],
            [4, 2],
            [0, 2],
        ]
        graph = graphs.Graph(adjacency, coords=coords)
        graph = graphs.LineGraph(graph)
        adjacency = [
            [0, 1, 1, 1, 0],
            [1, 0, 1, 1, 1],
            [1, 1, 0, 0, 1],
            [1, 1, 0, 0, 1],
            [0, 1, 1, 1, 0],
        ]
        coords = [
            [2, 0],
            [2, 1],
            [0, 1],
            [4, 1],
            [2, 2],
        ]
        np.testing.assert_equal(graph.W.toarray(), adjacency)
        np.testing.assert_equal(graph.coords, coords)
        with self.assertLogs(level='WARNING'):
            graphs.LineGraph(graphs.Graph([[0, 2], [2, 0]]))

    def test_subgraph(self, n_vertices=100):
        self._G.set_signal(self._G.coords, 'coords')
        graph = self._G.subgraph(range(n_vertices))
        self.assertEqual(graph.n_vertices, n_vertices)
        self.assertEqual(graph.coords.shape, (n_vertices, 2))
        self.assertEqual(graph.signals['coords'].shape, (n_vertices, 2))
        self.assertIs(graph.lap_type, self._G.lap_type)
        self.assertEqual(graph.plotting, self._G.plotting)

    def test_nngraph(self, n_vertices=30):
        rng = np.random.default_rng(42)
        Xin = rng.normal(size=(n_vertices, 3))
        dist_types = ['euclidean', 'manhattan', 'max_dist', 'minkowski']

        for dist_type in dist_types:

            # Only p-norms with 1<=p<=infinity permitted.
            if dist_type != 'minkowski':
                graphs.NNGraph(Xin, NNtype='radius', dist_type=dist_type)
                graphs.NNGraph(Xin, NNtype='knn', dist_type=dist_type)

            # Distance type unsupported in the C bindings,
            # use the C++ bindings instead.
            if dist_type != 'max_dist':
                graphs.NNGraph(Xin, use_flann=True, NNtype='knn',
                               dist_type=dist_type)

    def test_bunny(self):
        graphs.Bunny()

    def test_cube(self):
        graphs.Cube()
        graphs.Cube(nb_dim=2)

    def test_sphere(self):
        graphs.Sphere()

    def test_twomoons(self):
        graphs.TwoMoons(moontype='standard')
        graphs.TwoMoons(moontype='synthesized')

    def test_torus(self):
        graphs.Torus()

    def test_comet(self, n=100, k=10):
        graph = graphs.Comet(n, k)
        self.assertEqual(graph.n_vertices, n)
        self.assertEqual(graph.n_edges, n-1)
        self.assertEqual(graph.dw[0], k)
        graph = graphs.Comet(7, 4)
        adjacency = [
            [0, 1, 1, 1, 1, 0, 0],  # center
            [1, 0, 0, 0, 0, 0, 0],  # branch
            [1, 0, 0, 0, 0, 0, 0],  # branch
            [1, 0, 0, 0, 0, 0, 0],  # branch
            [1, 0, 0, 0, 0, 1, 0],  # tail
            [0, 0, 0, 0, 1, 0, 1],  # tail
            [0, 0, 0, 0, 0, 1, 0],  # tail
        ]
        np.testing.assert_array_equal(graph.W.toarray(), adjacency)
        coords = [[0, 0], [0, 1], [-1, 0], [0, -1], [1, 0], [2, 0], [3, 0]]
        np.testing.assert_allclose(graph.coords, coords, atol=1e-10)
        # Comet generalizes Path.
        g1 = graphs.Comet(n, 0)
        g2 = graphs.Path(n)
        np.testing.assert_array_equal(g1.W.toarray(), g2.W.toarray())
        # Comet generalizes Star.
        g1 = graphs.Comet(n, n-1)
        g2 = graphs.Star(n)
        np.testing.assert_array_equal(g1.W.toarray(), g2.W.toarray())

    def test_star(self, n=20):
        graph = graphs.Star(n)
        self.assertEqual(graph.n_vertices, n)
        self.assertEqual(graph.n_edges, n-1)
        np.testing.assert_array_equal(graph.d, [n-1] + (n-1) * [1])
        np.testing.assert_allclose(np.linalg.norm(graph.coords[1:], axis=1), 1)

    def test_lowstretchtree(self):
        graphs.LowStretchTree()

    def test_randomregular(self):
        k = 6
        G = graphs.RandomRegular(k=k)
        np.testing.assert_equal(G.W.sum(0), k)
        np.testing.assert_equal(G.W.sum(1), k)

    def test_ring(self):
        graphs.Ring()
        graphs.Ring(N=32, k=16)
        self.assertRaises(ValueError, graphs.Ring, 2)
        self.assertRaises(ValueError, graphs.Ring, 5, k=3)

    def test_community(self):
        graphs.Community()
        graphs.Community(comm_density=0.2)
        graphs.Community(k_neigh=5)
        graphs.Community(N=100, Nc=3, comm_sizes=[20, 50, 30])

    def test_minnesota(self):
        graphs.Minnesota()

    def test_sensor(self):
        graphs.Sensor(3000)
        graphs.Sensor(N=100, distributed=True)
        self.assertRaises(ValueError, graphs.Sensor, N=101, distributed=True)
        graphs.Sensor(N=101, distributed=False)
        graphs.Sensor(seed=10)
        graphs.Sensor(k=20)

    def test_stochasticblockmodel(self):
        graphs.StochasticBlockModel(N=100, directed=True)
        graphs.StochasticBlockModel(N=100, directed=False)
        graphs.StochasticBlockModel(N=100, self_loops=True)
        graphs.StochasticBlockModel(N=100, self_loops=False)
        graphs.StochasticBlockModel(N=100, connected=True, seed=42)
        graphs.StochasticBlockModel(N=100, connected=False)
        self.assertRaises(ValueError, graphs.StochasticBlockModel,
                          N=100, p=0, q=0, connected=True)

    def test_airfoil(self):
        graphs.Airfoil()

    def test_davidsensornet(self):
        graphs.DavidSensorNet()
        graphs.DavidSensorNet(N=500)
        graphs.DavidSensorNet(N=128)

    def test_erdosreny(self):
        graphs.ErdosRenyi(N=100, connected=False, directed=False)
        graphs.ErdosRenyi(N=100, connected=False, directed=True)
        graphs.ErdosRenyi(N=100, connected=True, directed=False, seed=42)
        graphs.ErdosRenyi(N=100, connected=True, directed=True, seed=42)
        G = graphs.ErdosRenyi(N=100, p=1, self_loops=True)
        self.assertEqual(G.W.nnz, 100**2)

    def test_fullconnected(self):
        graphs.FullConnected()

    def test_logo(self):
        graphs.Logo()

    def test_path(self, n=5):
        graph = graphs.Path(n, directed=False)
        adjacency = [
            [0, 1, 0, 0, 0],
            [1, 0, 1, 0, 0],
            [0, 1, 0, 1, 0],
            [0, 0, 1, 0, 1],
            [0, 0, 0, 1, 0],
        ]
        coords = [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]]
        np.testing.assert_array_equal(graph.W.toarray(), adjacency)
        np.testing.assert_array_equal(graph.coords, coords)
        graph = graphs.Path(n, directed=True)
        adjacency = [
            [0, 1, 0, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 0, 1, 0],
            [0, 0, 0, 0, 1],
            [0, 0, 0, 0, 0],
        ]
        np.testing.assert_array_equal(graph.W.toarray(), adjacency)
        np.testing.assert_array_equal(graph.coords, coords)

    def test_randomring(self):
        graphs.RandomRing()
        G = graphs.RandomRing(angles=[0, 2, 1])
        self.assertEqual(G.N, 3)
        self.assertRaises(ValueError, graphs.RandomRing, 2)
        self.assertRaises(ValueError, graphs.RandomRing, angles=[0, 2])
        self.assertRaises(ValueError, graphs.RandomRing, angles=[0, 2, 7])
        self.assertRaises(ValueError, graphs.RandomRing, angles=[0, 2, -1])

    def test_swissroll(self):
        graphs.SwissRoll(srtype='uniform')
        graphs.SwissRoll(srtype='classic')
        graphs.SwissRoll(noise=True)
        graphs.SwissRoll(noise=False)
        graphs.SwissRoll(dim=2)
        graphs.SwissRoll(dim=3)

    def test_grid2d(self):
        graphs.Grid2d(3, 2)
        graphs.Grid2d(3)

    def test_imgpatches(self):
        graphs.ImgPatches(img=self._img, patch_shape=(3, 3))

    def test_grid2dimgpatches(self):
        graphs.Grid2dImgPatches(img=self._img, patch_shape=(3, 3))

    def test_grid2d_diagonals(self):
        value = 0.5
        G = graphs.Grid2d(6, 7, diagonal=value)
        self.assertEqual(G.W[2, 8], value)
        self.assertEqual(G.W[9, 1], value)
        self.assertEqual(G.W[9, 3], value)
        self.assertEqual(G.W[2, 14], 0.0)
        self.assertEqual(G.W[17, 1], 0.0)
        self.assertEqual(G.W[9, 16], 1.0)
        self.assertEqual(G.W[20, 27], 1.0)

suite_graphs = unittest.TestLoader().loadTestsFromTestCase(TestCase)


class TestImportExport(unittest.TestCase):

    def test_networkx_export_import(self):
        # Export to networkx and reimport to PyGSP2

        # Exporting the Bunny graph
        g = graphs.Bunny()
        g_nx = g.to_networkx()
        g2 = graphs.Graph.from_networkx(g_nx)
        np.testing.assert_array_equal(g.W.todense(), g2.W.todense())

    def test_networkx_import_export(self):
        # Import from networkx then export to networkx again
        g_nx = nx.gnm_random_graph(100, 50)  # Generate a random graph
        g = graphs.Graph.from_networkx(g_nx).to_networkx()

        np.testing.assert_array_equal(nx.adjacency_matrix(g_nx).todense(),
                                      nx.adjacency_matrix(g).todense())

    def test_graphtool_export_import(self):
        # Export to graph tool and reimport to PyGSP2 directly
        # The exported graph is a simple one without an associated Signal
        g = graphs.Bunny()
        g_gt = g.to_graphtool()
        g2 = graphs.Graph.from_graphtool(g_gt)
        np.testing.assert_array_equal(g.W.todense(), g2.W.todense())

    def test_graphtool_multiedge_import(self):
        # Manualy create a graph with multiple edges
        g_gt = gt.Graph()
        g_gt.add_vertex(n=10)
        # connect edge (3,6) three times
        for i in range(3):
            g_gt.add_edge(g_gt.vertex(3), g_gt.vertex(6))
        g = graphs.Graph.from_graphtool(g_gt)
        self.assertEqual(g.W[3, 6], 3.0)

        eprop_double = g_gt.new_edge_property("double")

        # Set the weight of 2 out of the 3 edges. The last one has a default weight of 0
        e = g_gt.edge(3, 6, all_edges=True)
        eprop_double[e[0]] = 8.0
        eprop_double[e[1]] = 1.0

        g_gt.edge_properties["weight"] = eprop_double
        g3 = graphs.Graph.from_graphtool(g_gt)
        self.assertEqual(g3.W[3, 6], 9.0)

    def test_graphtool_import_export(self):
        # Import to PyGSP2 and export again to graph tool directly
        # create a random graphTool graph that does not contain multiple edges and no signal
        rng = np.random.default_rng(42)
        graph_gt = gt.generation.random_graph(100, lambda: (rng.poisson(4), rng.poisson(4)))

        eprop_double = graph_gt.new_edge_property("double")
        for e in graph_gt.edges():
            eprop_double[e] = random.random()
        graph_gt.edge_properties["weight"] = eprop_double

        graph2_gt = graphs.Graph.from_graphtool(graph_gt).to_graphtool()

        self.assertEqual(graph_gt.num_edges(), graph2_gt.num_edges(),
                         "the number of edges does not correspond")

        def key(edge): return str(edge.source()) + ":" + str(edge.target())

        for e1, e2 in zip(sorted(graph_gt.edges(), key=key), sorted(graph2_gt.edges(), key=key)):
            self.assertEqual(e1.source(), e2.source())
            self.assertEqual(e1.target(), e2.target())
        for v1, v2 in zip(graph_gt.vertices(), graph2_gt.vertices()):
            self.assertEqual(v1, v2)

    def test_networkx_signal_export(self):
        graph = graphs.BarabasiAlbert(N=100, seed=42)
        rng = np.random.default_rng(42)
        signal1 = rng.normal(0, 1, graph.N)
        signal2 = rng.integers(0, 10, graph.N)
        graph.set_signal(signal1, "signal1")
        graph.set_signal(signal2, "signal2")
        graph_nx = graph.to_networkx()
        for i in range(graph.N):
            self.assertEqual(graph_nx.nodes[i]["signal1"], signal1[i])
            self.assertEqual(graph_nx.nodes[i]["signal2"], signal2[i])
        # invalid signal type
        graph = graphs.Path(3)
        graph.set_signal(np.array(['a', 'b', 'c']), 'sig')
        self.assertRaises(ValueError, graph.to_networkx)

    def test_graphtool_signal_export(self):
        g = graphs.Logo()
        rng = np.random.default_rng(42)
        s = rng.normal(0, 1, size=g.N)
        s2 = rng.integers(0, 10, size=g.N)
        g.set_signal(s, "signal1")
        g.set_signal(s2, "signal2")
        g_gt = g.to_graphtool()
        # Check the signals on all nodes
        for i, v in enumerate(g_gt.vertices()):
            self.assertEqual(g_gt.vertex_properties["signal1"][v], s[i])
            self.assertEqual(g_gt.vertex_properties["signal2"][v], s2[i])
        # invalid signal type
        graph = graphs.Path(3)
        graph.set_signal(np.array(['a', 'b', 'c']), 'sig')
        self.assertRaises(TypeError, graph.to_graphtool)

    def test_graphtool_signal_import(self):
        g_gt = gt.Graph()
        g_gt.add_vertex(10)

        g_gt.add_edge(g_gt.vertex(3), g_gt.vertex(6))
        g_gt.add_edge(g_gt.vertex(4), g_gt.vertex(6))
        g_gt.add_edge(g_gt.vertex(7), g_gt.vertex(2))

        vprop_double = g_gt.new_vertex_property("double")

        vprop_double[g_gt.vertex(0)] = 5
        vprop_double[g_gt.vertex(1)] = -3
        vprop_double[g_gt.vertex(2)] = 2.4

        g_gt.vertex_properties["signal"] = vprop_double
        g = graphs.Graph.from_graphtool(g_gt)
        self.assertEqual(g.signals["signal"][0], 5.0)
        self.assertEqual(g.signals["signal"][1], -3.0)
        self.assertEqual(g.signals["signal"][2], 2.4)

    def test_networkx_signal_import(self):
        graph_nx = nx.Graph()
        graph_nx.add_nodes_from(range(2, 5))
        graph_nx.add_edges_from([(3, 4), (2, 4), (3, 5)])
        nx.set_node_attributes(graph_nx, {2: 4, 3: 5, 5: 2.3}, "s")
        graph_pg = graphs.Graph.from_networkx(graph_nx)
        np.testing.assert_allclose(graph_pg.signals["s"], [4, 5, np.nan, 2.3])

    def test_no_weights(self):

        adjacency = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])

        # NetworkX no weights.
        graph_nx = nx.Graph()
        graph_nx.add_edge(0, 1)
        graph_nx.add_edge(1, 2)
        graph_pg = graphs.Graph.from_networkx(graph_nx)
        np.testing.assert_allclose(graph_pg.W.toarray(), adjacency)

        # NetworkX non-existent weight name.
        graph_nx.edges[(0, 1)]['weight'] = 2
        graph_nx.edges[(1, 2)]['weight'] = 2
        graph_pg = graphs.Graph.from_networkx(graph_nx)
        np.testing.assert_allclose(graph_pg.W.toarray(), 2*adjacency)
        graph_pg = graphs.Graph.from_networkx(graph_nx, weight='unknown')
        np.testing.assert_allclose(graph_pg.W.toarray(), adjacency)

        # Graph-tool no weights.
        graph_gt = gt.Graph(directed=False)
        graph_gt.add_edge(0, 1)
        graph_gt.add_edge(1, 2)
        graph_pg = graphs.Graph.from_graphtool(graph_gt)
        np.testing.assert_allclose(graph_pg.W.toarray(), adjacency)

        # Graph-tool non-existent weight name.
        prop = graph_gt.new_edge_property("double")
        prop[graph_gt.edge(0, 1)] = 2
        prop[graph_gt.edge(1, 2)] = 2
        graph_gt.edge_properties["weight"] = prop
        graph_pg = graphs.Graph.from_graphtool(graph_gt)
        np.testing.assert_allclose(graph_pg.W.toarray(), 2*adjacency)
        graph_pg = graphs.Graph.from_graphtool(graph_gt, weight='unknown')
        np.testing.assert_allclose(graph_pg.W.toarray(), adjacency)

    def test_break_join_signals(self):
        """Multi-dim signals are broken on export and joined on import."""
        graph1 = graphs.Sensor(20, seed=42)
        graph1.set_signal(graph1.coords, 'coords')
        # networkx
        graph2 = graph1.to_networkx()
        graph2 = graphs.Graph.from_networkx(graph2)
        np.testing.assert_allclose(graph2.signals['coords'], graph1.coords)
        # graph-tool
        graph2 = graph1.to_graphtool()
        graph2 = graphs.Graph.from_graphtool(graph2)
        np.testing.assert_allclose(graph2.signals['coords'], graph1.coords)
        # save and load (need ordered dicts)
        filename = 'graph.graphml'
        graph1.save(filename)
        graph2 = graphs.Graph.load(filename)
        np.testing.assert_allclose(graph2.signals['coords'], graph1.coords)
        os.remove(filename)

    def test_save_load(self):

        # TODO: test with multiple graphs and signals
        # * dtypes (float, int, bool) of adjacency and signals
        # * empty graph / isolated nodes

        G1 = graphs.Sensor(seed=42)
        W = G1.W.toarray()
        sig = np.random.default_rng(42).normal(size=G1.N)
        G1.set_signal(sig, 's')

        for fmt in ['graphml', 'gml', 'gexf']:
            for backend in ['networkx', 'graph-tool']:

                if fmt == 'gexf' and backend == 'graph-tool':
                    self.assertRaises(ValueError, G1.save, 'g', fmt, backend)
                    self.assertRaises(ValueError, graphs.Graph.load, 'g', fmt,
                                      backend)
                    os.remove('g')
                    continue

                atol = 1e-5 if fmt == 'gml' and backend == 'graph-tool' else 0

                for filename, fmt in [('graph.' + fmt, None), ('graph', fmt)]:
                    G1.save(filename, fmt, backend)
                    G2 = graphs.Graph.load(filename, fmt, backend)
                    np.testing.assert_allclose(G2.W.toarray(), W, atol=atol)
                    np.testing.assert_allclose(G2.signals['s'], sig, atol=atol)
                    os.remove(filename)

        self.assertRaises(ValueError, graphs.Graph.load, 'g.gml', fmt='?')
        self.assertRaises(ValueError, graphs.Graph.load, 'g.gml', backend='?')
        self.assertRaises(ValueError, G1.save, 'g.gml', fmt='?')
        self.assertRaises(ValueError, G1.save, 'g.gml', backend='?')

    def test_import_errors(self):
        from unittest.mock import patch
        graph = graphs.Sensor()
        filename = 'graph.gml'
        with patch.dict(sys.modules, {'networkx': None}):
            self.assertRaises(ImportError, graph.to_networkx)
            self.assertRaises(ImportError, graphs.Graph.from_networkx, None)
            self.assertRaises(ImportError, graph.save, filename,
                              backend='networkx')
            self.assertRaises(ImportError, graphs.Graph.load, filename,
                              backend='networkx')
            graph.save(filename)
            graphs.Graph.load(filename)
        with patch.dict(sys.modules, {'graph_tool': None}):
            self.assertRaises(ImportError, graph.to_graphtool)
            self.assertRaises(ImportError, graphs.Graph.from_graphtool, None)
            self.assertRaises(ImportError, graph.save, filename,
                              backend='graph-tool')
            self.assertRaises(ImportError, graphs.Graph.load, filename,
                              backend='graph-tool')
            graph.save(filename)
            graphs.Graph.load(filename)
        with patch.dict(sys.modules, {'networkx': None, 'graph_tool': None}):
            self.assertRaises(ImportError, graph.save, filename)
            self.assertRaises(ImportError, graphs.Graph.load, filename)
        os.remove(filename)


suite_import_export = unittest.TestLoader().loadTestsFromTestCase(TestImportExport)
suite = unittest.TestSuite([suite_graphs, suite_import_export])
