diff --git a/easygraph/classes/operation.py b/easygraph/classes/operation.py index c53d506c..cb7b2d0e 100644 --- a/easygraph/classes/operation.py +++ b/easygraph/classes/operation.py @@ -260,8 +260,8 @@ def set_node_attributes(G, values, name=None): def topological_generations(G): if not G.is_directed(): raise AssertionError("Topological sort not defined on undirected graphs.") - indegree_map = {v: d for v, d in G.in_degree() if d > 0} - zero_indegree = [v for v, d in G.in_degree() if d == 0] + indegree_map = {v: d for v, d in G.in_degree().items() if d > 0} + zero_indegree = [v for v, d in G.in_degree().items() if d == 0] while zero_indegree: this_generation = zero_indegree zero_indegree = [] @@ -283,7 +283,7 @@ def topological_generations(G): def topological_sort(G): - for generation in eg.topological_generations(G): + for generation in topological_generations(G): yield from generation diff --git a/easygraph/classes/tests/test_base.py b/easygraph/classes/tests/test_base.py new file mode 100644 index 00000000..808a35ba --- /dev/null +++ b/easygraph/classes/tests/test_base.py @@ -0,0 +1,145 @@ +import sys + +import pytest + + +np = pytest.importorskip("numpy") +pd = pytest.importorskip("pandas") +sp = pytest.importorskip("scipy") + +import easygraph as eg + +from easygraph.utils.misc import * + + +class TestConvertNumpyArray: + def setup_method(self): + self.G1 = eg.complete_graph(5) + + def assert_equal(self, G1, G2): + assert nodes_equal(G1.nodes, G2.nodes) + assert edges_equal(G1.edges, G2.edges, need_data=False) + + def identity_conversion(self, G, A, create_using): + assert A.sum() > 0 + GG = eg.from_numpy_array(A, create_using=create_using) + self.assert_equal(G, GG) + GW = eg.to_easygraph_graph(A, create_using=create_using) + self.assert_equal(G, GW) + + def test_identity_graph_array(self): + A = eg.to_numpy_array(self.G1) + self.identity_conversion(self.G1, A, eg.Graph()) + + +class TestConvertPandas: + def setup_method(self): + self.rng = np.random.RandomState(seed=5) + ints = self.rng.randint(1, 11, size=(3, 2)) + a = ["A", "B", "C"] + b = ["D", "A", "E"] + df = pd.DataFrame(ints, columns=["weight", "cost"]) + df[0] = a + df["b"] = b + self.df = df + + mdf = pd.DataFrame([[4, 16, "A", "D"]], columns=["weight", "cost", 0, "b"]) + self.mdf = pd.concat([df, mdf]) + + def assert_equal(self, G1, G2): + assert nodes_equal(G1.nodes, G2.nodes) + assert edges_equal(G1.edges, G2.edges, need_data=False) + + def test_from_edgelist_multi_attr(self): + Gtrue = eg.Graph( + [ + ("E", "C", {"cost": 9, "weight": 10}), + ("B", "A", {"cost": 1, "weight": 7}), + ("A", "D", {"cost": 7, "weight": 4}), + ] + ) + G = eg.from_pandas_edgelist(self.df, 0, "b", ["weight", "cost"]) + self.assert_equal(G, Gtrue) + + def test_from_adjacency(self): + Gtrue = eg.DiGraph([("A", "B"), ("B", "C")]) + data = { + "A": {"A": 0, "B": 0, "C": 0}, + "B": {"A": 1, "B": 0, "C": 0}, + "C": {"A": 0, "B": 1, "C": 0}, + } + dftrue = pd.DataFrame(data, dtype=np.intp) + df = dftrue[["A", "C", "B"]] + G = eg.from_pandas_adjacency(df, create_using=eg.DiGraph()) + self.assert_equal(G, Gtrue) + + +class TestConvertScipy: + def setup_method(self): + self.G1 = eg.complete_graph(3) + + def assert_equal(self, G1, G2): + assert nodes_equal(G1.nodes, G2.nodes) + assert edges_equal(G1.edges, G2.edges, need_data=False) + + @pytest.mark.skipif( + sys.version_info < (3, 8), reason="requires python3.8 or higher" + ) + def test_from_scipy(self): + data = sp.sparse.csr_matrix([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) + G = eg.from_scipy_sparse_matrix(data) + self.assert_equal(self.G1, G) + + +def test_from_edgelist(): + edgelist = [(0, 1), (1, 2)] + G = eg.from_edgelist(edgelist) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_dict_of_lists(): + d = {0: [1], 1: [2]} + G = eg.to_easygraph_graph(d) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_dict_of_dicts(): + d = {0: {1: {}}, 1: {2: {}}} + G = eg.to_easygraph_graph(d) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_numpy_array(): + G = eg.complete_graph(3) + A = eg.to_numpy_array(G) + G2 = eg.from_numpy_array(A) + assert sorted((u, v) for u, v, _ in G.edges) == sorted( + (u, v) for u, v, _ in G2.edges + ) + + +def test_from_pandas_edgelist(): + df = pd.DataFrame({"source": [0, 1], "target": [1, 2], "weight": [0.5, 0.7]}) + G = eg.from_pandas_edgelist(df, source="source", target="target", edge_attr=True) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_pandas_adjacency(): + df = pd.DataFrame([[0, 1], [1, 0]], columns=["A", "B"], index=["A", "B"]) + G = eg.from_pandas_adjacency(df) + assert sorted((u, v) for u, v, _ in G.edges) == [("A", "B")] + + +def test_from_scipy_sparse_matrix(): + mat = sp.sparse.csr_matrix([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) + G = eg.from_scipy_sparse_matrix(mat) + expected_edges = [(0, 1), (1, 2)] + assert sorted((u, v) for u, v, _ in G.edges) == expected_edges + + +def test_invalid_dict_type(): + class NotGraph: + pass + + with pytest.raises(eg.EasyGraphError): + eg.to_easygraph_graph(NotGraph()) diff --git a/easygraph/classes/tests/test_directed_graph.py b/easygraph/classes/tests/test_directed_graph.py new file mode 100644 index 00000000..568286ff --- /dev/null +++ b/easygraph/classes/tests/test_directed_graph.py @@ -0,0 +1,97 @@ +import os +import unittest + +from easygraph import DiGraph + + +class TestDiGraph(unittest.TestCase): + def setUp(self): + self.G = DiGraph() + + def test_add_node_and_exists(self): + self.G.add_node("A") + self.assertTrue(self.G.has_node("A")) + self.assertIn("A", self.G.nodes) + + def test_add_nodes_with_attrs(self): + self.G.add_nodes(["B", "C"], nodes_attr=[{"age": 30}, {"age": 40}]) + self.assertEqual(self.G.nodes["B"]["age"], 30) + self.assertEqual(self.G.nodes["C"]["age"], 40) + + def test_add_edge_and_attrs(self): + self.G.add_edge("A", "B", weight=5) + self.assertTrue(self.G.has_edge("A", "B")) + self.assertEqual(self.G.adj["A"]["B"]["weight"], 5) + + def test_add_edges_with_attrs(self): + self.G.add_edges([("B", "C"), ("C", "D")], edges_attr=[{"w": 1}, {"w": 2}]) + self.assertEqual(self.G.adj["B"]["C"]["w"], 1) + self.assertEqual(self.G.adj["C"]["D"]["w"], 2) + + def test_remove_node_and_edges(self): + self.G.add_edges([("X", "Y"), ("Y", "Z")]) + self.G.remove_node("Y") + self.assertFalse("Y" in self.G.nodes) + self.assertFalse(self.G.has_edge("Y", "Z")) + + def test_remove_edge(self): + self.G.add_edge("M", "N") + self.G.remove_edge("M", "N") + self.assertFalse(self.G.has_edge("M", "N")) + + def test_degrees(self): + self.G.add_edges( + [("A", "B"), ("C", "B")], edges_attr=[{"weight": 3}, {"weight": 2}] + ) + + in_degrees = self.G.in_degree(weight="weight") + out_degrees = self.G.out_degree(weight="weight") + degrees = self.G.degree(weight="weight") + + self.assertEqual(in_degrees["B"], 5) + self.assertEqual(out_degrees["A"], 3) + self.assertEqual(degrees["B"], 5) + + def test_neighbors_and_preds(self): + self.G.add_edges([("P", "Q"), ("R", "P")]) + self.assertIn("Q", list(self.G.neighbors("P"))) + self.assertIn("R", list(self.G.predecessors("P"))) + all_n = list(self.G.all_neighbors("P")) + self.assertIn("Q", all_n) + self.assertIn("R", all_n) + + def test_size_and_num_edges_nodes(self): + self.G.add_edges([("X", "Y"), ("Y", "Z")]) + self.assertEqual(self.G.size(), 2) + self.assertEqual(self.G.number_of_edges(), 2) + self.assertEqual(self.G.number_of_nodes(), 3) + + def test_subgraph_and_ego(self): + self.G.add_edges([("A", "B"), ("B", "C"), ("C", "D")]) + sub = self.G.nodes_subgraph(["A", "B", "C"]) + self.assertTrue(sub.has_edge("A", "B")) + self.assertFalse(sub.has_edge("C", "D")) + ego = self.G.ego_subgraph("B") + self.assertIn("A", ego.nodes or []) + self.assertIn("C", ego.nodes or []) + + def test_to_index_node_graph(self): + self.G.add_edges([("foo", "bar"), ("bar", "baz")]) + G2, node2idx, idx2node = self.G.to_index_node_graph() + self.assertEqual(len(G2.nodes), 3) + self.assertEqual(node2idx["foo"], 0) + self.assertEqual(idx2node[0], "foo") + + def test_copy(self): + self.G.add_edge("copyA", "copyB", weight=42) + G_copy = self.G.copy() + self.assertEqual(G_copy.adj["copyA"]["copyB"]["weight"], 42) + + def test_file_add_edges(self): + fname = "temp_edges.txt" + with open(fname, "w") as f: + f.write("1 2 3.5\n2 3 4.5\n") + self.G.add_edges_from_file(fname, weighted=True) + os.remove(fname) + self.assertEqual(self.G.adj["1"]["2"]["weight"], 3.5) + self.assertEqual(self.G.adj["2"]["3"]["weight"], 4.5) diff --git a/easygraph/classes/tests/test_graphV2.py b/easygraph/classes/tests/test_graphV2.py new file mode 100644 index 00000000..f3a4a233 --- /dev/null +++ b/easygraph/classes/tests/test_graphV2.py @@ -0,0 +1,122 @@ +import unittest + +import easygraph as eg + + +class TestEasyGraph(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + + def test_add_single_node(self): + self.G.add_node(1) + self.assertIn(1, self.G.nodes) + + def test_add_multiple_nodes(self): + self.G.add_nodes([2, 3, 4]) + for node in [2, 3, 4]: + self.assertIn(node, self.G.nodes) + + def test_add_node_with_attributes(self): + self.G.add_node("node", color="red") + self.assertEqual(self.G.nodes["node"]["color"], "red") + + def test_add_single_edge(self): + self.G.add_edge(1, 2) + self.assertTrue(self.G.has_edge(1, 2)) + self.assertTrue(self.G.has_edge(2, 1)) + + def test_add_edge_with_weight(self): + self.G.add_edge("a", "b", weight=10) + self.assertEqual(self.G["a"]["b"]["weight"], 10) + + def test_add_edges(self): + self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 5}, {"weight": 6}]) + self.assertEqual(self.G[1][2]["weight"], 5) + self.assertEqual(self.G[2][3]["weight"], 6) + + def test_remove_node(self): + self.G.add_node(10) + self.G.remove_node(10) + self.assertNotIn(10, self.G.nodes) + + def test_remove_edge(self): + self.G.add_edge(1, 2) + self.G.remove_edge(1, 2) + self.assertFalse(self.G.has_edge(1, 2)) + + def test_neighbors(self): + self.G.add_edges([(1, 2), (1, 3)]) + neighbors = list(self.G.neighbors(1)) + self.assertIn(2, neighbors) + self.assertIn(3, neighbors) + + def test_subgraph(self): + self.G.add_edges([(1, 2), (2, 3), (3, 4)]) + subG = self.G.nodes_subgraph([2, 3]) + self.assertIn(2, subG.nodes) + self.assertIn(3, subG.nodes) + self.assertTrue(subG.has_edge(2, 3)) + self.assertFalse(subG.has_edge(3, 4)) + + def test_ego_subgraph(self): + self.G.add_edges([(1, 2), (2, 3), (2, 4)]) + ego = self.G.ego_subgraph(2) + self.assertIn(2, ego.nodes) + self.assertIn(1, ego.nodes) + self.assertIn(3, ego.nodes) + self.assertIn(4, ego.nodes) + + def test_to_index_node_graph(self): + self.G.add_edges([("a", "b"), ("b", "c")]) + G_index, index_of_node, node_of_index = self.G.to_index_node_graph() + self.assertEqual(len(G_index.nodes), 3) + self.assertTrue(all(isinstance(k, int) for k in G_index.nodes)) + + def test_directed_conversion(self): + self.G.add_edge(1, 2) + H = self.G.to_directed() + self.assertTrue(H.is_directed()) + self.assertTrue(H.has_edge(1, 2)) + self.assertTrue(H.has_edge(2, 1)) + + def test_clone_graph(self): + self.G.add_edges([(1, 2), (2, 3)]) + G_clone = self.G.copy() + self.assertTrue(G_clone.has_edge(1, 2)) + self.assertTrue(G_clone.has_edge(2, 3)) + + def test_degree(self): + self.G.add_edge(1, 2, weight=5) + deg = self.G.degree() + self.assertEqual(deg[1], 5) + self.assertEqual(deg[2], 5) + + def test_size(self): + self.G.add_edges([(1, 2), (2, 3)]) + self.assertEqual(self.G.size(), 2) + + def test_edge_weight_default(self): + self.G.add_edge(4, 5) + self.assertEqual(self.G[4][5].get("weight", 1), 1) + + def test_node_index_mappings(self): + self.G.add_nodes([10, 20, 30]) + index2node = self.G.index2node + node_index = self.G.node_index + for i, node in index2node.items(): + self.assertEqual(node_index[node], i) + + def test_graph_order(self): + self.G.add_nodes([1, 2, 3]) + self.assertEqual(self.G.order(), 3) + + def test_graph_size_with_weight(self): + self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 4}, {"weight": 6}]) + self.assertEqual(self.G.size(weight="weight"), 10.0) + + def test_clear_cache(self): + self.G.add_edge(1, 2) + _ = self.G.edges + self.assertIn("edge", self.G.cache) + self.G._clear_cache() + self.assertEqual(len(self.G.cache), 0) diff --git a/easygraph/classes/tests/test_multidigraph.py b/easygraph/classes/tests/test_multidigraph.py index bd10561b..7609d529 100644 --- a/easygraph/classes/tests/test_multidigraph.py +++ b/easygraph/classes/tests/test_multidigraph.py @@ -33,6 +33,82 @@ def test_attributes(self): print(self.g.in_edges) +class TestMultiDiGraph(unittest.TestCase): + def setUp(self): + self.G = eg.MultiDiGraph() + + def test_add_edge_without_key(self): + key1 = self.G.add_edge("A", "B", weight=1) + key2 = self.G.add_edge("A", "B", weight=2) + self.assertNotEqual(key1, key2) + self.assertEqual(len(self.G._adj["A"]["B"]), 2) + + def test_add_edge_with_key(self): + key = self.G.add_edge("A", "B", key="mykey", weight=3) + self.assertEqual(key, "mykey") + self.assertEqual(self.G._adj["A"]["B"]["mykey"]["weight"], 3) + + def test_edge_attributes_update(self): + self.G.add_edge("X", "Y", key=1, color="red") + self.G.add_edge("X", "Y", key=1, shape="circle") + self.assertEqual(self.G._adj["X"]["Y"][1]["color"], "red") + self.assertEqual(self.G._adj["X"]["Y"][1]["shape"], "circle") + + def test_remove_edge_by_key(self): + self.G.add_edge("A", "B", key="k1") + self.G.add_edge("A", "B", key="k2") + self.G.remove_edge("A", "B", key="k1") + self.assertIn("k2", self.G._adj["A"]["B"]) + self.assertNotIn("k1", self.G._adj["A"]["B"]) + + def test_remove_edge_without_key(self): + self.G.add_edge("A", "B", key="auto1") + self.G.add_edge("A", "B", key="auto2") + self.G.remove_edge("A", "B") + # Only one of the keys should remain + self.assertEqual(len(self.G._adj["A"]["B"]), 1) + + def test_remove_nonexistent_edge_raises(self): + with self.assertRaises(eg.EasyGraphError): + self.G.remove_edge("X", "Y", key="doesnotexist") + + def test_edges_property(self): + self.G.add_edge("U", "V", key="k", weight=5) + edges = self.G.edges + self.assertIn(("U", "V", "k", {"weight": 5}), edges) + + def test_in_out_degree(self): + self.G.add_edge("A", "B", weight=3) + self.G.add_edge("C", "B", weight=2) + + in_deg = {} + for n in self.G._node: + preds = self.G._pred[n] + in_deg[n] = sum( + d.get("weight", 1) + for key_dict in preds.values() + for d in key_dict.values() + ) + + self.assertEqual(in_deg["B"], 5) + + def test_to_undirected(self): + self.G.add_edge("A", "B", key="k", weight=10) + UG = self.G.to_undirected() + self.assertTrue(UG.has_edge("A", "B")) + self.assertEqual(UG["A"]["B"]["k"]["weight"], 10) + + def test_reverse_graph(self): + self.G.add_edge("A", "B", key="k", data=99) + RG = self.G.reverse() + self.assertTrue(RG.has_edge("B", "A")) + self.assertEqual(RG["B"]["A"]["k"]["data"], 99) + + def test_is_multigraph_and_directed(self): + self.assertTrue(self.G.is_multigraph()) + self.assertTrue(self.G.is_directed()) + + if __name__ == "__main__": unittest.main() # test() diff --git a/easygraph/classes/tests/test_multigraph.py b/easygraph/classes/tests/test_multigraph.py index 28c36210..963a0613 100644 --- a/easygraph/classes/tests/test_multigraph.py +++ b/easygraph/classes/tests/test_multigraph.py @@ -59,3 +59,91 @@ def test_remove_node(self): assert G.adj == {1: {2: {0: {}}}, 2: {1: {0: {}}}} with pytest.raises(eg.EasyGraphError): G.remove_node(-1) + + +class TestMultiGraphExtended: + def test_add_multiple_edges_and_keys(self): + G = eg.MultiGraph() + k0 = G.add_edge(1, 2) + k1 = G.add_edge(1, 2) + assert k0 == 0 + assert k1 == 1 + assert G.number_of_edges(1, 2) == 2 + + def test_add_edge_with_key_and_attributes(self): + G = eg.MultiGraph() + k = G.add_edge(1, 2, key="custom", weight=3, label="test") + assert k == "custom" + assert G.get_edge_data(1, 2, "custom") == {"weight": 3, "label": "test"} + + def test_add_edges_from_various_formats(self): + G = eg.MultiGraph() + edges = [ + (1, 2), # 2-tuple + (2, 3, {"weight": 7}), # 3-tuple with attr + (3, 4, "k1", {"color": "red"}), # 4-tuple + ] + keys = G.add_edges_from(edges, capacity=100) + assert len(keys) == 3 + assert G.get_edge_data(3, 4, "k1")["color"] == "red" + assert G.get_edge_data(2, 3, 0)["capacity"] == 100 + + def test_remove_edge_with_key(self): + G = eg.MultiGraph() + G.add_edge(1, 2, key="a") + G.add_edge(1, 2, key="b") + G.remove_edge(1, 2, key="a") + assert not G.has_edge(1, 2, key="a") + assert G.has_edge(1, 2, key="b") + + def test_remove_edge_arbitrary(self): + G = eg.MultiGraph() + G.add_edge(1, 2) + G.add_edge(1, 2) + G.remove_edge(1, 2) + assert G.number_of_edges(1, 2) == 1 + + def test_remove_edges_from_mixed(self): + G = eg.MultiGraph() + keys = G.add_edges_from([(1, 2), (1, 2), (2, 3)]) + G.remove_edges_from([(1, 2), (2, 3)]) + assert G.number_of_edges(1, 2) == 1 + assert G.number_of_edges(2, 3) == 0 + + def test_to_directed_graph(self): + G = eg.MultiGraph() + G.add_edge(0, 1, weight=10) + D = G.to_directed() + assert D.is_directed() + assert D.has_edge(0, 1) + assert D.has_edge(1, 0) + assert D.get_edge_data(0, 1, 0)["weight"] == 10 + + def test_copy_graph(self): + G = eg.MultiGraph() + G.add_edge(1, 2, key="x", weight=9) + H = G.copy() + assert H.get_edge_data(1, 2, "x") == {"weight": 9} + assert H is not G + assert H.get_edge_data(1, 2, "x") is not G.get_edge_data(1, 2, "x") + + def test_has_edge_variants(self): + G = eg.MultiGraph() + G.add_edge(1, 2) + G.add_edge(1, 2, key="z") + assert G.has_edge(1, 2) + assert G.has_edge(1, 2, key="z") + assert not G.has_edge(2, 1, key="nonexistent") + + def test_get_edge_data_defaults(self): + G = eg.MultiGraph() + assert G.get_edge_data(10, 20) is None + assert G.get_edge_data(10, 20, key="any", default="missing") == "missing" + + def test_edge_property_returns_all_edges(self): + G = eg.MultiGraph() + G.add_edge(0, 1, key=5, label="important") + G.add_edge(1, 0, key=3, label="also important") + edges = list(G.edges) + assert any((0, 1, 5, {"label": "important"}) == e for e in edges) + assert any((0, 1, 3, {"label": "also important"}) == e for e in edges) diff --git a/easygraph/classes/tests/test_operation.py b/easygraph/classes/tests/test_operation.py index 6245656a..2f653fac 100644 --- a/easygraph/classes/tests/test_operation.py +++ b/easygraph/classes/tests/test_operation.py @@ -1,6 +1,7 @@ import easygraph as eg import pytest +from easygraph.classes import operation from easygraph.utils import edges_equal @@ -13,3 +14,118 @@ def test_selfloops(graph_type): assert edges_equal(eg.selfloop_edges(G), [(0, 0)]) assert edges_equal(eg.selfloop_edges(G, data=True), [(0, 0, {})]) assert eg.number_of_selfloops(G) == 1 + + +def test_set_edge_attributes_scalar(): + G = eg.path_graph(3) + eg.set_edge_attributes(G, 5, "weight") + for _, _, data in G.edges: + assert data["weight"] == 5 + + +def test_set_edge_attributes_dict(): + G = eg.path_graph(3) + attrs = {(0, 1): 3, (1, 2): 7} + eg.set_edge_attributes(G, attrs, "weight") + assert G[0][1]["weight"] == 3 + assert G[1][2]["weight"] == 7 + + +def test_set_edge_attributes_dict_of_dict(): + G = eg.path_graph(3) + attrs = {(0, 1): {"a": 1}, (1, 2): {"b": 2}} + eg.set_edge_attributes(G, attrs) + assert G[0][1]["a"] == 1 + assert G[1][2]["b"] == 2 + + +def test_set_node_attributes_scalar(): + G = eg.path_graph(3) + eg.set_node_attributes(G, 42, "level") + for n in G.nodes: + assert G.nodes[n]["level"] == 42 + + +def test_set_node_attributes_dict(): + G = eg.path_graph(3) + eg.set_node_attributes(G, {0: "x", 1: "y"}, name="tag") + assert G.nodes[0]["tag"] == "x" + assert G.nodes[1]["tag"] == "y" + + +def test_set_node_attributes_dict_of_dict(): + G = eg.path_graph(3) + eg.set_node_attributes(G, {0: {"foo": 10}, 1: {"bar": 20}}) + assert G.nodes[0]["foo"] == 10 + assert G.nodes[1]["bar"] == 20 + + +def test_add_path_structure_and_attrs(): + G = eg.Graph() + eg.add_path(G, [10, 11, 12], weight=9) + actual_edges = {(u, v) for u, v, _ in G.edges} + assert actual_edges == {(10, 11), (11, 12)} + assert G[10][11]["weight"] == 9 + assert G[11][12]["weight"] == 9 + + +def test_topological_sort_linear(): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (2, 3)]) + assert list(operation.topological_sort(G)) == [1, 2, 3] + + +def test_topological_sort_cycle(): + G = eg.DiGraph([(0, 1), (1, 2), (2, 0)]) + with pytest.raises(AssertionError, match="contains a cycle"): + list(operation.topological_sort(G)) + + +def test_selfloop_edges_variants(): + G = eg.MultiGraph() + G.add_edge(0, 0, key="x", label="loop") + G.add_edge(1, 1, key="y", label="loop2") + basic = list(eg.selfloop_edges(G)) + with_data = list(eg.selfloop_edges(G, data=True)) + with_keys = list(eg.selfloop_edges(G, keys=True)) + full = list(eg.selfloop_edges(G, keys=True, data="label")) + assert (0, 0) in basic and (1, 1) in basic + assert all(len(t) == 3 for t in with_data) + assert all(len(t) == 3 for t in with_keys) + assert "x" in [k for _, _, k, _ in full] + + +def test_number_of_selfloops(): + G = eg.MultiGraph() + G.add_edges_from([(0, 0), (1, 1), (1, 2)]) + assert eg.number_of_selfloops(G) == 2 + + +def test_density_undirected(): + G = eg.complete_graph(5) + d = eg.density(G) + assert pytest.approx(d, 0.01) == 1.0 + + +def test_density_directed(): + G = eg.DiGraph() + G.add_edges_from([(0, 1), (1, 2)]) + d = eg.density(G) + assert pytest.approx(d, 0.01) == 2 / (3 * (3 - 1)) # 2/6 + + +def test_topological_generations_linear(): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (2, 3), (3, 4)]) + generations = list(operation.topological_generations(G)) + assert generations == [[1], [2], [3], [4]] + + +def test_topological_generations_branching(): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 4)]) + generations = list(operation.topological_generations(G)) + # Valid topological generations: [1], [2, 3], [4] + assert generations[0] == [1] + assert set(generations[1]) == {2, 3} + assert generations[2] == [4] diff --git a/easygraph/functions/basic/tests/test_avg_degree.py b/easygraph/functions/basic/tests/test_avg_degree.py index e69de29b..adf19b5e 100644 --- a/easygraph/functions/basic/tests/test_avg_degree.py +++ b/easygraph/functions/basic/tests/test_avg_degree.py @@ -0,0 +1,41 @@ +import easygraph as eg +import pytest + +from easygraph.functions.basic import average_degree + + +def test_average_degree_basic(): + G = eg.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + assert average_degree(G) == pytest.approx(4 / 3) + + +def test_average_degree_empty_graph(): + G = eg.Graph() + with pytest.raises(ZeroDivisionError): + average_degree(G) + + +def test_average_degree_self_loop(): + G = eg.Graph() + G.add_edge(1, 1) # self-loop + # Self-loop counts as 2 towards degree of node 1 + assert average_degree(G) == pytest.approx(2.0) + + +def test_average_degree_with_isolated_node(): + G = eg.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + G.add_node(4) # isolated node + assert average_degree(G) == pytest.approx(1.0) + + +def test_average_degree_directed_graph(): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (2, 3), (3, 1)]) + assert average_degree(G) == pytest.approx(2.0) + + +def test_average_degree_invalid_input(): + with pytest.raises(AttributeError): + average_degree(None) diff --git a/easygraph/functions/basic/tests/test_cluster.py b/easygraph/functions/basic/tests/test_cluster.py index 259499f4..548bb7ae 100644 --- a/easygraph/functions/basic/tests/test_cluster.py +++ b/easygraph/functions/basic/tests/test_cluster.py @@ -377,3 +377,42 @@ def test_triangle_and_signed_edge(self): G.add_edge(3, 0, weight=0) assert eg.clustering(G)[0] == 1 / 3 assert eg.clustering(G, weight="weight")[0] == -1 / 3 + + +class TestAdditionalClusteringCases: + def test_self_loops_ignored(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (1, 2), (2, 0)]) + G.add_edge(0, 0) # self-loop + assert eg.clustering(G, 0) == 1.0 + + def test_isolated_node(self): + G = eg.Graph() + G.add_node(1) + assert eg.clustering(G) == {1: 0} + + def test_degree_one_node(self): + G = eg.Graph() + G.add_edge(1, 2) + assert eg.clustering(G) == {1: 0, 2: 0} + + def test_custom_weight_name(self): + G = eg.Graph() + G.add_edge(0, 1, strength=2) + G.add_edge(1, 2, strength=2) + G.add_edge(2, 0, strength=2) + result = eg.clustering(G, weight="strength") + assert result[0] > 0 + + def test_negative_weights_mixed(self): + G = eg.complete_graph(3) + G[0][1]["weight"] = -1 + G[1][2]["weight"] = 1 + G[2][0]["weight"] = 1 + assert eg.clustering(G, 0, weight="weight") < 0 + + def test_directed_reciprocal_edges(self): + G = eg.DiGraph() + G.add_edges_from([(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)]) + result = eg.clustering(G) + assert all(0 <= v <= 1 for v in result.values()) diff --git a/easygraph/functions/basic/tests/test_localassort.py b/easygraph/functions/basic/tests/test_localassort.py index 5e895b38..86ddc4af 100644 --- a/easygraph/functions/basic/tests/test_localassort.py +++ b/easygraph/functions/basic/tests/test_localassort.py @@ -5,6 +5,8 @@ import numpy as np import pytest +from easygraph.functions.basic.localassort import localAssort + class TestLocalAssort: @classmethod @@ -34,3 +36,75 @@ def test_karateclub(self): _, assortT, Z = eg.functions.basic.localassort.localAssort( self.edgelist, self.valuelist, pr=np.array([0.9]) ) + + +def test_localassort_small_complete_graph(): + G = eg.complete_graph(4) + edgelist = np.array(list(G.edges)) + node_attr = np.array([0, 0, 1, 1]) + assortM, assortT, Z = localAssort(edgelist, node_attr) + assert assortM.shape == (4, 10) + assert assortT.shape == (4,) + assert Z.shape == (4,) + assert np.all(Z >= 0) and np.all(Z <= 1) + + +def test_localassort_with_missing_attributes(): + G = eg.path_graph(5) + edgelist = np.array(list(G.edges)) + node_attr = np.array([0, -1, 1, -1, 1]) + assortM, assortT, Z = localAssort(edgelist, node_attr, pr=np.array([0.5])) + assert assortT.shape == (5,) + assert Z.shape == (5,) + assert np.any(np.isnan(assortT)) + + +def test_localassort_directed_graph(): + G = eg.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3)]) + edgelist = np.array(list(G.edges)) + node_attr = np.array([0, 1, 0, 1]) + assortM, assortT, Z = localAssort(edgelist, node_attr, undir=False) + assert assortM.shape == (4, 10) + assert assortT.shape == (4,) + assert Z.shape == (4,) + + +def test_localassort_single_node_graph(): + edgelist = np.empty((0, 2), dtype=int) + node_attr = np.array([0]) + assortM, assortT, Z = localAssort(edgelist, node_attr) + assert assortM.shape == (1, 10) + assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5) + assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5) + assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5) + + +def test_localassort_disconnected_graph(): + G = eg.Graph() + G.add_nodes_from(range(5)) + edgelist = np.empty((0, 2), dtype=int) + node_attr = np.array([0, 1, 0, 1, 1]) + assortM, assortT, Z = localAssort(edgelist, node_attr) + assert assortM.shape == (5, 10) + assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5) + assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5) + assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5) + + +def test_localassort_high_restart_probabilities(): + G = eg.path_graph(5) + edgelist = np.array(list(G.edges)) + node_attr = np.array([1, 0, 1, 0, 1]) + pr = np.array([0.95, 0.99]) + assortM, assortT, Z = localAssort(edgelist, node_attr, pr=pr) + assert assortM.shape == (5, 2) + assert assortT.shape == (5,) + assert Z.shape == (5,) + + +def test_localassort_invalid_attribute_length(): + edgelist = np.array([[0, 1], [1, 2]]) + node_attr = np.array([0, 1]) # too short + with pytest.raises(ValueError): + localAssort(edgelist, node_attr) diff --git a/easygraph/functions/basic/tests/test_predecessor.py b/easygraph/functions/basic/tests/test_predecessor.py index d880560c..6c772c78 100644 --- a/easygraph/functions/basic/tests/test_predecessor.py +++ b/easygraph/functions/basic/tests/test_predecessor.py @@ -16,3 +16,64 @@ def test_predecessor(self): {2: [], 1: [2], 3: [2], 0: [1]}, {3: [], 2: [3], 1: [2], 0: [1]}, ] + + def test_basic_predecessor(self): + G = eg.path_graph(4) + result = eg.predecessor(G, 0) + assert result == {0: [], 1: [0], 2: [1], 3: [2]} + + def test_with_return_seen(self): + G = eg.path_graph(4) + pred, seen = eg.predecessor(G, 0, return_seen=True) + assert pred == {0: [], 1: [0], 2: [1], 3: [2]} + assert seen == {0: 0, 1: 1, 2: 2, 3: 3} + + def test_with_target(self): + G = eg.path_graph(4) + assert eg.predecessor(G, 0, target=2) == [1] + + def test_with_target_and_return_seen(self): + G = eg.path_graph(4) + pred, seen = eg.predecessor(G, 0, target=2, return_seen=True) + assert pred == [1] + assert seen == 2 + + def test_with_cutoff(self): + G = eg.path_graph(4) + pred = eg.predecessor(G, 0, cutoff=1) + assert pred == {0: [], 1: [0]} + + def test_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + pred = eg.predecessor(G, 0) + assert 2 not in pred and 3 not in pred + + def test_invalid_source(self): + G = eg.path_graph(4) + with pytest.raises(eg.NodeNotFound): + eg.predecessor(G, 99) + + def test_no_path_to_target(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + assert eg.predecessor(G, 0, target=3) == [] + + def test_no_path_to_target_with_return_seen(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + pred, seen = eg.predecessor(G, 0, target=3, return_seen=True) + assert pred == [] + assert seen == -1 + + def test_cycle_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) # cycled graph + pred = eg.predecessor(G, 0) + assert set(pred.keys()) == set(G.nodes) + + def test_directed_graph(self): + G = eg.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3)]) + pred = eg.predecessor(G, 0) + assert pred == {0: [], 1: [0], 2: [1], 3: [2]} diff --git a/easygraph/functions/centrality/tests/test_betweenness.py b/easygraph/functions/centrality/tests/test_betweenness.py index 83656038..82d6a556 100644 --- a/easygraph/functions/centrality/tests/test_betweenness.py +++ b/easygraph/functions/centrality/tests/test_betweenness.py @@ -17,10 +17,83 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] self.test_graphs.append(eg.classes.DiGraph(self.edges)) + self.undirected = eg.Graph() + self.undirected.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)]) + + self.directed = eg.DiGraph() + self.directed.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)]) + + self.disconnected = eg.Graph() + self.disconnected.add_edges_from([(0, 1), (2, 3)]) + + self.single_node = eg.Graph() + self.single_node.add_node(42) + + self.two_node = eg.Graph() + self.two_node.add_edge("A", "B") + + self.named_nodes = eg.Graph() + self.named_nodes.add_edges_from([("X", "Y"), ("Y", "Z")]) + def test_betweenness(self): for i in self.test_graphs: print(eg.functions.betweenness_centrality(i)) + def test_basic_undirected(self): + result = eg.functions.betweenness_centrality(self.undirected) + self.assertEqual(len(result), len(self.undirected.nodes)) + self.assertTrue(all(isinstance(x, float) for x in result)) + + def test_basic_directed(self): + result = eg.functions.betweenness_centrality(self.directed) + self.assertEqual(len(result), len(self.directed.nodes)) + + def test_disconnected(self): + result = eg.functions.betweenness_centrality(self.disconnected) + self.assertEqual(len(result), len(self.disconnected.nodes)) + self.assertTrue(all(v == 0.0 for v in result)) + + def test_single_node_graph(self): + result = eg.functions.betweenness_centrality(self.single_node) + self.assertEqual(result, [0.0]) + + def test_two_node_graph(self): + result = eg.functions.betweenness_centrality(self.two_node) + self.assertEqual(len(result), 2) + self.assertTrue(all(v == 0.0 for v in result)) + + def test_named_nodes_graph(self): + result = eg.functions.betweenness_centrality(self.named_nodes) + self.assertEqual(len(result), 3) + + def test_with_endpoints(self): + result = eg.functions.betweenness_centrality(self.undirected, endpoints=True) + self.assertEqual(len(result), len(self.undirected.nodes)) + + def test_unormalized(self): + result = eg.functions.betweenness_centrality(self.undirected, normalized=False) + self.assertEqual(len(result), len(self.undirected.nodes)) + + def test_subset_sources(self): + result = eg.functions.betweenness_centrality(self.undirected, sources=[1, 2]) + self.assertEqual(len(result), len(self.undirected.nodes)) + + def test_parallel_workers(self): + result = eg.functions.betweenness_centrality(self.undirected, n_workers=2) + self.assertEqual(len(result), len(self.undirected.nodes)) + + def test_multigraph_error(self): + G = eg.MultiGraph() + G.add_edges_from([(0, 1), (0, 1)]) + with self.assertRaises(eg.EasyGraphNotImplemented): + eg.functions.betweenness_centrality(G) + + def test_all_nodes_type_mix(self): + G = eg.Graph() + G.add_edges_from([(1, 2), ("A", "B"), ((1, 2), (3, 4))]) + result = eg.functions.betweenness_centrality(G) + self.assertEqual(len(result), len(G.nodes)) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/centrality/tests/test_closeness.py b/easygraph/functions/centrality/tests/test_closeness.py index c95a9950..af048602 100644 --- a/easygraph/functions/centrality/tests/test_closeness.py +++ b/easygraph/functions/centrality/tests/test_closeness.py @@ -2,6 +2,9 @@ import easygraph as eg +from easygraph.classes.multigraph import MultiGraph +from easygraph.functions.centrality import closeness_centrality + class Test_closeness(unittest.TestCase): def setUp(self): @@ -17,10 +20,66 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] self.test_graphs.append(eg.classes.DiGraph(self.edges)) + self.simple_graph = eg.Graph() + self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + self.weighted_graph = eg.Graph() + self.weighted_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + for u, v, data in self.weighted_graph.edges: + data["weight"] = 2 + + self.disconnected_graph = eg.Graph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) + + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(42) + + self.mixed_nodes_graph = eg.Graph() + self.mixed_nodes_graph.add_edges_from([(1, 2), ("X", "Y"), ((1, 2), (3, 4))]) + def test_closeness(self): for i in self.test_graphs: - print(i.nodes) - print(eg.functions.closeness_centrality(i)) + result = closeness_centrality(i) + self.assertEqual(len(result), len(i)) + + def test_simple_graph(self): + result = closeness_centrality(self.simple_graph) + self.assertEqual(len(result), len(self.simple_graph)) + self.assertTrue(all(isinstance(x, float) for x in result)) + + def test_directed_graph(self): + result = closeness_centrality(self.directed_graph) + self.assertEqual(len(result), len(self.directed_graph)) + + def test_weighted_graph(self): + result = closeness_centrality(self.weighted_graph, weight="weight") + self.assertEqual(len(result), len(self.weighted_graph)) + + def test_disconnected_graph(self): + result = closeness_centrality(self.disconnected_graph) + self.assertEqual(len(result), len(self.disconnected_graph)) + self.assertTrue(all(v <= 1.0 for v in result)) + + def test_single_node_graph(self): + result = closeness_centrality(self.single_node_graph) + self.assertEqual(result, [0.0]) + + def test_mixed_node_types(self): + result = closeness_centrality(self.mixed_nodes_graph) + self.assertEqual(len(result), len(self.mixed_nodes_graph)) + + def test_parallel_workers(self): + result = closeness_centrality(self.simple_graph, n_workers=2) + self.assertEqual(len(result), len(self.simple_graph)) + + def test_multigraph_raises(self): + G = MultiGraph() + G.add_edges_from([(0, 1), (0, 1)]) + with self.assertRaises(eg.EasyGraphNotImplemented): + closeness_centrality(G) if __name__ == "__main__": diff --git a/easygraph/functions/centrality/tests/test_degree.py b/easygraph/functions/centrality/tests/test_degree.py index d108c282..24a3079d 100644 --- a/easygraph/functions/centrality/tests/test_degree.py +++ b/easygraph/functions/centrality/tests/test_degree.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class Test_degree(unittest.TestCase): def setUp(self): @@ -17,6 +19,24 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] self.test_graphs.append(eg.classes.DiGraph(self.edges)) + self.undirected_graph = eg.Graph() + self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + # Directed graph + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + # Single-node graph + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(0) + + # Empty graph + self.empty_graph = eg.Graph() + + # Multigraph + self.multigraph = eg.MultiGraph() + self.multigraph.add_edges_from([(0, 1), (0, 1)]) + def test_degree(self): for i in self.test_graphs: print(i.edges) @@ -24,6 +44,35 @@ def test_degree(self): print(eg.functions.in_degree_centrality(i)) print(eg.functions.out_degree_centrality(i)) + def test_degree_centrality_undirected(self): + result = eg.functions.degree_centrality(self.undirected_graph) + self.assertEqual(len(result), len(self.undirected_graph)) + self.assertTrue(all(isinstance(v, float) for v in result.values())) + + def test_degree_centrality_directed(self): + result = eg.functions.degree_centrality(self.directed_graph) + self.assertEqual(len(result), len(self.directed_graph)) + + def test_degree_centrality_single_node(self): + result = eg.functions.degree_centrality(self.single_node_graph) + self.assertEqual(result, {0: 1}) + + def test_degree_centrality_empty_graph(self): + result = eg.functions.degree_centrality(self.empty_graph) + self.assertEqual(result, {}) + + def test_in_out_degree_centrality_directed(self): + in_deg = eg.functions.in_degree_centrality(self.directed_graph) + out_deg = eg.functions.out_degree_centrality(self.directed_graph) + self.assertEqual(len(in_deg), len(self.directed_graph)) + self.assertEqual(len(out_deg), len(self.directed_graph)) + + def test_in_out_degree_centrality_single_node(self): + G = eg.DiGraph() + G.add_node(1) + self.assertEqual(eg.functions.in_degree_centrality(G), {1: 1}) + self.assertEqual(eg.functions.out_degree_centrality(G), {1: 1}) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/centrality/tests/test_egobetweenness.py b/easygraph/functions/centrality/tests/test_egobetweenness.py index 42b53f34..48e7024f 100644 --- a/easygraph/functions/centrality/tests/test_egobetweenness.py +++ b/easygraph/functions/centrality/tests/test_egobetweenness.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class Test_egobetweenness(unittest.TestCase): def setUp(self): @@ -18,9 +20,54 @@ def setUp(self): self.test_graphs.append(eg.classes.DiGraph(self.edges)) print(self.test_graphs[-1].edges) + self.graph = eg.Graph() + self.graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)]) + + self.mixed_nodes_graph = eg.Graph() + self.mixed_nodes_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")]) + + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(42) + + self.disconnected_graph = eg.Graph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) # two components + + self.multigraph = eg.MultiGraph() + self.multigraph.add_edges_from([(0, 1), (0, 1)]) # parallel edges + def test_egobetweenness(self): print(eg.functions.ego_betweenness(self.test_graphs[-1], 4)) + def test_small_undirected_graph(self): + result = eg.functions.ego_betweenness(self.graph, 1) + self.assertIsInstance(result, float) + self.assertGreaterEqual(result, 0) + + def test_directed_graph(self): + result = eg.functions.ego_betweenness(self.directed_graph, 0) + self.assertIsInstance(result, int) + + def test_mixed_node_types(self): + result = eg.functions.ego_betweenness(self.mixed_nodes_graph, "A") + self.assertIsInstance(result, float) + + def test_single_node_graph(self): + result = eg.functions.ego_betweenness(self.single_node_graph, 42) + self.assertEqual(result, 0.0) + + def test_disconnected_graph_component(self): + result_0 = eg.functions.ego_betweenness(self.disconnected_graph, 0) + result_2 = eg.functions.ego_betweenness(self.disconnected_graph, 2) + self.assertIsInstance(result_0, float) + self.assertIsInstance(result_2, float) + + def test_raises_on_multigraph(self): + with self.assertRaises(EasyGraphNotImplemented): + eg.functions.ego_betweenness(self.multigraph, 0) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/centrality/tests/test_flowbetweenness.py b/easygraph/functions/centrality/tests/test_flowbetweenness.py index 9a455366..82759aa5 100644 --- a/easygraph/functions/centrality/tests/test_flowbetweenness.py +++ b/easygraph/functions/centrality/tests/test_flowbetweenness.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class Test_flowbetweenness(unittest.TestCase): def setUp(self): @@ -17,11 +19,72 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] self.test_graphs.append(eg.classes.DiGraph(self.edges)) + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from( + [ + (0, 1, {"weight": 3}), + (1, 2, {"weight": 1}), + (0, 2, {"weight": 1}), + (2, 3, {"weight": 2}), + (1, 3, {"weight": 4}), + ] + ) + + self.graph_with_self_loop = eg.DiGraph() + self.graph_with_self_loop.add_edges_from([(0, 1), (1, 2), (2, 2), (2, 3)]) + + self.disconnected_graph = eg.DiGraph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) + + self.undirected_graph = eg.Graph() + self.undirected_graph.add_edges_from([(0, 1), (1, 2)]) + + self.single_node_graph = eg.DiGraph() + self.single_node_graph.add_node(0) + + self.mixed_type_graph = eg.DiGraph() + self.mixed_type_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")]) + + self.multigraph = eg.MultiDiGraph() + self.multigraph.add_edges_from([(0, 1), (0, 1)]) + def test_flowbetweenness_centrality(self): for i in self.test_graphs: print(i.edges) print(eg.functions.flowbetweenness_centrality(i)) + def test_flowbetweenness_on_directed(self): + result = eg.functions.flowbetweenness_centrality(self.directed_graph) + self.assertIsInstance(result, dict) + self.assertTrue( + all(isinstance(v, float) or isinstance(v, int) for v in result.values()) + ) + + def test_flowbetweenness_on_self_loop(self): + result = eg.functions.flowbetweenness_centrality(self.graph_with_self_loop) + self.assertIsInstance(result, dict) + + def test_flowbetweenness_on_disconnected(self): + result = eg.functions.flowbetweenness_centrality(self.disconnected_graph) + self.assertIsInstance(result, dict) + + def test_flowbetweenness_on_single_node(self): + result = eg.functions.flowbetweenness_centrality(self.single_node_graph) + self.assertIsInstance(result, dict) + self.assertEqual(result, {0: 0}) + + def test_flowbetweenness_on_mixed_types(self): + result = eg.functions.flowbetweenness_centrality(self.mixed_type_graph) + self.assertIsInstance(result, dict) + + def test_flowbetweenness_on_undirected_warns(self): + result = eg.functions.flowbetweenness_centrality(self.undirected_graph) + self.assertIsNone(result) + + def test_flowbetweenness_raises_on_multigraph(self): + with self.assertRaises(EasyGraphNotImplemented): + eg.functions.flowbetweenness_centrality(self.multigraph) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/centrality/tests/test_laplacian.py b/easygraph/functions/centrality/tests/test_laplacian.py index 37261eb7..d7dda7ea 100644 --- a/easygraph/functions/centrality/tests/test_laplacian.py +++ b/easygraph/functions/centrality/tests/test_laplacian.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class Test_laplacian(unittest.TestCase): def setUp(self): @@ -16,12 +18,89 @@ def setUp(self): ] self.test_graphs = [eg.Graph(), eg.DiGraph()] self.test_graphs.append(eg.classes.DiGraph(self.edges)) + self.weighted_graph = eg.Graph() + self.weighted_graph.add_edges_from( + [ + (0, 1, {"weight": 2}), + (1, 2, {"weight": 3}), + (2, 3, {"weight": 4}), + (3, 0, {"weight": 1}), + ] + ) + + self.unweighted_graph = eg.Graph() + self.unweighted_graph.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 3), + ] + ) + + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from( + [ + (0, 1, {"weight": 2}), + (1, 2, {"weight": 1}), + (2, 0, {"weight": 3}), + ] + ) + + self.self_loop_graph = eg.Graph() + self.self_loop_graph.add_edges_from( + [ + (0, 0, {"weight": 2}), + (0, 1, {"weight": 1}), + ] + ) + + self.mixed_type_graph = eg.Graph() + self.mixed_type_graph.add_edges_from( + [ + ("A", "B"), + ("B", (1, 2)), + ] + ) + + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(42) + + self.multigraph = eg.MultiGraph() + self.multigraph.add_edges_from([(0, 1), (0, 1)]) def test_laplacian(self): for i in self.test_graphs: print(i.edges) print(eg.functions.laplacian(i)) + def test_weighted_graph(self): + result = eg.functions.laplacian(self.weighted_graph) + self.assertEqual(set(result.keys()), set(self.weighted_graph.nodes)) + + def test_unweighted_graph(self): + result = eg.functions.laplacian(self.unweighted_graph) + self.assertEqual(set(result.keys()), set(self.unweighted_graph.nodes)) + + def test_directed_graph(self): + result = eg.functions.laplacian(self.directed_graph) + self.assertEqual(set(result.keys()), set(self.directed_graph.nodes)) + + def test_self_loop_graph(self): + result = eg.functions.laplacian(self.self_loop_graph) + self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes)) + + def test_mixed_node_types(self): + result = eg.functions.laplacian(self.mixed_type_graph) + self.assertEqual(set(result.keys()), set(self.mixed_type_graph.nodes)) + + def test_single_node_graph(self): + result = eg.functions.laplacian(self.single_node_graph) + self.assertEqual(result, {}) + + def test_multigraph_raises(self): + with self.assertRaises(EasyGraphNotImplemented): + eg.functions.laplacian(self.multigraph) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/centrality/tests/test_pagerank.py b/easygraph/functions/centrality/tests/test_pagerank.py index 8b687898..6b1cfe8a 100644 --- a/easygraph/functions/centrality/tests/test_pagerank.py +++ b/easygraph/functions/centrality/tests/test_pagerank.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class Test_pagerank(unittest.TestCase): def setUp(self): @@ -14,6 +16,26 @@ def setUp(self): ((None, None), (None, None)), ] self.g = eg.classes.DiGraph(edges) + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)]) + + self.undirected_graph = eg.Graph() + self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 0)]) + + self.disconnected_graph = eg.DiGraph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) + + self.self_loop_graph = eg.DiGraph() + self.self_loop_graph.add_edges_from([(0, 0), (0, 1), (1, 2)]) + + self.mixed_graph = eg.DiGraph() + self.mixed_graph.add_edges_from([("A", "B"), ("B", "C"), ("C", (1, 2))]) + + self.single_node_graph = eg.DiGraph() + self.single_node_graph.add_node("solo") + + self.multigraph = eg.MultiDiGraph() + self.multigraph.add_edges_from([(0, 1), (0, 1)]) def test_pagerank(self): test_graphs = [eg.Graph(), eg.DiGraph()] @@ -30,6 +52,39 @@ def test_google_matrix(self): print(eg.functions.pagerank.(g)) """ + def test_directed_graph(self): + result = eg.functions.pagerank(self.directed_graph) + self.assertEqual(set(result.keys()), set(self.directed_graph.nodes)) + + def test_undirected_graph(self): + result = eg.functions.pagerank(self.undirected_graph) + self.assertEqual(set(result.keys()), set(self.undirected_graph.nodes)) + + def test_disconnected_graph(self): + result = eg.functions.pagerank(self.disconnected_graph) + self.assertEqual(set(result.keys()), set(self.disconnected_graph.nodes)) + + def test_self_loop_graph(self): + result = eg.functions.pagerank(self.self_loop_graph) + self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes)) + + def test_mixed_node_types(self): + result = eg.functions.pagerank(self.mixed_graph) + self.assertEqual(set(result.keys()), set(self.mixed_graph.nodes)) + + def test_single_node_graph(self): + result = eg.functions.pagerank(self.single_node_graph) + self.assertEqual(result, {"solo": 1.0}) + + def test_empty_graph(self): + empty_graph = eg.DiGraph() + result = eg.functions.pagerank(empty_graph) + self.assertEqual(result, {}) + + def test_multigraph_raises(self): + with self.assertRaises(EasyGraphNotImplemented): + eg.functions.pagerank(self.multigraph) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/community/louvain.py b/easygraph/functions/community/louvain.py index 6b102dd7..b8f65001 100644 --- a/easygraph/functions/community/louvain.py +++ b/easygraph/functions/community/louvain.py @@ -85,6 +85,8 @@ def louvain_communities(G, weight="weight", threshold=0.00002): -------- louvain_partitions """ + if len(G) == 0 or G.size(weight=weight) == 0: + return [{n} for n in G.nodes] d = louvain_partitions(G, weight, threshold) q = deque(d, maxlen=1) # q.append(d) @@ -129,6 +131,9 @@ def louvain_partitions(G, weight="weight", threshold=0.0000001): -------- louvain_communities """ + if len(G) == 0 or G.size(weight=weight) == 0: + yield [{n} for n in G.nodes] + return partition = [{u} for u in G.nodes] mod = modularity(G, partition) is_directed = G.is_directed() diff --git a/easygraph/functions/community/motif.py b/easygraph/functions/community/motif.py index 24ad59a0..fde69cf0 100644 --- a/easygraph/functions/community/motif.py +++ b/easygraph/functions/community/motif.py @@ -117,4 +117,6 @@ def random_extend_subgraph( VpExtension = Vextension | {u for u in NexclwVsubgraph if u > v} if random.random() > cut_prob[len(Vsubgraph)]: continue - random_extend_subgraph(G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs) + random_extend_subgraph( + G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs, cut_prob + ) diff --git a/easygraph/functions/community/tests/test_LPA.py b/easygraph/functions/community/tests/test_LPA.py new file mode 100644 index 00000000..34c96fb0 --- /dev/null +++ b/easygraph/functions/community/tests/test_LPA.py @@ -0,0 +1,65 @@ +import unittest + +import easygraph as eg + + +class TestLabelPropagationAlgorithms(unittest.TestCase): + def setUp(self): + self.graph_simple = eg.Graph() + self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)]) + + self.graph_weighted = eg.Graph() + self.graph_weighted.add_edges_from( + [ + (0, 1, {"weight": 3}), + (1, 2, {"weight": 2}), + (2, 0, {"weight": 4}), + (3, 4, {"weight": 1}), + ] + ) + + self.graph_disconnected = eg.Graph() + self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)]) + + self.graph_single_node = eg.Graph() + self.graph_single_node.add_node(42) + + self.graph_empty = eg.Graph() + + def test_lpa(self): + self.assertEqual(eg.functions.community.LPA(self.graph_single_node), {1: [42]}) + self.assertTrue(eg.functions.community.LPA(self.graph_simple)) + self.assertTrue(eg.functions.community.LPA(self.graph_weighted)) + self.assertTrue(eg.functions.community.LPA(self.graph_disconnected)) + + def test_slpa(self): + self.assertEqual( + eg.functions.community.SLPA(self.graph_single_node, T=5, r=0.01), {1: [42]} + ) + self.assertTrue(eg.functions.community.SLPA(self.graph_simple, T=10, r=0.1)) + self.assertTrue( + eg.functions.community.SLPA(self.graph_disconnected, T=15, r=0.1) + ) + + def test_hanp(self): + self.assertEqual( + eg.functions.community.HANP(self.graph_single_node, m=0.1, delta=0.05), + {1: [42]}, + ) + self.assertTrue( + eg.functions.community.HANP(self.graph_simple, m=0.3, delta=0.1) + ) + self.assertTrue( + eg.functions.community.HANP(self.graph_weighted, m=0.5, delta=0.2) + ) + + def test_bmlpa(self): + self.assertEqual( + eg.functions.community.BMLPA(self.graph_single_node, p=0.1), {1: [42]} + ) + self.assertTrue(eg.functions.community.BMLPA(self.graph_simple, p=0.3)) + self.assertTrue(eg.functions.community.BMLPA(self.graph_weighted, p=0.2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/community/tests/test_ego_graph.py b/easygraph/functions/community/tests/test_ego_graph.py new file mode 100644 index 00000000..e0cdbb70 --- /dev/null +++ b/easygraph/functions/community/tests/test_ego_graph.py @@ -0,0 +1,65 @@ +import unittest + +import easygraph as eg + + +class TestEgoGraph(unittest.TestCase): + def setUp(self): + self.simple_graph = eg.Graph() + self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)]) + + self.directed_graph = eg.DiGraph() + self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + + self.weighted_graph = eg.Graph() + self.weighted_graph.add_edges_from( + [(0, 1, {"weight": 1}), (1, 2, {"weight": 2}), (2, 3, {"weight": 3})] + ) + + self.disconnected_graph = eg.Graph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) + + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(42) + + def test_simple_graph_radius_1(self): + ego = eg.functions.community.ego_graph(self.simple_graph, 2, radius=1) + self.assertSetEqual(set(ego.nodes), {1, 2, 3}) + + def test_simple_graph_radius_2(self): + ego = eg.functions.community.ego_graph(self.simple_graph, 2, radius=2) + self.assertSetEqual(set(ego.nodes), {0, 1, 2, 3, 4}) + + def test_directed_graph(self): + ego = eg.functions.community.ego_graph(self.directed_graph, 1, radius=1) + self.assertSetEqual(set(ego.nodes), {1, 2}) + + def test_weighted_graph_with_distance(self): + ego = eg.functions.community.ego_graph( + self.weighted_graph, 0, radius=2, distance="weight" + ) + self.assertSetEqual(set(ego.nodes), {0, 1}) + + def test_disconnected_graph(self): + ego = eg.functions.community.ego_graph(self.disconnected_graph, 0, radius=1) + self.assertSetEqual(set(ego.nodes), {0, 1}) + + def test_single_node_graph(self): + ego = eg.functions.community.ego_graph(self.single_node_graph, 42, radius=1) + self.assertSetEqual(set(ego.nodes), {42}) + + def test_center_false(self): + ego = eg.functions.community.ego_graph( + self.simple_graph, 2, radius=1, center=False + ) + self.assertSetEqual(set(ego.nodes), {1, 3}) + + def test_empty_graph(self): + G = eg.Graph() + G.add_node("x") + ego = eg.functions.community.ego_graph(G, "x", radius=1) + self.assertSetEqual(set(ego.nodes), {"x"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/community/tests/test_louvian.py b/easygraph/functions/community/tests/test_louvian.py new file mode 100644 index 00000000..31f11ad4 --- /dev/null +++ b/easygraph/functions/community/tests/test_louvian.py @@ -0,0 +1,65 @@ +import unittest + +import easygraph as eg + + +class TestLouvainCommunityDetection(unittest.TestCase): + def setUp(self): + self.graph_simple = eg.Graph() + self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)]) + + self.graph_weighted = eg.Graph() + self.graph_weighted.add_edges_from( + [(0, 1, {"weight": 5}), (1, 2, {"weight": 3}), (3, 4, {"weight": 2})] + ) + + self.graph_directed = eg.DiGraph() + self.graph_directed.add_edges_from([(0, 1), (1, 2), (2, 0), (3, 4)]) + + self.graph_disconnected = eg.Graph() + self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)]) + + self.graph_single_node = eg.Graph() + self.graph_single_node.add_node(42) + + self.graph_empty = eg.Graph() + + def test_louvain_communities_simple(self): + communities = eg.functions.community.louvain_communities(self.graph_simple) + flat = {node for comm in communities for node in comm} + self.assertSetEqual(flat, set(self.graph_simple.nodes)) + + def test_louvain_communities_weighted(self): + communities = eg.functions.community.louvain_communities( + self.graph_weighted, weight="weight" + ) + flat = {node for comm in communities for node in comm} + self.assertSetEqual(flat, set(self.graph_weighted.nodes)) + + def test_louvain_communities_disconnected(self): + communities = eg.functions.community.louvain_communities( + self.graph_disconnected + ) + flat = {node for comm in communities for node in comm} + self.assertSetEqual(flat, set(self.graph_disconnected.nodes)) + + def test_louvain_communities_single_node(self): + communities = eg.functions.community.louvain_communities(self.graph_single_node) + self.assertEqual(len(communities), 1) + self.assertSetEqual(communities[0], {42}) + + def test_louvain_communities_empty_graph(self): + communities = eg.functions.community.louvain_communities(self.graph_empty) + self.assertEqual(communities, []) + + def test_louvain_partitions_progressive_size(self): + partitions = list(eg.functions.community.louvain_partitions(self.graph_simple)) + for partition in partitions: + total_nodes = sum(len(p) for p in partition) + self.assertEqual(total_nodes, len(self.graph_simple.nodes)) + flat = [node for part in partition for node in part] + self.assertEqual(len(flat), len(set(flat))) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/community/tests/test_modularity.py b/easygraph/functions/community/tests/test_modularity.py new file mode 100644 index 00000000..5ca1fd6e --- /dev/null +++ b/easygraph/functions/community/tests/test_modularity.py @@ -0,0 +1,71 @@ +import unittest + +import easygraph as eg + + +class TestModularity(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + self.G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) + + self.DG = eg.DiGraph() + self.DG.add_edges_from([(0, 1), (1, 2), (2, 0)]) + + self.G_weighted = eg.Graph() + self.G_weighted.add_edge(0, 1, weight=2) + self.G_weighted.add_edge(1, 2, weight=3) + self.G_weighted.add_edge(2, 0, weight=1) + + self.G_selfloop = eg.Graph() + self.G_selfloop.add_edges_from([(0, 0), (1, 1), (0, 1)]) + + self.G_empty = eg.Graph() + + def test_undirected_modularity(self): + communities = [{0, 1}, {2, 3}] + q = eg.functions.community.modularity(self.G, communities) + self.assertIsInstance(q, float) + + def test_directed_modularity(self): + communities = [{0, 1, 2}] + q = eg.functions.community.modularity(self.DG, communities) + self.assertIsInstance(q, float) + + def test_weighted_graph(self): + communities = [{0, 1}, {2}] + q = eg.functions.community.modularity( + self.G_weighted, communities, weight="weight" + ) + self.assertIsInstance(q, float) + + def test_self_loops(self): + communities = [{0, 1}] + q = eg.functions.community.modularity(self.G_selfloop, communities) + self.assertIsInstance(q, float) + + def test_single_community(self): + communities = [{0, 1, 2, 3}] + q = eg.functions.community.modularity(self.G, communities) + self.assertIsInstance(q, float) + + def test_each_node_its_own_community(self): + communities = [{0}, {1}, {2}, {3}] + q = eg.functions.community.modularity(self.G, communities) + self.assertIsInstance(q, float) + + def test_empty_graph(self): + with self.assertRaises(ZeroDivisionError): + eg.functions.community.modularity(self.G_empty, []) + + def test_empty_community_list(self): + q = eg.functions.community.modularity(self.G, []) + self.assertEqual(q, 0.0) + + def test_non_list_communities(self): + communities = (set([0, 1]), set([2, 3])) + q = eg.functions.community.modularity(self.G, communities) + self.assertIsInstance(q, float) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/community/tests/test_modularity_max_detection.py b/easygraph/functions/community/tests/test_modularity_max_detection.py new file mode 100644 index 00000000..a7ffbe1f --- /dev/null +++ b/easygraph/functions/community/tests/test_modularity_max_detection.py @@ -0,0 +1,81 @@ +import unittest + +import easygraph as eg + + +class TestGreedyModularityCommunities(unittest.TestCase): + def setUp(self): + # A simple connected graph + self.graph_simple = eg.Graph() + self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)]) + + # A weighted graph + self.graph_weighted = eg.Graph() + self.graph_weighted.add_edges_from( + [(0, 1, {"weight": 3}), (1, 2, {"weight": 2}), (3, 4, {"weight": 1})] + ) + + # A fully connected graph (clique) + self.graph_clique = eg.Graph() + self.graph_clique.add_edges_from([(0, 1), (0, 2), (1, 2)]) + + # A disconnected graph + self.graph_disconnected = eg.Graph() + self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)]) + + # A graph with a single node + self.graph_single_node = eg.Graph() + self.graph_single_node.add_node(42) + + # An empty graph + self.graph_empty = eg.Graph() + + def test_communities_simple(self): + result = eg.functions.community.greedy_modularity_communities(self.graph_simple) + flat_nodes = {node for group in result for node in group} + self.assertSetEqual(flat_nodes, set(self.graph_simple.nodes)) + + def test_communities_weighted(self): + result = eg.functions.community.greedy_modularity_communities( + self.graph_weighted + ) + flat_nodes = {node for group in result for node in group} + self.assertSetEqual(flat_nodes, set(self.graph_weighted.nodes)) + + def test_communities_clique(self): + result = eg.functions.community.greedy_modularity_communities(self.graph_clique) + self.assertEqual(len(result), 1) + self.assertSetEqual(result[0], set(self.graph_clique.nodes)) + + def test_communities_disconnected(self): + result = eg.functions.community.greedy_modularity_communities( + self.graph_disconnected + ) + flat_nodes = {node for group in result for node in group} + self.assertSetEqual(flat_nodes, set(self.graph_disconnected.nodes)) + + def test_communities_single_node(self): + with self.assertRaises(SystemExit): + eg.functions.community.greedy_modularity_communities(self.graph_single_node) + + def test_communities_empty_graph(self): + with self.assertRaises(SystemExit): + eg.functions.community.greedy_modularity_communities(self.graph_empty) + + def test_correct_partition_disjoint(self): + result = eg.functions.community.greedy_modularity_communities( + self.graph_disconnected + ) + all_nodes = [node for group in result for node in group] + self.assertEqual(len(all_nodes), len(set(all_nodes))) + + def test_communities_sorted_by_size(self): + result = eg.functions.community.greedy_modularity_communities( + self.graph_disconnected + ) + sizes = [len(group) for group in result] + self.assertEqual(sizes, sorted(sizes, reverse=True)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/community/tests/test_motif.py b/easygraph/functions/community/tests/test_motif.py index 996cbcf0..5b5fca9f 100644 --- a/easygraph/functions/community/tests/test_motif.py +++ b/easygraph/functions/community/tests/test_motif.py @@ -1,3 +1,6 @@ +import random +import unittest + import easygraph as eg @@ -14,3 +17,73 @@ def test_esu(self): exp_res = [{1, 3, 4}, {1, 2, 3}, {1, 3, 5}, {2, 3, 5}, {2, 3, 4}, {3, 4, 5}] exp_res = [list(x) for x in exp_res] assert sorted(res) == sorted(exp_res) + + +class TestMotifEnumeration(unittest.TestCase): + def setUp(self): + # Triangle plus a tail + self.G = eg.Graph() + self.G.add_edges_from( + [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5)] # triangle # tail + ) + + def test_esu_enumeration_correct(self): + motifs = eg.enumerate_subgraph(self.G, 3) + motifs = [frozenset(m) for m in motifs] + expected = [{1, 2, 3}, {2, 3, 4}, {3, 4, 5}] + expected = [frozenset(x) for x in expected] + self.assertTrue(all(m in motifs for m in expected)) + for m in motifs: + self.assertEqual(len(m), 3) + self.assertTrue(isinstance(m, frozenset)) + + def test_empty_graph(self): + G = eg.Graph() + motifs = eg.enumerate_subgraph(G, 3) + self.assertEqual(motifs, []) + + def test_graph_smaller_than_k(self): + G = eg.Graph() + G.add_edges_from([(1, 2)]) + motifs = eg.enumerate_subgraph(G, 3) + self.assertEqual(motifs, []) + + def test_k_equals_1(self): + G = eg.Graph() + G.add_nodes_from([1, 2, 3]) + motifs = eg.enumerate_subgraph(G, 1) + expected = [{1}, {2}, {3}] + motifs = [set(m) for m in motifs] + self.assertEqual(sorted(motifs), sorted(expected)) + + def test_random_enumerate_cut_prob_valid(self): + random.seed(0) + cut_prob = [1.0] * 3 + motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob) + for m in motifs: + self.assertEqual(len(m), 3) + + def test_random_enumerate_cut_prob_invalid_length(self): + cut_prob = [1.0, 0.9] + with self.assertRaises(eg.EasyGraphError): + eg.random_enumerate_subgraph(self.G, 3, cut_prob) + + def test_random_enumerate_zero_cut_prob(self): + cut_prob = [0.0, 0.0, 0.0] + motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob) + self.assertEqual(motifs, []) + + def test_directed_graph_enumeration(self): + DG = eg.DiGraph() + DG.add_edges_from([(1, 2), (2, 3), (3, 1)]) + motifs = eg.enumerate_subgraph(DG, 3) + motifs = [set(m) for m in motifs] + self.assertIn({1, 2, 3}, motifs) + + def test_multigraph_error(self): + MG = eg.MultiGraph() + MG.add_edges_from([(1, 2), (2, 3)]) + with self.assertRaises(eg.EasyGraphNotImplemented): + eg.enumerate_subgraph(MG, 3) + with self.assertRaises(eg.EasyGraphNotImplemented): + eg.random_enumerate_subgraph(MG, 3, [1.0] * 3) diff --git a/easygraph/functions/components/tests/test_biconnected.py b/easygraph/functions/components/tests/test_biconnected.py index d534c2b1..ae542675 100644 --- a/easygraph/functions/components/tests/test_biconnected.py +++ b/easygraph/functions/components/tests/test_biconnected.py @@ -1,6 +1,13 @@ import unittest import easygraph as eg +import pytest + +from easygraph import biconnected_components +from easygraph import generator_articulation_points +from easygraph import generator_biconnected_components_edges +from easygraph import generator_biconnected_components_nodes +from easygraph import is_biconnected class Test_biconnected(unittest.TestCase): @@ -30,5 +37,56 @@ def test_generator_articulation_points(self): eg.generator_articulation_points(i) +class TestBiconnectedFunctions(unittest.TestCase): + def test_single_node(self): + G = eg.Graph() + G.add_node(1) + self.assertFalse(is_biconnected(G)) + self.assertEqual(list(biconnected_components(G)), []) + self.assertEqual(list(generator_articulation_points(G)), []) + + def test_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + self.assertFalse(is_biconnected(G)) + self.assertGreaterEqual(len(list(generator_biconnected_components_edges(G))), 1) + + def test_triangle(self): + G = eg.Graph([(0, 1), (1, 2), (2, 0)]) + self.assertTrue(is_biconnected(G)) + comps = list(biconnected_components(G)) + self.assertEqual(len(comps), 1) + self.assertEqual(set(comps[0]), {(0, 1), (1, 2), (2, 0)}) + self.assertEqual(list(generator_articulation_points(G)), []) + + def test_with_articulation_point(self): + G = eg.Graph([(0, 1), (1, 2), (1, 3)]) + self.assertFalse(is_biconnected(G)) + arts = list(generator_articulation_points(G)) + self.assertIn(1, arts) + self.assertEqual(len(arts), 1) + + def test_cycle_plus_leaf(self): + G = eg.Graph([(0, 1), (1, 2), (2, 0), (2, 3)]) + self.assertFalse(is_biconnected(G)) + arts = list(generator_articulation_points(G)) + self.assertIn(2, arts) + + def test_multiple_biconnected_components(self): + G = eg.Graph() + G.add_edges_from([(1, 2), (2, 3), (3, 1)]) # triangle + G.add_edges_from([(3, 4), (4, 5)]) # path + components = list(generator_biconnected_components_edges(G)) + self.assertEqual(len(components), 3) + nodes_comps = list(generator_biconnected_components_nodes(G)) + self.assertTrue(any({1, 2, 3}.issubset(comp) for comp in nodes_comps)) + self.assertTrue(any({4, 5}.issubset(comp) for comp in nodes_comps)) + + def test_articulation_points_multiple(self): + G = eg.Graph([(0, 1), (1, 2), (2, 3), (3, 4)]) + aps = list(generator_articulation_points(G)) + self.assertEqual(aps, [3, 2, 1]) + + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/components/tests/test_connected.py b/easygraph/functions/components/tests/test_connected.py index f26c935c..4d7db5b7 100644 --- a/easygraph/functions/components/tests/test_connected.py +++ b/easygraph/functions/components/tests/test_connected.py @@ -3,6 +3,13 @@ import easygraph as eg +from easygraph import connected_component_of_node +from easygraph import connected_components +from easygraph import connected_components_directed +from easygraph import is_connected +from easygraph import number_connected_components +from easygraph.utils.exception import EasyGraphNotImplemented + class TestConnected(unittest.TestCase): def setUp(self): @@ -30,6 +37,76 @@ def test_connected_component_of_node(self): for i in self.test_graphs: print(eg.connected_component_of_node(i, 4)) + def test_empty_graph(self): + G = eg.Graph() + with self.assertRaises(AssertionError): + is_connected(G) + self.assertEqual(number_connected_components(G), 0) + self.assertEqual(list(connected_components(G)), []) + + def test_single_node(self): + G = eg.Graph() + G.add_node(1) + self.assertTrue(is_connected(G)) + self.assertEqual(number_connected_components(G), 1) + self.assertEqual(list(connected_components(G)), [{1}]) + self.assertEqual(connected_component_of_node(G, 1), {1}) + + def test_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + self.assertFalse(is_connected(G)) + self.assertEqual(number_connected_components(G), 2) + comps = list(connected_components(G)) + self.assertTrue({0, 1} in comps and {2, 3} in comps) + + def test_connected_graph(self): + G = eg.path_graph(5) + self.assertTrue(is_connected(G)) + self.assertEqual(number_connected_components(G), 1) + comps = list(connected_components(G)) + self.assertEqual(len(comps), 1) + self.assertEqual(comps[0], set(range(5))) + + def test_node_component_lookup(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + comp = connected_component_of_node(G, 0) + self.assertEqual(comp, {0, 1}) + with self.assertRaises(KeyError): + connected_component_of_node(G, 999) # non-existent node + + def test_undirected_with_self_loops(self): + G = eg.Graph() + G.add_edges_from([(1, 1), (2, 2), (1, 2)]) + self.assertTrue(is_connected(G)) + self.assertEqual(number_connected_components(G), 1) + self.assertEqual(list(connected_components(G))[0], {1, 2}) + + def test_directed_components(self): + G = eg.DiGraph() + G.add_edges_from([(0, 1), (2, 3)]) + self.assertEqual(number_connected_components(G), 2) + components = list(connected_components_directed(G)) + self.assertTrue({0, 1} in components and {2, 3} in components) + + def test_directed_strong_vs_weak(self): + G = eg.DiGraph([(0, 1), (1, 0), (2, 3)]) + comps = list(connected_components_directed(G)) + self.assertTrue({0, 1} in comps) + self.assertTrue({2, 3} in comps) + + def test_multigraph_blocked(self): + G = eg.MultiGraph([(1, 2), (2, 3)]) + with self.assertRaises(EasyGraphNotImplemented): + is_connected(G) + with self.assertRaises(EasyGraphNotImplemented): + number_connected_components(G) + with self.assertRaises(EasyGraphNotImplemented): + list(connected_components(G)) + with self.assertRaises(EasyGraphNotImplemented): + connected_component_of_node(G, 1) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/components/tests/test_strongly_connected.py b/easygraph/functions/components/tests/test_strongly_connected.py index 32ed9fb3..904f3456 100644 --- a/easygraph/functions/components/tests/test_strongly_connected.py +++ b/easygraph/functions/components/tests/test_strongly_connected.py @@ -3,6 +3,13 @@ import easygraph as eg +from easygraph import condensation +from easygraph import is_strongly_connected +from easygraph import number_strongly_connected_components +from easygraph import strongly_connected_components +from easygraph.utils.exception import EasyGraphNotImplemented +from easygraph.utils.exception import EasyGraphPointlessConcept + class Test_strongly_connected(unittest.TestCase): def setUp(self): @@ -10,17 +17,104 @@ def setUp(self): self.test_graphs = [eg.Graph([(4, -4)]), eg.DiGraph([(4, False)])] self.test_graphs.append(eg.classes.DiGraph(self.edges)) - def test_number_strongly_connected_components(self): - pass + def test_empty_graph(self): + G = eg.DiGraph() + with self.assertRaises(EasyGraphPointlessConcept): + is_strongly_connected(G) + self.assertEqual(number_strongly_connected_components(G), 0) + self.assertEqual(list(strongly_connected_components(G)), []) + + def test_single_node(self): + G = eg.DiGraph() + G.add_node(1) + self.assertTrue(is_strongly_connected(G)) + self.assertEqual(number_strongly_connected_components(G), 1) + scc = list(strongly_connected_components(G)) + self.assertEqual(scc, [{1}]) + + def test_cycle_graph(self): + G = eg.DiGraph([(1, 2), (2, 3), (3, 1)]) + self.assertTrue(is_strongly_connected(G)) + self.assertEqual(number_strongly_connected_components(G), 1) + scc = list(strongly_connected_components(G)) + self.assertEqual(scc, [{1, 2, 3}]) + + def test_disconnected_scc(self): + G = eg.DiGraph([(0, 1), (1, 0), (2, 3), (3, 2), (4, 5)]) + scc = list(strongly_connected_components(G)) + self.assertEqual(len(scc), 4) + self.assertIn({0, 1}, scc) + self.assertIn({2, 3}, scc) + self.assertIn({4}, scc) + self.assertIn({5}, scc) + self.assertFalse(is_strongly_connected(G)) + self.assertEqual(number_strongly_connected_components(G), 4) + + def test_scc_with_self_loops(self): + G = eg.DiGraph([(1, 1), (2, 2), (3, 4), (4, 3)]) + scc = list(strongly_connected_components(G)) + self.assertEqual(len(scc), 3) + self.assertIn({1}, scc) + self.assertIn({2}, scc) + self.assertIn({3, 4}, scc) + + def test_condensation_structure(self): + G = eg.DiGraph( + [(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)] + ) + cond = condensation(G) + self.assertTrue(cond.is_directed()) + self.assertIn("mapping", cond.graph) + self.assertEqual(len(cond), number_strongly_connected_components(G)) + + def has_cycle(G): + visited = set() + temp_mark = set() + + def visit(node): + if node in temp_mark: + return True + if node in visited: + return False + temp_mark.add(node) + for neighbor in G[node]: + if visit(neighbor): + return True + temp_mark.remove(node) + visited.add(node) + return False + + return any(visit(v) for v in G) + + self.assertFalse(has_cycle(cond)) + + def test_condensation_empty_graph(self): + G = eg.DiGraph() + C = condensation(G) + self.assertEqual(len(C), 0) - def test_strongly_connected_components(self): - pass + def test_undirected_raises(self): + G = eg.Graph([(1, 2), (2, 3)]) + with self.assertRaises(EasyGraphNotImplemented): + list(strongly_connected_components(G)) + with self.assertRaises(EasyGraphNotImplemented): + is_strongly_connected(G) + with self.assertRaises(EasyGraphNotImplemented): + number_strongly_connected_components(G) - def test_is_strongly_connected(self): - pass + def test_condensation_on_undirected_graph_raises(self): + G = eg.Graph([(1, 2), (2, 3)]) + with self.assertRaises(EasyGraphNotImplemented): + condensation(G) - def test_condensation(self): - pass + def test_condensation_manual_scc_input(self): + G = eg.DiGraph([(1, 2), (2, 1), (3, 4)]) + scc = list(strongly_connected_components(G)) + C = condensation(G, scc=scc) + self.assertEqual(len(C.nodes), len(scc)) + # Check if mapping is consistent + all_mapped = set(C.graph["mapping"].keys()) + self.assertEqual(all_mapped, set(G.nodes)) if __name__ == "__main__": diff --git a/easygraph/functions/components/tests/test_weakly_connected.py b/easygraph/functions/components/tests/test_weakly_connected.py index 2bbc70af..43941f46 100644 --- a/easygraph/functions/components/tests/test_weakly_connected.py +++ b/easygraph/functions/components/tests/test_weakly_connected.py @@ -2,19 +2,80 @@ import easygraph as eg +from easygraph import is_weakly_connected +from easygraph import number_weakly_connected_components +from easygraph import weakly_connected_components +from easygraph.utils.exception import EasyGraphNotImplemented +from easygraph.utils.exception import EasyGraphPointlessConcept + class Test_weakly_connected(unittest.TestCase): - def setUp(self): - pass + def test_empty_graph(self): + G = eg.DiGraph() + with self.assertRaises(EasyGraphPointlessConcept): + is_weakly_connected(G) + self.assertEqual(number_weakly_connected_components(G), 0) + self.assertEqual(list(weakly_connected_components(G)), []) + + def test_single_node(self): + G = eg.DiGraph() + G.add_node(1) + self.assertTrue(is_weakly_connected(G)) + self.assertEqual(number_weakly_connected_components(G), 1) + self.assertEqual(list(weakly_connected_components(G)), [{1}]) + + def test_connected_graph(self): + G = eg.DiGraph([(1, 2), (2, 3), (3, 4)]) + self.assertTrue(is_weakly_connected(G)) + self.assertEqual(number_weakly_connected_components(G), 1) + self.assertEqual(list(weakly_connected_components(G)), [{1, 2, 3, 4}]) + + def test_disconnected_graph(self): + G = eg.DiGraph([(1, 2), (3, 4)]) + self.assertFalse(is_weakly_connected(G)) + wcc = list(weakly_connected_components(G)) + self.assertEqual(len(wcc), 2) + self.assertIn({1, 2}, wcc) + self.assertIn({3, 4}, wcc) + + def test_self_loops(self): + G = eg.DiGraph([(1, 1), (2, 2)]) + wcc = list(weakly_connected_components(G)) + self.assertEqual(len(wcc), 2) + self.assertIn({1}, wcc) + self.assertIn({2}, wcc) + self.assertFalse(is_weakly_connected(G)) + + def test_multiple_components(self): + G = eg.DiGraph([(1, 2), (3, 4), (5, 6), (6, 5)]) + wcc = list(weakly_connected_components(G)) + self.assertEqual(number_weakly_connected_components(G), 3) + self.assertIn({1, 2}, wcc) + self.assertIn({3, 4}, wcc) + self.assertIn({5, 6}, wcc) - def test_number_weakly_connected_components(self): - pass + def test_unconnected_nodes(self): + G = eg.DiGraph([(1, 2), (3, 4)]) + G.add_node(99) # isolated node + wcc = list(weakly_connected_components(G)) + self.assertEqual(len(wcc), 3) + self.assertIn({99}, wcc) - def test_weakly_connected_components(self): - pass + def test_is_weakly_connected_after_adding_edge(self): + G = eg.DiGraph([(0, 1), (2, 1)]) + G.add_node(3) + self.assertFalse(is_weakly_connected(G)) + G.add_edge(2, 3) + self.assertTrue(is_weakly_connected(G)) - def test_is_weakly_connected(self): - pass + def test_undirected_raises(self): + G = eg.Graph([(1, 2), (2, 3)]) + with self.assertRaises(EasyGraphNotImplemented): + is_weakly_connected(G) + with self.assertRaises(EasyGraphNotImplemented): + number_weakly_connected_components(G) + with self.assertRaises(EasyGraphNotImplemented): + list(weakly_connected_components(G)) if __name__ == "__main__": diff --git a/easygraph/functions/core/tests/test_k_core.py b/easygraph/functions/core/tests/test_k_core.py index 24dd7f26..55efcd69 100644 --- a/easygraph/functions/core/tests/test_k_core.py +++ b/easygraph/functions/core/tests/test_k_core.py @@ -1,5 +1,8 @@ +import easygraph as eg import pytest +from easygraph import k_core + @pytest.mark.parametrize( "edges,k", @@ -17,13 +20,82 @@ def test_k_core(edges, k): from easygraph import Graph from easygraph import k_core - # Create EasyGraph and NetworkX graphs from the edge list G = Graph() G_nx = nx.Graph() G.add_edges_from(edges) G_nx.add_edges_from(edges) - # Compute the k-core of the graphs using the k_core function and nx.k_core H = k_core(G) - H_nx = nx.core_number(G_nx) # type: ignore + H_nx = nx.core_number(G_nx) assert H == list(H_nx.values()) + + +def test_k_core_empty_graph(): + G = eg.Graph() + result = k_core(G) + assert result == [] + + +def test_k_core_single_node_isolated(): + G = eg.Graph() + G.add_node(1) + result = k_core(G) + assert result == [0] + + +def test_k_core_clique(): + G = eg.complete_graph(5) # Each node has degree 4 + result = k_core(G) + assert set(result) == {4} + + +def test_k_core_star_graph(): + nx = pytest.importorskip("networkx") + G = eg.Graph() + G.add_node(0) + G.add_edges_from((0, i) for i in range(1, 6)) + result = k_core(G) + G_nx = nx.Graph() + G_nx.add_node(0) + G_nx.add_edges_from((0, i) for i in range(1, 6)) + expected = list(nx.core_number(G_nx).values()) + assert sorted(result) == sorted(expected) + + +def test_k_core_disconnected_components(): + G = eg.Graph() + # Component 1: triangle + G.add_edges_from([(0, 1), (1, 2), (2, 0)]) + # Component 2: line + G.add_edges_from([(3, 4)]) + result = k_core(G) + core_component_1 = {result[i] for i in [0, 1, 2]} + core_component_2 = {result[i] for i in [3, 4]} + assert core_component_1 == {2} + assert core_component_2 == {1} + + +def test_k_core_all_zero_core(): + G = eg.path_graph(5) + result = k_core(G) + assert all(isinstance(v, int) or isinstance(v, float) for v in result) + assert max(result) <= 2 + + +def test_k_core_index_to_node_mapping_consistency(): + G = eg.Graph() + edges = [(5, 10), (10, 15), (15, 20)] + G.add_edges_from(edges) + result = k_core(G) + for i, node in enumerate(G.index2node): + assert isinstance(result[i], (int, float)) + deg_map = dict(G.degree()) + if node in deg_map: + assert result[i] <= deg_map[node] + + +def test_k_core_large_k(): + G = eg.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + result = k_core(G) + assert max(result) <= 2 diff --git a/easygraph/functions/drawing/tests/test_geometry.py b/easygraph/functions/drawing/tests/test_geometry.py new file mode 100644 index 00000000..4b155dc3 --- /dev/null +++ b/easygraph/functions/drawing/tests/test_geometry.py @@ -0,0 +1,78 @@ +import math +import unittest + +import numpy as np + +from easygraph.functions.drawing.geometry import common_tangent_radian +from easygraph.functions.drawing.geometry import polar_position +from easygraph.functions.drawing.geometry import rad_2_deg +from easygraph.functions.drawing.geometry import radian_from_atan +from easygraph.functions.drawing.geometry import vlen + + +class TestGeometryUtils(unittest.TestCase): + def test_radian_from_atan_axes(self): + self.assertAlmostEqual(radian_from_atan(0, 1), math.pi / 2) + self.assertAlmostEqual(radian_from_atan(0, -1), 3 * math.pi / 2) + self.assertAlmostEqual(radian_from_atan(1, 0), 0) + self.assertAlmostEqual(radian_from_atan(-1, 0), math.pi) + + def test_radian_from_atan_quadrants(self): + # Q1 + self.assertAlmostEqual(radian_from_atan(1, 1), math.atan(1)) + # Q4 + self.assertAlmostEqual(radian_from_atan(1, -1), math.atan(-1) + 2 * math.pi) + # Q2 + self.assertAlmostEqual(radian_from_atan(-1, 1), math.atan(-1) + math.pi) + # Q3 + self.assertAlmostEqual(radian_from_atan(-1, -1), math.atan(1) + math.pi) + + def test_radian_from_atan_zero_vector(self): + result = radian_from_atan(0, 0) + self.assertAlmostEqual(result, 3 * math.pi / 2) + + def test_vlen(self): + self.assertEqual(vlen((3, 4)), 5.0) + self.assertEqual(vlen((0, 0)), 0.0) + self.assertAlmostEqual(vlen((-3, -4)), 5.0) + + def test_common_tangent_radian_basic(self): + r1, r2, d = 3, 2, 5 + angle = common_tangent_radian(r1, r2, d) + expected = math.acos(abs(r2 - r1) / d) + self.assertAlmostEqual(angle, expected) + + def test_common_tangent_radian_reversed(self): + r1, r2, d = 2, 3, 5 + angle = common_tangent_radian(r1, r2, d) + expected = math.pi - math.acos(abs(r2 - r1) / d) + self.assertAlmostEqual(angle, expected) + + def test_common_tangent_radian_touching(self): + self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2) + + def test_common_tangent_radian_invalid(self): + with self.assertRaises(ValueError): + common_tangent_radian(5, 1, 2) + + def test_polar_position_origin(self): + pos = polar_position(0, 0, np.array([5, 5])) + np.testing.assert_array_almost_equal(pos, np.array([5, 5])) + + def test_polar_position_90deg(self): + pos = polar_position(1, math.pi / 2, np.array([0, 0])) + np.testing.assert_array_almost_equal(pos, np.array([0, 1])) + + def test_polar_position_negative_angle(self): + pos = polar_position(1, -math.pi / 2, np.array([1, 1])) + np.testing.assert_array_almost_equal(pos, np.array([1, 0])) + + def test_rad_2_deg(self): + self.assertEqual(rad_2_deg(0), 0) + self.assertEqual(rad_2_deg(math.pi), 180) + self.assertEqual(rad_2_deg(2 * math.pi), 360) + self.assertEqual(rad_2_deg(-math.pi / 2), -90) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/graph_embedding/NOBE.py b/easygraph/functions/graph_embedding/NOBE.py index 7533ce8d..80fae94c 100644 --- a/easygraph/functions/graph_embedding/NOBE.py +++ b/easygraph/functions/graph_embedding/NOBE.py @@ -47,6 +47,7 @@ def NOBE(G, K): @not_implemented_for("multigraph") +@only_implemented_for_UnDirected_graph def NOBE_GA(G, K): """Graph embedding via NOBE-GA[1]. diff --git a/easygraph/functions/graph_embedding/line.py b/easygraph/functions/graph_embedding/line.py index c857c1d7..c750fd48 100644 --- a/easygraph/functions/graph_embedding/line.py +++ b/easygraph/functions/graph_embedding/line.py @@ -135,7 +135,7 @@ def forward(self, g, return_dict=True): self.G = g self.is_directed = g.is_directed() - self.num_node = g.size() + self.num_node = len(g.nodes) self.num_edge = g.number_of_edges() self.num_sampling_edge = self.walk_length * self.walk_num * self.num_node @@ -190,9 +190,9 @@ def forward(self, g, return_dict=True): for vid, node in enumerate(g.nodes): features_matrix[node] = embeddings[vid] else: - features_matrix = np.zeros((g.num_nodes, embeddings.shape[1])) - nx_nodes = g.nodes() - features_matrix[nx_nodes] = embeddings[np.arange(g.num_nodes)] + features_matrix = np.zeros((len(g.nodes), embeddings.shape[1])) + nx_nodes = list(g.nodes) + features_matrix[nx_nodes] = embeddings[np.arange(len(g.nodes))] return features_matrix def _update(self, vec_u, vec_v, vec_error, label): diff --git a/easygraph/functions/graph_embedding/tests/test.emb b/easygraph/functions/graph_embedding/tests/test.emb new file mode 100644 index 00000000..edd65208 --- /dev/null +++ b/easygraph/functions/graph_embedding/tests/test.emb @@ -0,0 +1,4 @@ +-1.765814423561096191e-01 2.083084881305694580e-01 -1.271556913852691650e-01 -1.702362895011901855e-01 8.119292855262756348e-01 -3.134809732437133789e-01 -9.992567449808120728e-02 -1.093881502747535706e-01 +-2.064122706651687622e-01 -1.475724577903747559e-01 -1.439859867095947266e-01 -7.331190109252929688e-01 6.787545084953308105e-01 -3.651908636093139648e-01 -9.232180565595626831e-02 -8.407155275344848633e-01 +-1.765814423561096191e-01 2.083084881305694580e-01 -1.271556913852691650e-01 -1.702362895011901855e-01 8.119292855262756348e-01 -3.134809732437133789e-01 -9.992567449808120728e-02 -1.093881502747535706e-01 +-2.064122706651687622e-01 -1.475724577903747559e-01 -1.439859867095947266e-01 -7.331190109252929688e-01 6.787545084953308105e-01 -3.651908636093139648e-01 -9.232180565595626831e-02 -8.407155275344848633e-01 diff --git a/easygraph/functions/graph_embedding/tests/test_deepwalk.py b/easygraph/functions/graph_embedding/tests/test_deepwalk.py index 7042af5c..29770164 100644 --- a/easygraph/functions/graph_embedding/tests/test_deepwalk.py +++ b/easygraph/functions/graph_embedding/tests/test_deepwalk.py @@ -12,10 +12,90 @@ def setUp(self): self.test_graphs.append(eg.classes.DiGraph(self.edges)) self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3)) + self.graph = eg.Graph() + self.graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]) + + self.empty_graph = eg.Graph() + + self.single_node_graph = eg.Graph() + self.single_node_graph.add_node(0) + def test_deepwalk(self): for i in self.test_graphs: print(eg.deepwalk(i)) + def test_deepwalk_output_structure(self): + emb, sim = eg.deepwalk( + self.graph, + dimensions=16, + walk_length=5, + num_walks=3, + window=2, + min_count=1, + batch_words=4, + epochs=5, + ) + self.assertIsInstance(emb, dict) + self.assertIsInstance(sim, dict) + for k, v in emb.items(): + self.assertEqual(len(v), 16) + self.assertTrue(isinstance(v, np.ndarray)) + + def test_deepwalk_similarity_keys_match_nodes(self): + emb, sim = eg.deepwalk( + self.graph, + dimensions=8, + walk_length=3, + num_walks=2, + window=2, + min_count=1, + batch_words=2, + epochs=3, + ) + self.assertEqual(set(emb.keys()), set(sim.keys())) + self.assertEqual(set(emb.keys()), set(self.graph.nodes)) + + def test_deepwalk_on_single_node(self): + emb, sim = eg.deepwalk( + self.single_node_graph, + dimensions=4, + walk_length=2, + num_walks=1, + window=1, + min_count=1, + batch_words=2, + epochs=2, + ) + self.assertEqual(len(emb), 1) + self.assertEqual(list(emb.keys()), [0]) + self.assertEqual(len(emb[0]), 4) + + def test_deepwalk_on_empty_graph(self): + with self.assertRaises(RuntimeError): + eg.deepwalk( + self.empty_graph, + dimensions=4, + walk_length=2, + num_walks=1, + window=1, + min_count=1, + batch_words=2, + epochs=2, + ) + + def test_deepwalk_walk_length_zero(self): + emb, sim = eg.deepwalk( + self.graph, + dimensions=4, + walk_length=0, + num_walks=2, + window=1, + min_count=1, + batch_words=2, + epochs=2, + ) + self.assertEqual(len(emb), len(self.graph.nodes)) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/graph_embedding/tests/test_line.py b/easygraph/functions/graph_embedding/tests/test_line.py index c8c635cc..80bf7fb5 100644 --- a/easygraph/functions/graph_embedding/tests/test_line.py +++ b/easygraph/functions/graph_embedding/tests/test_line.py @@ -4,10 +4,74 @@ import numpy as np -class Test_Line(unittest.TestCase): +class Test_LINE(unittest.TestCase): def setUp(self): - pass + self.edges = [(0, 1), (1, 2), (2, 3), (3, 4)] + self.graph = eg.Graph() + self.graph.add_edges_from(self.edges) + def test_output_is_dict_with_correct_dim(self): + model = eg.functions.graph_embedding.LINE( + dimension=16, walk_length=10, walk_num=5, order=1 + ) + emb = model(self.graph, return_dict=True) + self.assertIsInstance(emb, dict) + for v in emb.values(): + self.assertEqual(len(v), 16) -if __name__ == "__main__": - unittest.main() + def test_output_as_matrix(self): + model = eg.functions.graph_embedding.LINE( + dimension=8, walk_length=5, walk_num=3, order=1 + ) + emb = model(self.graph, return_dict=False) + self.assertEqual(emb.shape, (len(self.graph.nodes), 8)) + + def test_output_with_order_2(self): + model = eg.functions.graph_embedding.LINE( + dimension=16, walk_length=10, walk_num=5, order=2 + ) + emb = model(self.graph) + for vec in emb.values(): + self.assertEqual(len(vec), 16) + + def test_output_with_order_3_combination(self): + model = eg.functions.graph_embedding.LINE( + dimension=16, walk_length=10, walk_num=5, order=3 + ) + emb = model(self.graph) + for vec in emb.values(): + self.assertEqual(len(vec), 16) + + def test_directed_graph(self): + g = eg.DiGraph() + g.add_edges_from(self.edges) + model = eg.functions.graph_embedding.LINE( + dimension=8, walk_length=5, walk_num=3, order=1 + ) + emb = model(g) + self.assertEqual(len(emb), len(g.nodes)) + + def test_empty_graph_raises(self): + g = eg.Graph() + model = eg.functions.graph_embedding.LINE( + dimension=8, walk_length=5, walk_num=3, order=1 + ) + with self.assertRaises(Exception): + _ = model(g) + + def test_embeddings_are_normalized(self): + model = eg.functions.graph_embedding.LINE( + dimension=16, walk_length=10, walk_num=5, order=1 + ) + emb = model(self.graph) + for vec in emb.values(): + norm = np.linalg.norm(vec) + self.assertTrue(np.isclose(norm, 1.0, atol=1e-5)) + + def test_embedding_value_finiteness(self): + model = eg.functions.graph_embedding.LINE( + dimension=16, walk_length=10, walk_num=5, order=1 + ) + emb = model(self.graph) + for vec in emb.values(): + self.assertTrue(np.all(np.isfinite(vec))) diff --git a/easygraph/functions/graph_embedding/tests/test_nobe.py b/easygraph/functions/graph_embedding/tests/test_nobe.py index b7d87f1d..0b8bd781 100644 --- a/easygraph/functions/graph_embedding/tests/test_nobe.py +++ b/easygraph/functions/graph_embedding/tests/test_nobe.py @@ -14,6 +14,13 @@ def setUp(self): self.test_directed_graphs.append(eg.classes.DiGraph(self.edges)) self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3)) + self.valid_graph = eg.Graph([(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)]) + self.directed_graph = eg.DiGraph([(0, 1), (1, 2)]) + self.graph_with_isolated = eg.Graph() + self.graph_with_isolated.add_edges_from([(0, 1), (1, 2)]) + self.graph_with_isolated.add_node(3) + self.graph_with_isolated.add_node(4) + def test_NOBE(self): fn.NOBE(self.test_undirected_graphs[0], 1) @@ -25,6 +32,26 @@ def test_NOBE_GA(self): """ fn.NOBE_GA(self.test_directed_graphs[1], 1) + def test_nobe_output_shape(self): + emb = fn.NOBE(self.valid_graph, K=2) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape[1], 2) + + def test_nobe_ga_output_shape(self): + undirected_graph = eg.Graph([(0, 1), (1, 2), (2, 3)]) + emb = fn.NOBE_GA(undirected_graph, K=2) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape[1], 2) + + def test_nobe_on_graph_with_isolated_nodes(self): + emb = fn.NOBE(self.graph_with_isolated, K=2) + self.assertEqual(emb.shape[0], len(self.graph_with_isolated)) + + def test_nobe_invalid_K_zero(self): + emb = fn.NOBE(self.valid_graph, 0) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape, (len(self.valid_graph), 0)) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/graph_embedding/tests/test_node2vec.py b/easygraph/functions/graph_embedding/tests/test_node2vec.py index 1c39948a..f579295b 100644 --- a/easygraph/functions/graph_embedding/tests/test_node2vec.py +++ b/easygraph/functions/graph_embedding/tests/test_node2vec.py @@ -15,6 +15,11 @@ def setUp(self): self.test_undirected_graphs = [eg.classes.Graph(self.edges)] self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3)) + self.valid_graph = eg.Graph([(0, 1), (1, 2), (2, 3)]) + self.directed_graph = eg.DiGraph([(0, 1), (1, 2)]) + self.graph_with_isolated = eg.Graph([(0, 1), (1, 2)]) + self.graph_with_isolated.add_node(5) # isolated node + # def test_NOBE(self): for i in self.test_graphs: @@ -24,6 +29,30 @@ def test_NOBE_GA(self): for i in self.test_undirected_graphs: NOBE_GA(i, K=1) + def test_nobe_embedding_shape(self): + emb = NOBE(self.valid_graph, K=2) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape, (len(self.valid_graph.nodes), 2)) + + def test_nobe_ga_embedding_shape(self): + emb = NOBE_GA(self.valid_graph, K=2) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape, (len(self.valid_graph.nodes), 2)) + + def test_nobe_invalid_k_zero(self): + emb = NOBE(self.valid_graph, 0) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape, (len(self.valid_graph), 0)) + + def test_nobe_ga_invalid_k_zero(self): + emb = NOBE_GA(self.valid_graph, 0) + self.assertIsInstance(emb, np.ndarray) + self.assertEqual(emb.shape, (len(self.valid_graph), 0)) + + def test_nobe_with_isolated_node(self): + emb = NOBE(self.graph_with_isolated, K=2) + self.assertEqual(emb.shape[0], len(self.graph_with_isolated)) + # if __name__ == "__main__": # unittest.main() diff --git a/easygraph/functions/graph_embedding/tests/test_sdne.py b/easygraph/functions/graph_embedding/tests/test_sdne.py index 009d67cc..1464cd53 100644 --- a/easygraph/functions/graph_embedding/tests/test_sdne.py +++ b/easygraph/functions/graph_embedding/tests/test_sdne.py @@ -1,6 +1,8 @@ import unittest import easygraph as eg +import numpy as np +import torch class Test_Sdne(unittest.TestCase): @@ -16,6 +18,9 @@ def setUp(self): self.test_graphs = [] self.test_graphs.append(eg.classes.DiGraph(self.edges)) self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3)) + self.graph = eg.DiGraph() + self.graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") def test_sdne(self): sdne = eg.SDNE( @@ -29,3 +34,74 @@ def test_sdne(self): ) # todo add test # emb = sdne.train(sdne) + + def test_sdne_model_instantiation(self): + model = eg.SDNE( + graph=self.graph, + node_size=len(self.graph.nodes), + nhid0=32, + nhid1=16, + dropout=0.05, + alpha=0.01, + beta=5.0, + ) + self.assertIsInstance(model, eg.SDNE) + + def test_sdne_training_embedding_output(self): + model = eg.SDNE( + graph=self.graph, + node_size=len(self.graph.nodes), + nhid0=16, + nhid1=8, + dropout=0.05, + alpha=0.01, + beta=5.0, + ) + embedding = model.train( + model=model, + epochs=5, + lr=0.01, + bs=2, + step_size=2, + gamma=0.9, + nu1=1e-5, + nu2=1e-4, + device=self.device, + output="test.emb", + ) + self.assertIsInstance(embedding, np.ndarray) + self.assertEqual(embedding.shape, (len(self.graph.nodes), 8)) + + def test_savector_output_shape(self): + adj, _ = eg.get_adj(self.graph) + model = eg.SDNE( + graph=self.graph, + node_size=len(self.graph.nodes), + nhid0=16, + nhid1=8, + dropout=0.05, + alpha=0.01, + beta=5.0, + ) + with torch.no_grad(): + emb = model.savector(adj) + self.assertEqual(emb.shape, (len(self.graph.nodes), 8)) + + def test_get_adj_shape_and_symmetry(self): + adj, node_count = eg.get_adj(self.graph) + self.assertEqual(adj.shape[0], node_count) + self.assertTrue(torch.equal(adj, adj.T)) # check symmetry for undirected + + def test_training_on_empty_graph(self): + empty_graph = eg.Graph() + model = eg.SDNE( + graph=empty_graph, + node_size=0, + nhid0=8, + nhid1=4, + dropout=0.05, + alpha=0.01, + beta=5.0, + ) + with self.assertRaises(ValueError): + model.train(model=model, epochs=5, device=self.device) diff --git a/easygraph/functions/graph_generator/tests/test_Random_Network.py b/easygraph/functions/graph_generator/tests/test_Random_Network.py index c9c1ed04..fca17174 100644 --- a/easygraph/functions/graph_generator/tests/test_Random_Network.py +++ b/easygraph/functions/graph_generator/tests/test_Random_Network.py @@ -22,6 +22,42 @@ def test_WS_Random(self): def test_graph_Gnm(self): print(eg.graph_Gnm(8, 5).nodes) + def test_erdos_renyi_M_max_edges(self): + n = 5 + max_edges = n * (n - 1) // 2 + G = eg.erdos_renyi_M(n, max_edges) + self.assertEqual(len(G.edges), max_edges) + + def test_erdos_renyi_P_extreme_p(self): + G0 = eg.erdos_renyi_P(10, 0.0) + G1 = eg.erdos_renyi_P(10, 1.0) + self.assertEqual(len(G0.edges), 0) + self.assertEqual(len(G1.edges), 45) # 10 * 9 / 2 + + def test_fast_erdos_renyi_P_large_p(self): + G = eg.fast_erdos_renyi_P(10, 0.9) + self.assertEqual(len(G.nodes), 10) + + def test_WS_Random_structure(self): + G = eg.WS_Random(10, 2, 0.1) + self.assertEqual(len(G.nodes), 10) + self.assertTrue(all(0 <= u < 10 and 0 <= v < 10 for u, v, *_ in G.edges)) + + def test_WS_Random_invalid_k(self): + G = eg.WS_Random(5, 5, 0.1) + self.assertIsNone(G) + + def test_graph_Gnm_basic(self): + G = eg.graph_Gnm(10, 15) + self.assertEqual(len(G.nodes), 10) + self.assertEqual(len(G.edges), 15) + + def test_graph_Gnm_invalid_inputs(self): + with self.assertRaises(AssertionError): + eg.graph_Gnm(1, 1) + with self.assertRaises(AssertionError): + eg.graph_Gnm(5, 11) # 5*4/2 = 10 max + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/graph_generator/tests/test_classic.py b/easygraph/functions/graph_generator/tests/test_classic.py index a86b2c91..36868614 100644 --- a/easygraph/functions/graph_generator/tests/test_classic.py +++ b/easygraph/functions/graph_generator/tests/test_classic.py @@ -17,6 +17,71 @@ def test_path_graph(self): def test_complete_graph(self): eg.complete_graph(10, eg.DiGraph) + def test_empty_graph_default(self): + G = eg.empty_graph() + self.assertEqual(len(G.nodes), 0) + self.assertEqual(len(G.edges), 0) + + def test_empty_graph_with_n(self): + G = eg.empty_graph(5) + self.assertEqual(set(G.nodes), set(range(5))) + self.assertEqual(len(G.edges), 0) + + def test_empty_graph_with_custom_nodes(self): + G = eg.empty_graph(["a", "b", "c"]) + self.assertEqual(set(G.nodes), {"a", "b", "c"}) + self.assertEqual(len(G.edges), 0) + + def test_empty_graph_with_existing_graph(self): + existing = eg.Graph() + existing.add_node(999) + G = eg.empty_graph(3, create_using=existing) + self.assertIn(0, G.nodes) # node 0 added + self.assertEqual(len(G.nodes), 4) # 999 is retained + self.assertEqual(len(G.edges), 0) + + def test_path_graph_basic(self): + G = eg.path_graph(4) + self.assertEqual(len(G.nodes), 4) + self.assertEqual(len(G.edges), 3) + edges = {(u, v) for u, v, _ in G.edges} + self.assertTrue((0, 1) in edges and (1, 2) in edges and (2, 3) in edges) + + def test_path_graph_with_custom_nodes(self): + G = eg.path_graph(["x", "y", "z"]) + self.assertEqual(len(G.nodes), 3) + actual_edges = {(u, v) for u, v, _ in G.edges} + expected_edges = {("x", "y"), ("y", "z")} + self.assertEqual(actual_edges, expected_edges) + + def test_complete_graph_basic(self): + G = eg.complete_graph(4) + self.assertEqual(len(G.nodes), 4) + self.assertEqual(len(G.edges), 6) # n*(n-1)/2 for undirected + + def test_complete_graph_directed(self): + G = eg.complete_graph(3, create_using=eg.DiGraph()) + self.assertTrue(G.is_directed()) + self.assertEqual(len(G.nodes), 3) + self.assertEqual(len(G.edges), 6) # n*(n-1) for directed + + def test_complete_graph_custom_nodes(self): + G = eg.complete_graph(["a", "b", "c"]) + self.assertEqual(set(G.nodes), {"a", "b", "c"}) + actual_edges = {(u, v) for u, v, _ in G.edges} + expected_edges = {("a", "b"), ("a", "c"), ("b", "c")} + self.assertEqual(actual_edges, expected_edges) + + def test_complete_graph_one_node(self): + G = eg.complete_graph(1) + self.assertEqual(len(G.nodes), 1) + self.assertEqual(len(G.edges), 0) + + def test_complete_graph_zero_nodes(self): + G = eg.complete_graph(0) + self.assertEqual(len(G.nodes), 0) + self.assertEqual(len(G.edges), 0) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/s_centrality.py b/easygraph/functions/hypergraph/centrality/s_centrality.py index c72e9413..4e02955e 100644 --- a/easygraph/functions/hypergraph/centrality/s_centrality.py +++ b/easygraph/functions/hypergraph/centrality/s_centrality.py @@ -81,7 +81,7 @@ def s_eccentricity(H, s=1, edges=True, source=None): """ - g = H.get_linegraph(s=s, edges=edges) + g = H.get_linegraph(s=s) result = eg.eccentricity(g) if source: return result[source] diff --git a/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py b/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py new file mode 100644 index 00000000..90e39d78 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py @@ -0,0 +1,72 @@ +import unittest + +import easygraph as eg + + +class TestCycleRatioCentrality(unittest.TestCase): + def setUp(self): + self.G_triangle = eg.Graph() + self.G_triangle.add_edges([(1, 2), (2, 3), (3, 1)]) + + self.G_star = eg.Graph() + self.G_star.add_edges([(1, 2), (1, 3), (1, 4)]) + + self.G_complete = eg.complete_graph(4) + + self.G_disconnected = eg.Graph() + self.G_disconnected.add_edges([(1, 2), (3, 4)]) + + def test_triangle_graph(self): + result = eg.cycle_ratio_centrality(self.G_triangle.copy()) + self.assertTrue(all(isinstance(v, float) for v in result.values())) + self.assertEqual(len(result), 3) + + def test_star_graph(self): + result = eg.cycle_ratio_centrality(self.G_star.copy()) + self.assertEqual(result, {}) + + def test_complete_graph(self): + result = eg.cycle_ratio_centrality(self.G_complete.copy()) + self.assertEqual(len(result), 4) + self.assertTrue(all(v > 0 for v in result.values())) + + def test_disconnected_graph(self): + result = eg.cycle_ratio_centrality(self.G_disconnected.copy()) + self.assertEqual(result, {}) + + def test_my_all_shortest_paths_valid(self): + G = eg.Graph() + G.add_edges([(1, 2), (2, 3), (3, 4)]) + paths = list(eg.my_all_shortest_paths(G, 1, 4)) + self.assertIn([1, 2, 3, 4], paths) + + def test_my_all_shortest_paths_invalid(self): + G = eg.Graph() + G.add_edges([(1, 2), (3, 4)]) + with self.assertRaises(eg.EasyGraphNoPath): + list(eg.my_all_shortest_paths(G, 1, 4)) + + def test_getandJudgeSimpleCircle_true(self): + G = eg.Graph() + G.add_edges([(1, 2), (2, 3), (3, 1)]) + self.assertTrue(eg.getandJudgeSimpleCircle([1, 2, 3], G)) + + def test_getandJudgeSimpleCircle_false(self): + G = eg.Graph() + G.add_edges([(1, 2), (2, 3)]) + self.assertFalse(eg.getandJudgeSimpleCircle([1, 2, 3], G)) + + def test_statistics_and_calculate_indicators(self): + SmallestCyclesOfNodes = {1: set(), 2: set(), 3: set()} + CycLenDict = {3: 0} + SmallestCycles = {(1, 2, 3)} + result = eg.StatisticsAndCalculateIndicators( + SmallestCyclesOfNodes, CycLenDict, SmallestCycles + ) + self.assertTrue(isinstance(result, dict)) + self.assertIn(1, result) + self.assertGreater(result[1], 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/tests/test_degree.py b/easygraph/functions/hypergraph/centrality/tests/test_degree.py new file mode 100644 index 00000000..85aaaef3 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_degree.py @@ -0,0 +1,38 @@ +import unittest + +import easygraph as eg + + +class TestHypergraphDegreeCentrality(unittest.TestCase): + def test_basic_degree_centrality(self): + hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2), (2, 3), (0, 2)]) + result = eg.hyepergraph_degree_centrality(hg) + expected = {0: 2, 1: 2, 2: 3, 3: 1} + self.assertEqual(result, expected) + + def test_empty_hypergraph(self): + hg = eg.Hypergraph(num_v=1, e_list=[]) + result = eg.hyepergraph_degree_centrality(hg) + self.assertEqual(result, {0: 0}) + + def test_single_edge(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)]) + result = eg.hyepergraph_degree_centrality(hg) + expected = {0: 1, 1: 1, 2: 1} + self.assertEqual(result, expected) + + def test_singleton_nodes(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)]) + result = eg.hyepergraph_degree_centrality(hg) + expected = {0: 1, 1: 1, 2: 1} + self.assertEqual(result, expected) + + def test_node_with_no_edges(self): + hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2)]) + result = eg.hyepergraph_degree_centrality(hg) + expected = {0: 1, 1: 2, 2: 1, 3: 0} # node 3 has no edges + self.assertEqual(result, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py b/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py new file mode 100644 index 00000000..69b56120 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py @@ -0,0 +1,51 @@ +import unittest + +import easygraph as eg + + +class TestHypercoreness(unittest.TestCase): + def test_simple_hypergraph(self): + hg = eg.Hypergraph(num_v=5, e_list=[(0, 1), (1, 2, 3), (3, 4)]) + si = eg.size_independent_hypercoreness(hg) + fb = eg.frequency_based_hypercoreness(hg) + + self.assertIsInstance(si, dict) + self.assertIsInstance(fb, dict) + self.assertTrue(set(si.keys()).issubset(set(hg.v))) + self.assertTrue(set(fb.keys()).issubset(set(hg.v))) + + for val in si.values(): + self.assertIsInstance(val, float) + self.assertGreaterEqual(val, 0) + + for val in fb.values(): + self.assertIsInstance(val, float) + self.assertGreaterEqual(val, 0) + + def test_single_hyperedge(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)]) + si = eg.size_independent_hypercoreness(hg) + fb = eg.frequency_based_hypercoreness(hg) + + self.assertTrue(all(v >= 0 for v in si.values())) + self.assertTrue(all(v >= 0 for v in fb.values())) + + def test_large_uniform_hypergraph(self): + hg = eg.Hypergraph(num_v=10, e_list=[(i, i + 1, i + 2) for i in range(7)]) + si = eg.size_independent_hypercoreness(hg) + fb = eg.frequency_based_hypercoreness(hg) + + self.assertEqual(len(si), 10) + self.assertEqual(len(fb), 10) + + def test_empty_hypergraph_raises(self): + hg = eg.Hypergraph(num_v=1, e_list=[]) + with self.assertRaises(IndexError): + eg.size_independent_hypercoreness(hg) + + with self.assertRaises(IndexError): + eg.frequency_based_hypercoreness(hg) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py b/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py new file mode 100644 index 00000000..a8c3450c --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py @@ -0,0 +1,40 @@ +import unittest + +import easygraph as eg +import numpy as np + + +class TestHypergraphSCentrality(unittest.TestCase): + def setUp(self): + # Simple test hypergraph + self.hg = eg.Hypergraph(num_v=5, e_list=[(0, 1), (1, 2, 3), (3, 4)]) + self.empty_hg = eg.Hypergraph(num_v=1, e_list=[]) + self.singleton_hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)]) + + def test_s_betweenness_normal(self): + result = eg.s_betweenness(self.hg) + self.assertIsInstance(result, (list, dict)) + self.assertTrue(all(isinstance(x, (int, float)) for x in result)) + + def test_s_closeness_normal(self): + result = eg.s_closeness(self.hg) + self.assertIsInstance(result, (list, dict)) + self.assertTrue(all(isinstance(x, (int, float)) for x in result)) + + def test_s_eccentricity_all(self): + result = eg.s_eccentricity(self.hg) + self.assertIsInstance(result, dict) + for v in result.values(): + self.assertIsInstance(v, (int, float, np.integer, np.floating)) + + def test_s_eccentricity_edges_false(self): + result = eg.s_eccentricity(self.hg, edges=False) + self.assertIsInstance(result, dict) + + def test_s_eccentricity_invalid_source(self): + with self.assertRaises(KeyError): + eg.s_eccentricity(self.hg, source=(999, 888)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py b/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py new file mode 100644 index 00000000..d674ca09 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py @@ -0,0 +1,43 @@ +import unittest + +import easygraph as eg +import numpy as np + +from easygraph.exception import EasyGraphError + + +class TestVectorCentrality(unittest.TestCase): + def test_single_edge(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)]) + result = eg.vector_centrality(hg) + self.assertEqual(set(result.keys()), {0, 1, 2}) + for val in result.values(): + self.assertEqual(len(val), 2) # because D = 3 → k = 2 and 3 + + def test_multiple_edges_different_orders(self): + hg = eg.Hypergraph(num_v=4, e_list=[(0, 1), (1, 2, 3)]) + result = eg.vector_centrality(hg) + self.assertEqual(set(result.keys()), {0, 1, 2, 3}) + for val in result.values(): + self.assertEqual(len(val), 2) + self.assertTrue(all(isinstance(x, (float, np.floating)) for x in val)) + + def test_disconnected_hypergraph_raises(self): + hg = eg.Hypergraph(num_v=6, e_list=[(0, 1), (2, 3)]) + with self.assertRaises(EasyGraphError): + eg.vector_centrality(hg) + + def test_non_consecutive_node_ids(self): + hg = eg.Hypergraph(num_v=5, e_list=[(0, 2, 4)]) + result = eg.vector_centrality(hg) + self.assertEqual(len(result), 5) + for val in result.values(): + self.assertEqual(len(val), 2) + + def test_index_error_due_to_wrong_num_v(self): + with self.assertRaises(eg.EasyGraphError): + eg.Hypergraph(num_v=3, e_list=[(0, 1, 5)]) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/hypergraph/centrality/vector_centrality.py b/easygraph/functions/hypergraph/centrality/vector_centrality.py index bf26c47d..1cff7b9e 100644 --- a/easygraph/functions/hypergraph/centrality/vector_centrality.py +++ b/easygraph/functions/hypergraph/centrality/vector_centrality.py @@ -36,7 +36,7 @@ def vector_centrality(H): LG = H.get_linegraph() if not eg.is_connected(LG): raise EasyGraphError("This method is not defined for disconnected hypergraphs.") - LGcent = eg.eigenvector_centrality(LG) + LGcent = eigenvector_centrality(LG) vc = {node: [] for node in range(0, H.num_v)} @@ -64,3 +64,28 @@ def vector_centrality(H): vc[node].append(c_i[node]) return vc + + +def eigenvector_centrality(G, max_iter=100, tol=1.0e-6): + from collections import defaultdict + + nodes = list(G.nodes) + n = len(nodes) + x = {v: 1.0 for v in nodes} + + for _ in range(max_iter): + x_new = defaultdict(float) + for v in G: + for nbr in G.neighbors(v): + x_new[v] += x[nbr] + + # Normalize + norm = sum(v**2 for v in x_new.values()) ** 0.5 + if norm == 0: + return x_new + x_new = {k: v / norm for k, v in x_new.items()} + + # Check convergence + if all(abs(x_new[v] - x[v]) < tol for v in nodes): + return x_new + x = x_new diff --git a/easygraph/functions/hypergraph/null_model/tests/test_classic.py b/easygraph/functions/hypergraph/null_model/tests/test_classic.py index dbb81a98..3da8b2a5 100644 --- a/easygraph/functions/hypergraph/null_model/tests/test_classic.py +++ b/easygraph/functions/hypergraph/null_model/tests/test_classic.py @@ -1,6 +1,8 @@ import easygraph as eg import pytest +from easygraph.utils.exception import EasyGraphError + class TestClassic: def test_complete_hypergraph(self): @@ -46,3 +48,38 @@ def test_uniform_hypergraph(self): H3 = eg.uniform_HPPM(10, 6, 0.9, 10, 0.9) print("H3:", H3) + + +class TestHypergraphGenerators: + def test_empty_hypergraph_default(self): + hg = eg.empty_hypergraph() + assert hg.num_v == 1 + assert len(hg.e[0]) == 0 + + def test_empty_hypergraph_custom_size(self): + hg = eg.empty_hypergraph(5) + assert hg.num_v == 5 + assert len(hg.e[0]) == 0 + + def test_complete_hypergraph_zero_nodes_raises(self): + with pytest.raises(EasyGraphError): + eg.complete_hypergraph(0) + + def test_complete_hypergraph_n_1_excludes_singletons(self): + hg = eg.complete_hypergraph(1, include_singleton=False) + assert hg.num_v == 1 + assert len(hg.e[0]) == 0 + + def test_complete_hypergraph_n_3_excludes_singletons(self): + hg = eg.complete_hypergraph(3, include_singleton=False) + expected_edges = [[0, 1], [0, 2], [1, 2], [0, 1, 2]] + assert sorted(sorted(e) for e in hg.e[0]) == sorted( + sorted(e) for e in expected_edges + ) + + def test_complete_hypergraph_n_3_includes_singletons(self): + hg = eg.complete_hypergraph(3, include_singleton=True) + expected_edges = [[0], [1], [2], [0, 1], [0, 2], [1, 2], [0, 1, 2]] + assert sorted(sorted(e) for e in hg.e[0]) == sorted( + sorted(e) for e in expected_edges + ) diff --git a/easygraph/functions/hypergraph/null_model/tests/test_lattice.py b/easygraph/functions/hypergraph/null_model/tests/test_lattice.py new file mode 100644 index 00000000..66410370 --- /dev/null +++ b/easygraph/functions/hypergraph/null_model/tests/test_lattice.py @@ -0,0 +1,49 @@ +import easygraph as eg +import pytest + +from easygraph.utils.exception import EasyGraphError + + +class TestRingLatticeHypergraph: + def test_valid_ring_lattice(self): + H = eg.ring_lattice(n=10, d=3, k=4, l=1) + assert isinstance(H, eg.Hypergraph) + assert H.num_v == 10 + assert all(len(edge) == 3 for edge in H.e[0]) + + def test_k_less_than_zero_raises_error(self): + with pytest.raises(EasyGraphError, match="Invalid k value!"): + eg.ring_lattice(n=10, d=3, k=-2, l=1) + + def test_k_less_than_two_warns(self): + with pytest.warns(UserWarning, match="disconnected"): + H = eg.ring_lattice(n=10, d=3, k=1, l=1) + assert isinstance(H, eg.Hypergraph) + + def test_k_odd_warns(self): + with pytest.warns(UserWarning, match="divisible by 2"): + H = eg.ring_lattice(n=10, d=3, k=3, l=1) + assert isinstance(H, eg.Hypergraph) + + def test_ring_lattice_with_d_eq_1(self): + H = eg.ring_lattice(n=5, d=1, k=2, l=0) + assert all(len(edge) == 1 for edge in H.e[0]) + + def test_ring_lattice_with_overlap_zero(self): + H = eg.ring_lattice(n=6, d=2, k=2, l=0) + assert all(len(edge) == 2 for edge in H.e[0]) + + def test_large_n(self): + H = eg.ring_lattice(n=100, d=4, k=6, l=2) + assert H.num_v == 100 + assert all(len(e) == 4 for e in H.e[0]) + + def test_n_equals_1(self): + H = eg.ring_lattice(n=1, d=1, k=2, l=0) + assert H.num_v == 1 + assert isinstance(H, eg.Hypergraph) + + def test_k_zero(self): + H = eg.ring_lattice(n=5, d=2, k=0, l=1) + assert H.num_v == 5 + assert len(H.e[0]) == 0 diff --git a/easygraph/functions/hypergraph/null_model/tests/test_simple.py b/easygraph/functions/hypergraph/null_model/tests/test_simple.py new file mode 100644 index 00000000..eae3f2bc --- /dev/null +++ b/easygraph/functions/hypergraph/null_model/tests/test_simple.py @@ -0,0 +1,60 @@ +from itertools import combinations + +import easygraph as eg +import pytest + + +class TestStarCliqueHypergraph: + def test_valid_star_clique(self): + H = eg.star_clique(n_star=5, n_clique=4, d_max=2) + assert isinstance(H, eg.Hypergraph) + assert H.num_v == 9 # 5 star nodes + 4 clique nodes + assert any(0 in edge for edge in H.e[0]) # star center connected + + def test_minimum_valid_values(self): + H = eg.star_clique(n_star=2, n_clique=2, d_max=1) + assert H.num_v == 4 + assert len(H.e[0]) >= 2 + + def test_n_star_zero_raises(self): + with pytest.raises(ValueError, match="n_star must be an integer > 0."): + eg.star_clique(0, 3, 1) + + def test_n_clique_zero_raises(self): + with pytest.raises(ValueError, match="n_clique must be an integer > 0."): + eg.star_clique(3, 0, 1) + + def test_d_max_negative_raises(self): + with pytest.raises(ValueError, match="d_max must be an integer >= 0."): + eg.star_clique(3, 4, -1) + + def test_d_max_too_large_raises(self): + with pytest.raises(ValueError, match="d_max must be <= n_clique - 1."): + eg.star_clique(3, 4, 5) + + def test_no_clique_edges_if_d_max_zero(self): + H = eg.star_clique(3, 3, 0) + clique_nodes = set(range(3, 6)) + for edge in H.e[0]: + assert not clique_nodes.issubset(edge) + + def test_clique_hyperedges_match_combinations(self): + n_star, n_clique, d_max = 3, 4, 2 + H = eg.star_clique(n_star, n_clique, d_max) + clique_nodes = list(range(n_star, n_star + n_clique)) + expected = { + tuple(sorted(e)) + for d in range(1, d_max + 1) + for e in combinations(clique_nodes, d + 1) + } + actual = { + tuple(sorted(e)) for e in H.e[0] if all(node in clique_nodes for node in e) + } + assert expected.issubset(actual) + + def test_star_legs_connect_to_center(self): + H = eg.star_clique(5, 4, 1) + star_nodes = list(range(5)) + center = star_nodes[0] + for i in range(1, 4): # last star leg is used to connect to clique + assert any({center, i}.issubset(edge) for edge in H.e[0]) diff --git a/easygraph/functions/hypergraph/tests/test_assortativity.py b/easygraph/functions/hypergraph/tests/test_assortativity.py index 874ed70c..452aec9a 100644 --- a/easygraph/functions/hypergraph/tests/test_assortativity.py +++ b/easygraph/functions/hypergraph/tests/test_assortativity.py @@ -6,6 +6,8 @@ import easygraph as eg import numpy as np +from easygraph.utils.exception import EasyGraphError + class test_assortativity(unittest.TestCase): def setUp(self): @@ -15,7 +17,39 @@ def setUp(self): eg.Hypergraph(num_v=10, e_list=self.edges, e_property=None), eg.Hypergraph(num_v=2, e_list=[(0, 1)]), ] - # checked -- num_v cannot be set to negative number + # Valid uniform hypergraph + self.hg_uniform = eg.Hypergraph( + num_v=5, + e_list=[ + (0, 1, 2), + (1, 2, 3), + (2, 3, 4), + ], + ) + + # Non-uniform hypergraph + self.hg_non_uniform = eg.Hypergraph( + num_v=4, + e_list=[ + (0, 1), + (2, 3, 0), + ], + ) + + # Singleton edge hypergraph (still needs num_v > 0) + self.hg_singleton = eg.Hypergraph( + num_v=3, + e_list=[ + (0,), + (1, 2), + ], + ) + + # "Empty" hypergraph (has 1 node but no edges) + self.hg_empty = eg.Hypergraph( + num_v=1, + e_list=[], + ) def test_dynamical_assortativity(self): for i in self.hg: @@ -36,6 +70,34 @@ def test_degree_assortativity(self): for i in self.hg: print(eg.degree_assortativity(i)) + def test_dynamical_assortativity_valid(self): + result = eg.dynamical_assortativity(self.hg_uniform) + self.assertIsInstance(result, float) + + def test_dynamical_assortativity_raises_on_empty(self): + with self.assertRaises(EasyGraphError): + eg.dynamical_assortativity(self.hg_empty) + + def test_dynamical_assortativity_raises_on_singleton(self): + with self.assertRaises(EasyGraphError): + eg.dynamical_assortativity(self.hg_singleton) + + def test_dynamical_assortativity_raises_on_nonuniform(self): + with self.assertRaises(EasyGraphError): + eg.dynamical_assortativity(self.hg_non_uniform) + + def test_degree_assortativity_raises_on_invalid_kind(self): + with self.assertRaises(EasyGraphError): + eg.degree_assortativity(self.hg_uniform, kind="invalid") + + def test_degree_assortativity_raises_on_singleton(self): + with self.assertRaises(EasyGraphError): + eg.degree_assortativity(self.hg_singleton) + + def test_degree_assortativity_raises_on_empty(self): + with self.assertRaises(EasyGraphError): + eg.degree_assortativity(self.hg_empty) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py b/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py index 14f2f0b2..a0318e60 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphError + class test_hypergraph_operation(unittest.TestCase): def setUp(self): @@ -25,5 +27,63 @@ def test_hypergraph_two_node_clustering_coefficient(self): print(eg.hypergraph_two_node_clustering_coefficient(i)) +class TestHypergraphClustering(unittest.TestCase): + def setUp(self): + self.edges = [(0, 1), (1, 2), (2, 3), (3, 0)] + self.hg = eg.Hypergraph(num_v=4, e_list=self.edges) + + def test_hypergraph_clustering_coefficient_basic(self): + cc = eg.hypergraph_clustering_coefficient(self.hg) + self.assertIsInstance(cc, dict) + for k, v in cc.items(): + self.assertIn(k, self.hg.v) + self.assertGreaterEqual(v, 0) + + def test_hypergraph_local_clustering_coefficient_basic(self): + cc = eg.hypergraph_local_clustering_coefficient(self.hg) + self.assertIsInstance(cc, dict) + for k, v in cc.items(): + self.assertIn(k, self.hg.v) + self.assertGreaterEqual(v, 0) + + def test_hypergraph_two_node_clustering_union(self): + cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="union") + self.assertIsInstance(cc, dict) + + def test_hypergraph_two_node_clustering_min(self): + cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="min") + self.assertIsInstance(cc, dict) + + def test_hypergraph_two_node_clustering_max(self): + cc = eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="max") + self.assertIsInstance(cc, dict) + + def test_hypergraph_two_node_clustering_invalid_kind(self): + with self.assertRaises(EasyGraphError): + eg.hypergraph_two_node_clustering_coefficient(self.hg, kind="invalid") + + def test_single_edge(self): + hg = eg.Hypergraph(num_v=2, e_list=[(0, 1)]) + cc = eg.hypergraph_clustering_coefficient(hg) + self.assertTrue(all(k in cc for k in hg.v)) + + def test_disconnected_nodes(self): + hg = eg.Hypergraph(num_v=4, e_list=[(0, 1)]) + cc = eg.hypergraph_clustering_coefficient(hg) + for v in [2, 3]: + self.assertEqual(cc[v], 0) + + def test_fully_connected_hyperedge(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)]) + cc = eg.hypergraph_clustering_coefficient(hg) + for v in cc.values(): + self.assertEqual(v, 1.0) + + def test_nan_safety_in_two_node_coefficient(self): + hg = eg.Hypergraph(num_v=1, e_list=[(0,)]) + result = eg.hypergraph_two_node_clustering_coefficient(hg) + self.assertEqual(result[0], 0.0) + + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py b/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py index 046bf89d..2188ddb2 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py @@ -3,6 +3,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphError + class test_hypergraph_operation(unittest.TestCase): def setUp(self): @@ -19,6 +21,52 @@ def test_hypergraph_operation(self): print(eg.hypergraph_density(i)) i.draw(v_color="#e6928f", e_color="#4e9595") + def test_basic_density(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0, 1), (1, 2)]) + expected = 2 / (2**3 - 1) + self.assertAlmostEqual(eg.hypergraph_density(hg), expected) + + def test_density_ignore_singletons(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1, 2)]) + expected = 2 / ((2**3 - 1) - 3) + self.assertAlmostEqual( + eg.hypergraph_density(hg, ignore_singletons=True), expected + ) + + def test_density_all_singletons(self): + hg = eg.Hypergraph(num_v=3, e_list=[(0,), (1,), (2,)]) + expected = 3 / (2**3 - 1) + self.assertAlmostEqual(eg.hypergraph_density(hg), expected) + expected_ignoring = 3 / ((2**3 - 1) - 3) + self.assertAlmostEqual( + eg.hypergraph_density(hg, ignore_singletons=True), expected_ignoring + ) + + def test_no_edges_returns_zero(self): + hg = eg.Hypergraph(num_v=5, e_list=[]) + self.assertEqual(eg.hypergraph_density(hg), 0.0) + + def test_single_node_single_edge(self): + hg = eg.Hypergraph(num_v=1, e_list=[(0,)]) + self.assertEqual(eg.hypergraph_density(hg), 1.0) + + def test_density_max_possible_edges(self): + n = 4 + from itertools import chain + from itertools import combinations + + powerset = list( + chain.from_iterable(combinations(range(n), r) for r in range(1, n + 1)) + ) + hg = eg.Hypergraph(num_v=n, e_list=powerset) + self.assertAlmostEqual(eg.hypergraph_density(hg), 1.0) + + def test_density_zero_division_guard(self): + # Singleton ignored in n=1 graph should not divide by zero + hg = eg.Hypergraph(num_v=1, e_list=[(0,)]) + result = eg.hypergraph_density(hg, ignore_singletons=True) + self.assertEqual(result, 0.0) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/path/average_shortest_path_length.py b/easygraph/functions/path/average_shortest_path_length.py index 271f26ea..4098884a 100644 --- a/easygraph/functions/path/average_shortest_path_length.py +++ b/easygraph/functions/path/average_shortest_path_length.py @@ -72,7 +72,7 @@ def average_shortest_path_length(G, weight=None, method=None): 1.0 """ - single_source_methods = ["single_source_bfs", "Dijkstra"] + single_source_methods = ["single_source_bfs", "dijkstra"] all_pairs_methods = ["Floyed"] supported_methods = single_source_methods + all_pairs_methods diff --git a/easygraph/functions/path/bridges.py b/easygraph/functions/path/bridges.py index b5ef42fa..e38a1690 100644 --- a/easygraph/functions/path/bridges.py +++ b/easygraph/functions/path/bridges.py @@ -57,6 +57,8 @@ def bridges(G, root=None): ---------- .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions """ + if root is not None and root not in G.nodes: + raise eg.NodeNotFound(f"Node {root} is not in the graph.") chains = chain_decomposition(G, root=root) chain_edges = set(chain.from_iterable(chains)) for u, v, t in G.edges: @@ -106,7 +108,7 @@ def has_bridges(G, root=None): """ try: - next(bridges(G)) + next(bridges(G, root)) except StopIteration: return False else: diff --git a/easygraph/functions/path/tests/test_average_shortest_path_length.py b/easygraph/functions/path/tests/test_average_shortest_path_length.py index 3f774447..00867f78 100644 --- a/easygraph/functions/path/tests/test_average_shortest_path_length.py +++ b/easygraph/functions/path/tests/test_average_shortest_path_length.py @@ -2,10 +2,54 @@ import easygraph as eg +from easygraph import average_shortest_path_length +from easygraph.utils.exception import EasyGraphError +from easygraph.utils.exception import EasyGraphPointlessConcept -class test_average_shortested_path_length(unittest.TestCase): - def setUp(self): - pass + +class TestAverageShortestPathLength(unittest.TestCase): + def test_unweighted_path_graph(self): + G = eg.path_graph(5) + result = average_shortest_path_length(G) + self.assertEqual(result, 2.0) + + def test_weighted_graph(self): + G = eg.Graph() + G.add_edge(0, 1, weight=1) + G.add_edge(1, 2, weight=2) + G.add_edge(2, 3, weight=3) + result = average_shortest_path_length(G, weight="weight", method="dijkstra") + self.assertAlmostEqual(result, 3.333, places=3) + + def test_trivial_graph(self): + G = eg.Graph() + G.add_node(1) + self.assertEqual(average_shortest_path_length(G), 0) + + def test_disconnected_graph_undirected(self): + G = eg.Graph([(1, 2), (3, 4)]) + with self.assertRaises(EasyGraphError): + average_shortest_path_length(G) + + def test_disconnected_graph_directed(self): + G = eg.DiGraph([(0, 1), (2, 3)]) + with self.assertRaises(EasyGraphError): + average_shortest_path_length(G) + + def test_null_graph(self): + G = eg.Graph() + with self.assertRaises(EasyGraphPointlessConcept): + average_shortest_path_length(G) + + def test_directed_strongly_connected(self): + G = eg.DiGraph([(0, 1), (1, 2), (2, 0)]) + result = average_shortest_path_length(G) + self.assertEqual(result, 1.5) + + def test_unsupported_method(self): + G = eg.path_graph(5) + with self.assertRaises(ValueError): + average_shortest_path_length(G, method="unsupported_method") if __name__ == "__main__": diff --git a/easygraph/functions/path/tests/test_bridges.py b/easygraph/functions/path/tests/test_bridges.py index 7ee01e83..29a2edb1 100644 --- a/easygraph/functions/path/tests/test_bridges.py +++ b/easygraph/functions/path/tests/test_bridges.py @@ -2,6 +2,8 @@ import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented + class test_bridges(unittest.TestCase): def setUp(self): @@ -90,6 +92,67 @@ def test_bridges(self): def test_has_bridges(self): print(eg.has_bridges(self.g2)) + def test_empty_graph(self): + g = eg.Graph() + self.assertFalse(eg.has_bridges(g)) + self.assertEqual(list(eg.bridges(g)), []) + + def test_single_node_graph(self): + g = eg.Graph() + g.add_node(1) + self.assertFalse(eg.has_bridges(g)) + self.assertEqual(list(eg.bridges(g)), []) + + def test_disconnected_graph(self): + g = eg.Graph() + g.add_edges_from([(0, 1), (2, 3)]) + self.assertTrue(eg.has_bridges(g)) + self.assertCountEqual(list(eg.bridges(g)), [(0, 1), (2, 3)]) + + def test_cycle_graph(self): + g = eg.DiGraph([(1, 2), (2, 3), (3, 1)]) + self.assertFalse(eg.has_bridges(g)) + self.assertEqual(list(eg.bridges(g)), []) + + def test_path_graph(self): + g = eg.path_graph(4) + self.assertTrue(eg.has_bridges(g)) + self.assertCountEqual(list(eg.bridges(g)), [(0, 1), (1, 2), (2, 3)]) + + def test_star_graph(self): + g = eg.Graph() + g.add_edges_from([(0, i) for i in range(1, 5)]) + + expected = [(0, 1), (0, 2), (0, 3), (0, 4)] + self.assertTrue(eg.has_bridges(g)) + self.assertCountEqual(list(eg.bridges(g)), expected) + + def test_complete_graph(self): + g = eg.complete_graph(5) + self.assertFalse(eg.has_bridges(g)) + self.assertEqual(list(eg.bridges(g)), []) + + def test_graph_with_invalid_root(self): + g = eg.path_graph(3) + with self.assertRaises(eg.NodeNotFound): + list(eg.bridges(g, root=10)) + + def test_multigraph_exception(self): + g = eg.MultiGraph() + g.add_edges_from([(0, 1), (1, 2)]) + with self.assertRaises(EasyGraphNotImplemented): + list(eg.bridges(g)) + with self.assertRaises(EasyGraphNotImplemented): + eg.has_bridges(g) + + def test_weighted_graph_should_ignore_weights(self): + g = eg.Graph() + g.add_edges_from( + [(0, 1), (1, 2), (2, 3), (3, 0)], + edges_attr=[{"weight": 10}, {"weight": 20}, {"weight": 30}, {"weight": 40}], + ) + self.assertFalse(eg.has_bridges(g)) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/path/tests/test_diameter.py b/easygraph/functions/path/tests/test_diameter.py index 8c54c3d2..bf6d5bd9 100644 --- a/easygraph/functions/path/tests/test_diameter.py +++ b/easygraph/functions/path/tests/test_diameter.py @@ -88,6 +88,56 @@ def test_eccentricity(self): print(eg.eccentricity(self.g3)) print(eg.eccentricity(self.g4)) + def test_single_node_graph(self): + G = eg.Graph() + G.add_node(1) + self.assertEqual(eg.eccentricity(G), {1: 0}) + self.assertEqual(eg.diameter(G), 0) + + def test_two_node_graph(self): + G = eg.Graph([(1, 2)]) + self.assertEqual(eg.eccentricity(G), {1: 1, 2: 1}) + self.assertEqual(eg.diameter(G), 1) + + def test_disconnected_graph(self): + G = eg.Graph() + G.add_nodes_from([1, 2, 3]) + G.add_edge(1, 2) + with self.assertRaises(eg.EasyGraphError): + eg.eccentricity(G) + + def test_directed_not_strongly_connected(self): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (2, 3)]) # Not strongly connected + with self.assertRaises(eg.EasyGraphError): + eg.eccentricity(G) + + def test_eccentricity_with_sp(self): + G = eg.Graph([(1, 2), (2, 3)]) + sp = { + 1: {1: 0, 2: 1, 3: 2}, + 2: {2: 0, 1: 1, 3: 1}, + 3: {3: 0, 2: 1, 1: 2}, + } + self.assertEqual(eg.eccentricity(G, sp=sp), {1: 2, 2: 1, 3: 2}) + self.assertEqual(eg.diameter(G, e=eg.eccentricity(G, sp=sp)), 2) + + def test_eccentricity_single_node_query(self): + G = eg.Graph([(1, 2), (2, 3)]) + self.assertEqual(eg.eccentricity(G, v=1), 2) + self.assertEqual(eg.eccentricity(G, v=2), 1) + + def test_eccentricity_subset_of_nodes(self): + G = eg.Graph([(1, 2), (2, 3)]) + result = eg.eccentricity(G, v=[1, 3]) + self.assertEqual(result[1], 2) + self.assertEqual(result[3], 2) + + def test_diameter_matches_max_eccentricity(self): + G = eg.Graph([(1, 2), (2, 3)]) + ecc = eg.eccentricity(G) + self.assertEqual(eg.diameter(G, e=ecc), max(ecc.values())) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/path/tests/test_mst.py b/easygraph/functions/path/tests/test_mst.py index 92556208..39271f54 100644 --- a/easygraph/functions/path/tests/test_mst.py +++ b/easygraph/functions/path/tests/test_mst.py @@ -76,6 +76,36 @@ def setUp(self): {"weight": -6}, ], ) + self.nan_graph = eg.Graph() + self.nan_graph.add_edges( + [(0, 1), (1, 2)], edges_attr=[{"weight": float("nan")}, {"weight": 1}] + ) + + self.no_weight_graph = eg.Graph() + self.no_weight_graph.add_edges([(0, 1), (1, 2)]) + + self.equal_weight_graph = eg.Graph() + self.equal_weight_graph.add_edges( + [(0, 1), (1, 2), (2, 0)], + edges_attr=[{"weight": 1}, {"weight": 1}, {"weight": 1}], + ) + + self.negative_weight_graph = eg.Graph() + self.negative_weight_graph.add_edges( + [(0, 1), (1, 2), (2, 3)], + edges_attr=[{"weight": -1}, {"weight": -2}, {"weight": -3}], + ) + + self.disconnected_graph = eg.Graph() + self.disconnected_graph.add_edges( + [(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 2}] + ) + + self.G = eg.Graph() + self.G.add_edges( + [(0, 1), (1, 2), (2, 3), (3, 0)], + edges_attr=[{"weight": 1}, {"weight": 2}, {"weight": 3}, {"weight": 4}], + ) def helper(self, g: eg.Graph, func): result = func(g) @@ -110,6 +140,37 @@ def test_maximum_spanning_tree(self): self.helper(self.g2, eg.maximum_spanning_tree) self.helper(self.g4, eg.maximum_spanning_tree) + def test_nan_handling(self): + with self.assertRaises(ValueError): + list(eg.minimum_spanning_edges(self.nan_graph)) + edges = list(eg.minimum_spanning_edges(self.nan_graph, ignore_nan=True)) + self.assertEqual(len(edges), 1) + + def test_missing_weight_defaults_to_one(self): + edges = list(eg.minimum_spanning_edges(self.no_weight_graph)) + self.assertEqual(len(edges), 2) + + def test_negative_weights(self): + edges = list(eg.minimum_spanning_edges(self.negative_weight_graph)) + weights = [attr["weight"] for _, _, attr in edges] + self.assertIn(-3, weights) + self.assertEqual(len(edges), 3) + + def test_disconnected_graph(self): + edges = list(eg.minimum_spanning_edges(self.disconnected_graph)) + self.assertEqual(len(edges), 2) + + def test_maximum_vs_minimum_edges(self): + min_edges = list(eg.minimum_spanning_edges(self.G)) + max_edges = list(eg.maximum_spanning_edges(self.G)) + min_set = {(min(u, v), max(u, v)) for u, v, _ in min_edges} + max_set = {(min(u, v), max(u, v)) for u, v, _ in max_edges} + self.assertNotEqual(min_set, max_set) + + def test_invalid_algorithm_name(self): + with self.assertRaises(ValueError): + list(eg.minimum_spanning_edges(self.G, algorithm="invalid_algo")) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/path/tests/test_path.py b/easygraph/functions/path/tests/test_path.py index 8c3afcc8..97ffac38 100644 --- a/easygraph/functions/path/tests/test_path.py +++ b/easygraph/functions/path/tests/test_path.py @@ -132,6 +132,69 @@ def test_multi_source_dijkstra(self): print(e) print() + def test_dijkstra_negative_weights_raises(self): + with self.assertRaises(ValueError): + eg.Dijkstra(self.g4, node=0) + + def test_dijkstra_disconnected_graph(self): + g = eg.Graph() + g.add_edges([(1, 2)], edges_attr=[{"weight": 3}]) + g.add_node(3) # disconnected + result = eg.Dijkstra(g, node=1) + self.assertIn(3, g.nodes) + self.assertNotIn(3, result) + + def test_floyd_disconnected_graph(self): + g = eg.Graph() + g.add_edges([(1, 2)], edges_attr=[{"weight": 3}]) + g.add_node(3) + result = eg.Floyd(g) + self.assertEqual(result[1][3], float("inf")) + self.assertEqual(result[3][3], 0) + + def test_prim_disconnected_graph(self): + g = eg.Graph() + g.add_edges([(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 1}]) + result = eg.Prim(g) + count = sum(len(v) for v in result.values()) + self.assertLess( + count, len(g.nodes) - 1 + ) # not enough edges to connect all nodes + + def test_kruskal_disconnected_graph(self): + g = eg.Graph() + g.add_edges([(0, 1), (2, 3)], edges_attr=[{"weight": 1}, {"weight": 1}]) + result = eg.Kruskal(g) + count = sum(len(v) for v in result.values()) + self.assertLess(count, len(g.nodes) - 1) + + def test_spfa_always_errors(self): + with self.assertRaises(eg.EasyGraphError): + eg.Spfa(self.g2, 0) + + def test_single_source_bfs_no_target(self): + result = eg.single_source_bfs(self.g2, 1) + self.assertIn(0, result.values()) # BFS level exists + self.assertIsInstance(result, dict) + + def test_single_source_bfs_target_not_found(self): + g = eg.Graph() + g.add_edges([(1, 2)], edges_attr=[{"weight": 1}]) + g.add_node(99) + result = eg.single_source_bfs(g, 1, target=99) + self.assertNotIn(99, result) + + def test_multi_source_dijkstra_empty_sources(self): + result = eg.multi_source_dijkstra(self.g2, sources=[]) + self.assertEqual(result, {}) + + def test_multi_source_dijkstra_matches_single(self): + sources = [1, 2] + multi = eg.multi_source_dijkstra(self.g2, sources) + for s in sources: + single = eg.single_source_dijkstra(self.g2, s) + self.assertEqual(multi[s], single) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/structural_holes/HAM.py b/easygraph/functions/structural_holes/HAM.py index 6a5fe808..8e30cdb0 100644 --- a/easygraph/functions/structural_holes/HAM.py +++ b/easygraph/functions/structural_holes/HAM.py @@ -193,6 +193,14 @@ def get_structural_holes_HAM(G, k, c, ground_truth_labels): .. [1] https://dl.acm.org/doi/10.1145/2939672.2939807 """ + if k <= 0 or k > G.number_of_nodes(): + raise ValueError("`k` must be between 1 and number of nodes in the graph.") + if c <= 0: + raise ValueError("Number of communities `c` must be greater than 0") + if len(ground_truth_labels) != G.number_of_nodes(): + raise ValueError( + "Length of `ground_truth_labels` must match number of nodes in the graph." + ) import scipy.linalg as spl import scipy.sparse as sps diff --git a/easygraph/functions/structural_holes/HIS.py b/easygraph/functions/structural_holes/HIS.py index 44eda18d..43c12c83 100644 --- a/easygraph/functions/structural_holes/HIS.py +++ b/easygraph/functions/structural_holes/HIS.py @@ -68,8 +68,15 @@ def get_structural_holes_HIS(G, C: List[frozenset], epsilon=1e-4, weight="weight S.extend(list(combinations(range(len(C)), community_subset_size))) # I: dict[node][cmnt_index] # H: dict[node][subset_index] + + if not G.nodes or not C: + return [], {}, {} + I, H = initialize(G, C, S, weight=weight) + if not S: + return S, I, H + alphas = [0.3 for i in range(len(C))] # list[cmnt_index] betas = [(0.5 - math.pow(0.5, len(subset))) for subset in S] # list[subset_index] diff --git a/easygraph/functions/structural_holes/ICC.py b/easygraph/functions/structural_holes/ICC.py index 6a3c24ed..b23e4dd3 100644 --- a/easygraph/functions/structural_holes/ICC.py +++ b/easygraph/functions/structural_holes/ICC.py @@ -7,11 +7,15 @@ def inverse_closeness_centrality(G, v): + if len(G) <= 1: + return 0 c_v = sum(eg.Dijkstra(G, v).values()) / (len(G) - 1) return c_v def bounded_inverse_closeness_centrality(G, v, l): + if len(G) <= 1: + return 0 queue = [] queue.append(v) seen = set() diff --git a/easygraph/functions/structural_holes/NOBE.py b/easygraph/functions/structural_holes/NOBE.py index 82081594..62f5f3f8 100644 --- a/easygraph/functions/structural_holes/NOBE.py +++ b/easygraph/functions/structural_holes/NOBE.py @@ -36,6 +36,10 @@ def NOBE_SH(G, K, topk): .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation """ + if K <= 0: + raise ValueError("Embedding dimension K must be a positive integer.") + if topk <= 0: + raise ValueError("Parameter topk must be a positive integer.") from sklearn.cluster import KMeans Y = eg.graph_embedding.NOBE(G, K) @@ -101,6 +105,10 @@ def NOBE_GA_SH(G, K, topk): .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation """ + if K <= 0: + raise ValueError("Embedding dimension K must be a positive integer.") + if topk <= 0: + raise ValueError("Parameter topk must be a positive integer.") from sklearn.cluster import KMeans Y = eg.NOBE_GA(G, K) diff --git a/easygraph/functions/structural_holes/__init__.py b/easygraph/functions/structural_holes/__init__.py index 752bf7ed..ec4af9e7 100644 --- a/easygraph/functions/structural_holes/__init__.py +++ b/easygraph/functions/structural_holes/__init__.py @@ -3,6 +3,7 @@ from .HAM import * from .HIS import * from .ICC import * +from .maxBlock import * from .MaxD import * from .metrics import * from .NOBE import * diff --git a/easygraph/functions/structural_holes/tests/test_AP_Greedy.py b/easygraph/functions/structural_holes/tests/test_AP_Greedy.py new file mode 100644 index 00000000..38524f31 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_AP_Greedy.py @@ -0,0 +1,85 @@ +import unittest + +import easygraph as eg + + +class TestStructuralHoleSpanners(unittest.TestCase): + def setUp(self): + self.G = eg.get_graph_karateclub() + self.small_graph = eg.Graph() + self.small_graph.add_edges_from([(0, 1), (1, 2), (2, 3)]) + self.disconnected_graph = eg.Graph() + self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) + + def test_common_greedy_basic(self): + result = eg.common_greedy(self.G, k=3) + self.assertIsInstance(result, list) + self.assertEqual(len(result), 3) + for node in result: + self.assertIn(node, self.G.nodes) + + def test_ap_greedy_basic(self): + result = eg.AP_Greedy(self.G, k=3) + self.assertIsInstance(result, list) + self.assertEqual(len(result), 3) + for node in result: + self.assertIn(node, self.G.nodes) + + def test_common_greedy_k_zero(self): + result = eg.common_greedy(self.G, k=0) + self.assertEqual(result, []) + + def test_ap_greedy_k_zero(self): + result = eg.AP_Greedy(self.G, k=0) + self.assertEqual(result, []) + + def test_common_greedy_on_disconnected_graph(self): + result = eg.common_greedy(self.disconnected_graph, k=2) + self.assertEqual(len(result), 2) + for node in result: + self.assertIn(node, self.disconnected_graph.nodes) + + def test_ap_greedy_on_disconnected_graph(self): + result = eg.AP_Greedy(self.disconnected_graph, k=2) + self.assertEqual(len(result), 2) + for node in result: + self.assertIn(node, self.disconnected_graph.nodes) + + def test_common_greedy_with_custom_c(self): + result_default = eg.common_greedy(self.G, k=2) + result_custom = eg.common_greedy(self.G, k=2, c=2.5) + self.assertEqual(len(result_default), 2) + self.assertEqual(len(result_custom), 2) + + def test_ap_greedy_with_custom_c(self): + result_default = eg.AP_Greedy(self.G, k=2) + result_custom = eg.AP_Greedy(self.G, k=2, c=2.5) + self.assertEqual(len(result_default), 2) + self.assertEqual(len(result_custom), 2) + + def test_common_greedy_unweighted_vs_weighted(self): + # With and without weights + G_weighted = self.small_graph.copy() + for edge in G_weighted.edges: + u, v = edge[:2] + G_weighted[u][v]["weight"] = 1.0 + + result_unweighted = eg.common_greedy(G_weighted, k=2, weight=None) + result_weighted = eg.common_greedy(G_weighted, k=2, weight="weight") + self.assertEqual(len(result_unweighted), 2) + self.assertEqual(len(result_weighted), 2) + + def test_ap_greedy_unweighted_vs_weighted(self): + G_weighted = self.small_graph.copy() + for edge in G_weighted.edges: + u, v = edge[:2] + G_weighted[u][v]["weight"] = 1.0 + + result_unweighted = eg.AP_Greedy(G_weighted, k=2, weight=None) + result_weighted = eg.AP_Greedy(G_weighted, k=2, weight="weight") + self.assertEqual(len(result_unweighted), 2) + self.assertEqual(len(result_weighted), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_HAM.py b/easygraph/functions/structural_holes/tests/test_HAM.py new file mode 100644 index 00000000..3757a78e --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_HAM.py @@ -0,0 +1,71 @@ +import unittest + +import easygraph as eg +import numpy as np + + +class TestHAMStructuralHoles(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + self.G.add_edges_from( + [ + (0, 1), + (0, 2), + (1, 2), # Community 0 + (3, 4), + (3, 5), + (4, 5), # Community 1 + (2, 3), # Bridge between 0 and 1 + (6, 7), + (6, 8), + (7, 8), # Community 2 + ] + ) + self.labels = [[0], [0], [0], [1], [1], [1], [2], [2], [2]] + + def test_output_structure(self): + top_k, sh_score, cmnt_labels = eg.get_structural_holes_HAM( + self.G, k=2, c=3, ground_truth_labels=self.labels + ) + self.assertIsInstance(top_k, list) + self.assertTrue(all(isinstance(n, int) for n in top_k)) + self.assertEqual(len(top_k), 2) + + self.assertIsInstance(sh_score, dict) + self.assertEqual(len(sh_score), self.G.number_of_nodes()) + self.assertTrue( + all(isinstance(k, int) and isinstance(v, int) for k, v in sh_score.items()) + ) + + self.assertIsInstance(cmnt_labels, dict) + self.assertEqual(len(cmnt_labels), self.G.number_of_nodes()) + + def test_single_community(self): + labels = [[0]] * self.G.number_of_nodes() + top_k, _, _ = eg.get_structural_holes_HAM( + self.G, k=1, c=1, ground_truth_labels=labels + ) + self.assertEqual(len(top_k), 1) + + def test_invalid_k(self): + with self.assertRaises(ValueError): + eg.get_structural_holes_HAM( + self.G, k=-1, c=2, ground_truth_labels=self.labels + ) + + def test_invalid_c(self): + with self.assertRaises(ValueError): + eg.get_structural_holes_HAM( + self.G, k=2, c=0, ground_truth_labels=self.labels + ) + + def test_mismatched_labels(self): + bad_labels = [[0]] * (self.G.number_of_nodes() - 1) + with self.assertRaises(ValueError): + eg.get_structural_holes_HAM( + self.G, k=2, c=2, ground_truth_labels=bad_labels + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_HIS.py b/easygraph/functions/structural_holes/tests/test_HIS.py new file mode 100644 index 00000000..9464cff0 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_HIS.py @@ -0,0 +1,87 @@ +import unittest + +import easygraph as eg + +from easygraph.functions.structural_holes import get_structural_holes_HIS + + +class TestHISStructuralHoles(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + self.G.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 0), # Community 0 + (3, 4), + (4, 5), + (5, 3), # Community 1 + (2, 3), # Bridge between communities + ] + ) + self.communities = [frozenset([0, 1, 2]), frozenset([3, 4, 5])] + + def test_normal_output_structure(self): + S, I, H = get_structural_holes_HIS(self.G, self.communities) + self.assertIsInstance(S, list) + self.assertTrue(all(isinstance(s, tuple) for s in S)) + self.assertIsInstance(I, dict) + self.assertIsInstance(H, dict) + self.assertEqual(set(I.keys()), set(self.G.nodes)) + self.assertEqual(set(H.keys()), set(self.G.nodes)) + self.assertTrue(all(isinstance(v, dict) for v in I.values())) + self.assertTrue(all(isinstance(v, dict) for v in H.values())) + + def test_empty_graph(self): + G = eg.Graph() + communities = [] + S, I, H = get_structural_holes_HIS(G, communities) + self.assertEqual(S, []) + self.assertEqual(I, {}) + self.assertEqual(H, {}) + + def test_single_node_community(self): + G = eg.Graph() + G.add_node(42) + communities = [frozenset([42])] + S, I, H = get_structural_holes_HIS(G, communities) + self.assertEqual(list(I.keys()), [42]) + self.assertEqual(list(H.keys()), [42]) + self.assertEqual(list(I[42].values())[0], 0) + + def test_disconnected_communities(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (1, 2), (3, 4), (4, 5)]) + communities = [frozenset([0, 1, 2]), frozenset([3, 4, 5])] + S, I, H = get_structural_holes_HIS(G, communities) + self.assertEqual(set(I.keys()), set(G.nodes)) + + def test_node_in_multiple_communities(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) + communities = [frozenset([0, 1]), frozenset([1, 2]), frozenset([2, 3])] + S, I, H = get_structural_holes_HIS(G, communities) + self.assertIn(1, I) + self.assertGreaterEqual(len(I[1]), 2) + + def test_weighted_graph(self): + G = eg.Graph() + G.add_edge(0, 1, weight=2.0) + G.add_edge(1, 2, weight=3.0) + G.add_edge(2, 0, weight=1.0) + G.add_edge(2, 3, weight=4.0) + G.add_edge(3, 4, weight=5.0) + G.add_edge(4, 5, weight=6.0) + G.add_edge(5, 3, weight=1.0) + communities = [frozenset([0, 1, 2]), frozenset([3, 4, 5])] + S, I, H = get_structural_holes_HIS(G, communities, weight="weight") + self.assertIsInstance(list(I[0].values())[0], float) + + def test_convergence_with_high_epsilon(self): + S, I, H = get_structural_holes_HIS(self.G, self.communities, epsilon=1.0) + self.assertTrue(S) + self.assertEqual(set(I.keys()), set(self.G.nodes)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_ICC.py b/easygraph/functions/structural_holes/tests/test_ICC.py new file mode 100644 index 00000000..d8efd0e5 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_ICC.py @@ -0,0 +1,72 @@ +import unittest + +import easygraph as eg + +from easygraph.functions.structural_holes.ICC import AP_BICC +from easygraph.functions.structural_holes.ICC import BICC +from easygraph.functions.structural_holes.ICC import ICC + + +class TestICCBICCFunctions(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + self.G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (1, 3), (2, 4)]) + + def test_icc_basic(self): + result = ICC(self.G, k=2) + self.assertEqual(len(result), 2) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_icc_k_exceeds_nodes(self): + result = ICC(self.G, k=10) + self.assertLessEqual(len(result), len(self.G.nodes)) + + def test_bicc_basic(self): + result = BICC(self.G, k=2, K=4, l=2) + self.assertEqual(len(result), 2) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_ap_bicc_basic(self): + result = AP_BICC(self.G, k=2, K=4, l=2) + self.assertEqual(len(result), 2) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_icc_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + result = ICC(G, k=2) + self.assertTrue(all(node in G.nodes for node in result)) + + def test_bicc_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + result = BICC(G, k=1, K=2, l=1) + self.assertTrue(all(node in G.nodes for node in result)) + + def test_ap_bicc_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + result = AP_BICC(G, k=1, K=2, l=1) + self.assertTrue(all(node in G.nodes for node in result)) + + def test_icc_single_node(self): + G = eg.Graph() + G.add_node(1) + result = ICC(G, k=1) + self.assertEqual(result, [1]) + + def test_bicc_single_node(self): + G = eg.Graph() + G.add_node(1) + result = BICC(G, k=1, K=1, l=1) + self.assertEqual(result, [1]) + + def test_ap_bicc_single_node(self): + G = eg.Graph() + G.add_node(1) + result = AP_BICC(G, k=1, K=1, l=1) + self.assertEqual(result, [1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_MaxD.py b/easygraph/functions/structural_holes/tests/test_MaxD.py new file mode 100644 index 00000000..fe7adf30 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_MaxD.py @@ -0,0 +1,64 @@ +import unittest + +import easygraph as eg + + +class TestStructuralHolesMaxD(unittest.TestCase): + def setUp(self): + # Small undirected graph with a bridge between communities + self.G = eg.Graph() + self.G.add_edges_from( + [ + (1, 2), + (2, 3), # Community A + (4, 5), + (5, 6), # Community B + (3, 4), # Bridge edge + ] + ) + self.communities = [frozenset([1, 2, 3]), frozenset([4, 5, 6])] + + def test_basic_top1(self): + result = eg.get_structural_holes_MaxD(self.G, k=1, C=self.communities) + self.assertEqual(len(result), 1) + self.assertIn(result[0], self.G.nodes) + + def test_top_k_greater_than_1(self): + result = eg.get_structural_holes_MaxD(self.G, k=3, C=self.communities) + self.assertEqual(len(result), 3) + for node in result: + self.assertIn(node, self.G.nodes) + + def test_unweighted_graph(self): + result = eg.get_structural_holes_MaxD(self.G, k=2, C=self.communities) + self.assertEqual(len(result), 2) + + def test_disconnected_communities(self): + self.G.add_node(7) + new_comms = [frozenset([1, 2]), frozenset([7])] + result = eg.get_structural_holes_MaxD(self.G, k=1, C=new_comms) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_single_node_communities(self): + result = eg.get_structural_holes_MaxD( + self.G, k=1, C=[frozenset([1]), frozenset([6])] + ) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_disconnected_graph(self): + G = eg.Graph() + G.add_nodes_from([1, 2, 3, 4]) + G.add_edges_from([(1, 2), (3, 4)]) + comms = [frozenset([1, 2]), frozenset([3, 4])] + result = eg.get_structural_holes_MaxD(G, k=2, C=comms) + self.assertEqual(len(result), 2) + + def test_duplicate_nodes_in_communities(self): + result = eg.get_structural_holes_MaxD( + self.G, k=2, C=[frozenset([1, 2, 3]), frozenset([3, 4, 5])] + ) + self.assertEqual(len(result), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_NOBE.py b/easygraph/functions/structural_holes/tests/test_NOBE.py new file mode 100644 index 00000000..a350f89e --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_NOBE.py @@ -0,0 +1,75 @@ +import unittest + +import easygraph as eg + + +class TestNOBESpanners(unittest.TestCase): + def setUp(self): + self.G = eg.datasets.get_graph_karateclub() + + def test_nobe_sh_basic(self): + result = eg.NOBE_SH(self.G, K=2, topk=3) + self.assertEqual(len(result), 3) + self.assertTrue(all(node in self.G.nodes for node in result)) + + def test_nobe_ga_sh_basic(self): + result = eg.NOBE_GA_SH(self.G, K=2, topk=3) + self.assertEqual(len(result), 3) + + def test_nobe_sh_topk_equals_n(self): + result = eg.NOBE_SH(self.G, K=2, topk=self.G.number_of_nodes()) + self.assertEqual(len(result), self.G.number_of_nodes()) + + def test_nobe_ga_sh_topk_greater_than_n(self): + result = eg.NOBE_GA_SH(self.G, K=2, topk=self.G.number_of_nodes() + 5) + self.assertEqual(len(result), self.G.number_of_nodes()) + + def test_nobe_sh_k_equals_1(self): + result = eg.NOBE_SH(self.G, K=1, topk=2) + self.assertEqual(len(result), 2) + + def test_nobe_ga_sh_k_equals_1(self): + result = eg.NOBE_GA_SH(self.G, K=1, topk=2) + self.assertEqual(len(result), 2) + + def test_nobe_sh_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(1, 2), (3, 4)]) + result = eg.NOBE_SH(G, K=2, topk=2) + self.assertEqual(len(result), 2) + + def test_nobe_ga_sh_disconnected_graph(self): + G = eg.Graph() + G.add_edges_from([(1, 2), (3, 4)]) + result = eg.NOBE_GA_SH(G, K=2, topk=2) + self.assertEqual(len(result), 2) + + def test_nobe_sh_empty_graph(self): + G = eg.Graph() + with self.assertRaises(ValueError): + eg.NOBE_SH(G, K=2, topk=1) + + def test_nobe_ga_sh_empty_graph(self): + G = eg.Graph() + with self.assertRaises(ValueError): + eg.NOBE_GA_SH(G, K=2, topk=1) + + def test_nobe_sh_invalid_k(self): + with self.assertRaises(ValueError): + eg.NOBE_SH(self.G, K=0, topk=3) + + def test_nobe_ga_sh_invalid_k(self): + with self.assertRaises(ValueError): + eg.NOBE_GA_SH(self.G, K=0, topk=3) + + def test_nobe_sh_invalid_topk(self): + with self.assertRaises(ValueError): + eg.NOBE_SH(self.G, K=2, topk=0) + + def test_nobe_ga_sh_invalid_topk(self): + with self.assertRaises(ValueError): + eg.NOBE_GA_SH(self.G, K=2, topk=0) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_SHII_metric.py b/easygraph/functions/structural_holes/tests/test_SHII_metric.py new file mode 100644 index 00000000..1f7d8699 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_SHII_metric.py @@ -0,0 +1,61 @@ +import unittest + +import easygraph as eg +import numpy as np + + +class TestStructuralHoleInfluenceIndex(unittest.TestCase): + def setUp(self): + self.G = eg.datasets.get_graph_karateclub() + self.Com = [ + [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 18, 20, 22], + [9, 10, 15, 16, 19, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34], + ] + self.valid_seeds = [3, 20, 9] + + def test_ic_model_output(self): + result = eg.structural_hole_influence_index( + self.G, self.valid_seeds, self.Com, "IC", seedRatio=0.1, Directed=False + ) + self.assertIsInstance(result, dict) + + def test_lt_model_output(self): + result = eg.structural_hole_influence_index( + self.G, self.valid_seeds, self.Com, "LT", seedRatio=0.1, Directed=False + ) + self.assertIsInstance(result, dict) + + def test_directed_graph(self): + DG = self.G.to_directed() + result = eg.structural_hole_influence_index( + DG, self.valid_seeds, self.Com, "IC", Directed=True + ) + self.assertIsInstance(result, dict) + + def test_empty_seed_list(self): + result = eg.structural_hole_influence_index(self.G, [], self.Com, "IC") + self.assertEqual(result, {}) + + def test_seed_not_in_community(self): + result = eg.structural_hole_influence_index(self.G, [0], self.Com, "IC") + self.assertEqual(result, {}) + + def test_invalid_model(self): + with self.assertRaises(Exception): + eg.structural_hole_influence_index( + self.G, self.valid_seeds, self.Com, "XYZ" + ) + + def test_empty_community_list(self): + result = eg.structural_hole_influence_index(self.G, self.valid_seeds, [], "IC") + self.assertEqual(result, {}) + + def test_large_seed_ratio(self): + result = eg.structural_hole_influence_index( + self.G, self.valid_seeds, self.Com, "IC", seedRatio=2.0 + ) + self.assertIsInstance(result, dict) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_evaluation.py b/easygraph/functions/structural_holes/tests/test_evaluation.py new file mode 100644 index 00000000..b1d18ac3 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_evaluation.py @@ -0,0 +1,72 @@ +import math +import unittest + +import easygraph as eg + +from easygraph.functions.structural_holes import constraint +from easygraph.functions.structural_holes import effective_size +from easygraph.functions.structural_holes import efficiency +from easygraph.functions.structural_holes import hierarchy + + +class TestStructuralHoleMetrics(unittest.TestCase): + def setUp(self): + self.G = eg.Graph() + self.G.add_edges_from( + [ + (0, 1, {"weight": 1.0}), + (0, 2, {"weight": 2.0}), + (1, 2, {"weight": 1.0}), + (2, 3, {"weight": 3.0}), + (3, 4, {"weight": 1.0}), + ] + ) + self.G.add_node(5) # isolated node + + def test_effective_size_unweighted(self): + result = effective_size(self.G) + self.assertIn(0, result) + self.assertTrue(math.isnan(result[5])) + + def test_effective_size_weighted(self): + result = effective_size(self.G, weight="weight") + self.assertIn(0, result) + self.assertTrue(math.isnan(result[5])) + + def test_constraint_unweighted(self): + result = constraint(self.G) + self.assertIn(0, result) + self.assertTrue(math.isnan(result[5])) + + def test_constraint_weighted(self): + result = constraint(self.G, weight="weight") + self.assertIn(0, result) + self.assertTrue(math.isnan(result[5])) + + def test_hierarchy_unweighted(self): + result = hierarchy(self.G) + self.assertIn(0, result) + self.assertEqual(result[5], 0) + + def test_hierarchy_weighted(self): + result = hierarchy(self.G, weight="weight") + self.assertIn(0, result) + self.assertEqual(result[5], 0) + + def test_disconnected_components(self): + G = eg.Graph() + G.add_edges_from([(0, 1), (2, 3)]) # 2 components + for func in [effective_size, efficiency, constraint, hierarchy]: + result = func(G) + self.assertEqual(set(result.keys()), set(G.nodes)) + + def test_directed_graph_support(self): + DG = eg.DiGraph() + DG.add_edges_from([(0, 1), (1, 2)]) + result = effective_size(DG) + self.assertIsInstance(result, dict) + self.assertTrue(all(node in result for node in DG.nodes)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_maxBlock.py b/easygraph/functions/structural_holes/tests/test_maxBlock.py new file mode 100644 index 00000000..d503a166 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_maxBlock.py @@ -0,0 +1,66 @@ +import random +import unittest + +import easygraph as eg + + +class TestMaxBlockMethods(unittest.TestCase): + def setUp(self): + self.G = eg.DiGraph() + self.G.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 0), # Strongly connected + (2, 3), + (3, 4), + (4, 2), # Another cycle + ] + ) + for e in self.G.edges: + self.G[e[0]][e[1]]["weight"] = 0.9 + + self.f_set = {node: 0.5 for node in self.G.nodes} + + def test_maxBlockFast_single_node(self): + G = eg.DiGraph() + G.add_node(0) + result = eg.maxBlockFast(G, k=1, f_set={0: 1.0}, L=1) + self.assertEqual(result, [0]) + + def test_maxBlockFast_disconnected_graph(self): + G = eg.DiGraph() + G.add_nodes_from([0, 1, 2]) + result = eg.maxBlockFast(G, k=2, f_set={0: 0.2, 1: 0.3, 2: 0.5}, L=2) + self.assertEqual(len(result), 2) + + def test_maxBlock_basic(self): + result = eg.maxBlock( + self.G.copy(), + k=2, + f_set=self.f_set, + delta=1, + eps=0.5, + c=1, + flag_weight=True, + ) + self.assertEqual(len(result), 2) + + def test_maxBlock_unweighted_graph(self): + G = self.G.copy() + for e in G.edges: + del G[e[0]][e[1]]["weight"] + result = eg.maxBlock(G, k=2, f_set=self.f_set) + self.assertEqual(len(result), 2) + + def test_maxBlock_random_f_set(self): + result = eg.maxBlock(self.G.copy(), k=2, f_set=None, flag_weight=True) + self.assertEqual(len(result), 2) + + def test_maxBlock_invalid_k(self): + with self.assertRaises(IndexError): + eg.maxBlock(self.G.copy(), k=100, f_set=self.f_set) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_metrics.py b/easygraph/functions/structural_holes/tests/test_metrics.py new file mode 100644 index 00000000..de5417ad --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_metrics.py @@ -0,0 +1,105 @@ +import unittest + +import easygraph as eg + +from easygraph.functions.structural_holes.metrics import nodes_of_max_cc_without_shs +from easygraph.functions.structural_holes.metrics import structural_hole_influence_index +from easygraph.functions.structural_holes.metrics import sum_of_shortest_paths + + +class TestStructuralHoleMetrics(unittest.TestCase): + def setUp(self): + self.G = eg.datasets.get_graph_karateclub() + self.shs = [3, 9, 20] + self.communities = [ + [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 18, 20, 22], + [9, 10, 15, 16, 19, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34], + ] + + def test_sum_of_shortest_paths_valid(self): + result = sum_of_shortest_paths(self.G, self.shs) + self.assertIsInstance(result, (int, float)) + + def test_sum_of_shortest_paths_empty(self): + result = sum_of_shortest_paths(self.G, []) + self.assertEqual(result, 0) + + def test_nodes_of_max_cc_without_shs(self): + result = nodes_of_max_cc_without_shs(self.G, self.shs) + self.assertIsInstance(result, int) + self.assertLessEqual(result, self.G.number_of_nodes()) + + def test_nodes_of_max_cc_without_all_nodes(self): + result = nodes_of_max_cc_without_shs(self.G, list(self.G.nodes)) + self.assertEqual(result, 0) + + def test_structural_hole_influence_index_IC(self): + result = structural_hole_influence_index( + self.G, + self.shs, + self.communities, + model="IC", + Directed=False, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, + ) + self.assertIsInstance(result, dict) + self.assertTrue(all(isinstance(k, int) for k in result)) + self.assertTrue(all(isinstance(v, float) for v in result.values())) + + def test_structural_hole_influence_index_LT(self): + result = structural_hole_influence_index( + self.G, + self.shs, + self.communities, + model="LT", + Directed=False, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, + ) + self.assertIsInstance(result, dict) + + def test_structural_hole_influence_index_variant_LT(self): + result = structural_hole_influence_index( + self.G, + self.shs, + self.communities, + model="LT", + variant=True, + Directed=False, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, + ) + self.assertIsInstance(result, dict) + + def test_structural_hole_influence_index_empty_shs(self): + result = structural_hole_influence_index( + self.G, [], self.communities, model="IC", Directed=False + ) + self.assertEqual(result, {}) + + def test_structural_hole_influence_index_directed_flag(self): + result = structural_hole_influence_index( + self.G, + self.shs, + self.communities, + model="IC", + Directed=True, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, + ) + self.assertIsInstance(result, dict) + + def test_structural_hole_influence_index_no_shs_in_any_community(self): + result = structural_hole_influence_index( + self.G, [34], self.communities, model="LT", Directed=False + ) + self.assertIn(34, result) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/structural_holes/tests/test_weakTie.py b/easygraph/functions/structural_holes/tests/test_weakTie.py new file mode 100644 index 00000000..6a3861fc --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_weakTie.py @@ -0,0 +1,59 @@ +import unittest + +import easygraph as eg + +from easygraph.functions.structural_holes.weakTie import weakTie +from easygraph.functions.structural_holes.weakTie import weakTieLocal + + +class TestWeakTieFunctions(unittest.TestCase): + def setUp(self): + self.G = eg.DiGraph() + self.G.add_edges_from( + [ + (1, 5), + (1, 4), + (2, 1), + (2, 6), + (2, 9), + (3, 4), + (3, 1), + (4, 3), + (4, 1), + (4, 5), + (5, 4), + (5, 8), + (6, 1), + (6, 2), + (7, 2), + (7, 3), + (7, 10), + (8, 4), + (8, 5), + (9, 6), + (9, 10), + (10, 7), + (10, 9), + ] + ) + self.threshold = 0.2 + self.k = 3 + + def test_weak_tie_returns_top_k(self): + SHS_list, score_dict = weakTie(self.G.copy(), self.threshold, self.k) + self.assertEqual(len(SHS_list), self.k) + self.assertTrue(all(node in self.G.nodes for node in SHS_list)) + + def test_weak_tie_zero_k(self): + SHS_list, _ = weakTie(self.G.copy(), self.threshold, 0) + self.assertEqual(SHS_list, []) + + def test_with_isolated_node(self): + self.G.add_node(99) + SHS_list, score_dict = weakTie(self.G.copy(), self.threshold, self.k) + self.assertIn(99, score_dict) + self.assertIsInstance(score_dict[99], (int, float)) + + +if __name__ == "__main__": + unittest.main() diff --git a/easygraph/functions/tests/test_isolate.py b/easygraph/functions/tests/test_isolate.py index 9d1a2d67..89a88b95 100644 --- a/easygraph/functions/tests/test_isolate.py +++ b/easygraph/functions/tests/test_isolate.py @@ -1,6 +1,7 @@ """Unit tests for the :mod:`easygraph.functions.isolates` module.""" import easygraph as eg +import pytest def test_is_isolate(): @@ -24,3 +25,69 @@ def test_number_of_isolates(): G.add_edge(0, 1) G.add_nodes_from([2, 3]) assert eg.number_of_isolates(G) == 2 + + +def test_empty_graph_isolates(): + G = eg.Graph() + assert list(eg.isolates(G)) == [] + assert eg.number_of_isolates(G) == 0 + + +def test_all_isolates_graph(): + G = eg.Graph() + G.add_nodes_from(range(5)) + assert sorted(eg.isolates(G)) == list(range(5)) + assert all(eg.is_isolate(G, n) for n in G.nodes) + assert eg.number_of_isolates(G) == 5 + + +def test_directed_graph_sources_and_sinks_not_isolates(): + G = eg.DiGraph() + G.add_edges_from([(1, 2), (2, 3)]) + G.add_node(4) # truly isolated + assert eg.is_isolate(G, 4) + assert not eg.is_isolate(G, 1) # has out-degree + assert not eg.is_isolate(G, 3) # has in-degree + assert sorted(eg.isolates(G)) == [4] + assert eg.number_of_isolates(G) == 1 + + +def test_selfloop_not_isolate(): + G = eg.Graph() + G.add_node(1) + G.add_edge(1, 1) + assert not eg.is_isolate(G, 1) + assert list(eg.isolates(G)) == [] + assert eg.number_of_isolates(G) == 0 + + +def test_weighted_edges_isolate_behavior(): + G = eg.Graph() + G.add_edge(1, 2, weight=5) + G.add_node(3) + assert eg.is_isolate(G, 3) + assert not eg.is_isolate(G, 1) + assert eg.number_of_isolates(G) == 1 + + +def test_remove_isolate_then_check(): + G = eg.Graph() + G.add_nodes_from([1, 2, 3]) + G.add_edge(1, 2) + assert 3 in eg.isolates(G) + G.remove_node(3) + assert 3 not in G + assert 3 not in list(eg.isolates(G)) + + +def test_mixed_isolates_and_edges(): + G = eg.Graph() + G.add_nodes_from([0, 1, 2, 3, 4]) + G.add_edges_from([(0, 1), (1, 2)]) + # 3 and 4 are isolates + assert set(eg.isolates(G)) == {3, 4} + assert eg.number_of_isolates(G) == 2 + for node in [0, 1, 2]: + assert not eg.is_isolate(G, node) + for node in [3, 4]: + assert eg.is_isolate(G, node) diff --git a/easygraph/test.emb b/easygraph/test.emb new file mode 100644 index 00000000..8681bf01 --- /dev/null +++ b/easygraph/test.emb @@ -0,0 +1,4 @@ +-2.048150897026062012e-01 -5.754017829895019531e-01 1.261189728975296021e-01 1.013371825218200684e+00 -3.512124419212341309e-01 4.569326341152191162e-02 -2.158973515033721924e-01 -4.128263890743255615e-01 +1.521192789077758789e-01 -1.977750957012176514e-01 3.980364799499511719e-01 5.215392112731933594e-01 -3.355502784252166748e-01 -4.523791372776031494e-02 4.426106810569763184e-03 -2.394587099552154541e-01 +-2.048150897026062012e-01 -5.754017829895019531e-01 1.261189728975296021e-01 1.013371825218200684e+00 -3.512124419212341309e-01 4.569326341152191162e-02 -2.158973515033721924e-01 -4.128263890743255615e-01 +1.521192789077758789e-01 -1.977750957012176514e-01 3.980364799499511719e-01 5.215392112731933594e-01 -3.355502784252166748e-01 -4.523791372776031494e-02 4.426106810569763184e-03 -2.394587099552154541e-01 diff --git a/easygraph/tests/test_convert.py b/easygraph/tests/test_convert.py index 24757e9e..a4bc021b 100644 --- a/easygraph/tests/test_convert.py +++ b/easygraph/tests/test_convert.py @@ -97,3 +97,57 @@ def test_from_scipy(self): data = sp.sparse.csr_matrix([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) G = eg.from_scipy_sparse_matrix(data) self.assert_equal(self.G1, G) + + +def test_from_edgelist(): + edgelist = [(0, 1), (1, 2)] + G = eg.from_edgelist(edgelist) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_dict_of_lists(): + d = {0: [1], 1: [2]} + G = eg.to_easygraph_graph(d) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_dict_of_dicts(): + d = {0: {1: {}}, 1: {2: {}}} + G = eg.to_easygraph_graph(d) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_numpy_array(): + G = eg.complete_graph(3) + A = eg.to_numpy_array(G) + G2 = eg.from_numpy_array(A) + assert sorted((u, v) for u, v, _ in G.edges) == sorted( + (u, v) for u, v, _ in G2.edges + ) + + +def test_from_pandas_edgelist(): + df = pd.DataFrame({"source": [0, 1], "target": [1, 2], "weight": [0.5, 0.7]}) + G = eg.from_pandas_edgelist(df, source="source", target="target", edge_attr=True) + assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)] + + +def test_from_pandas_adjacency(): + df = pd.DataFrame([[0, 1], [1, 0]], columns=["A", "B"], index=["A", "B"]) + G = eg.from_pandas_adjacency(df) + assert sorted((u, v) for u, v, _ in G.edges) == [("A", "B")] + + +def test_from_scipy_sparse_matrix(): + mat = sp.sparse.csr_matrix([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) + G = eg.from_scipy_sparse_matrix(mat) + expected_edges = [(0, 1), (1, 2)] + assert sorted((u, v) for u, v, _ in G.edges) == expected_edges + + +def test_invalid_dict_type(): + class NotGraph: + pass + + with pytest.raises(eg.EasyGraphError): + eg.to_easygraph_graph(NotGraph())