From c34b027145fecbc580b851416499082c0d9b2780 Mon Sep 17 00:00:00 2001 From: sama Date: Thu, 26 Jun 2025 00:11:52 -0600 Subject: [PATCH 01/11] added test coverage in functions module --- .../functions/basic/tests/test_avg_degree.py | 38 ++++++++ .../functions/basic/tests/test_cluster.py | 38 ++++++++ .../functions/basic/tests/test_localassort.py | 89 ++++++++++++++++++- .../functions/basic/tests/test_predecessor.py | 60 +++++++++++++ 4 files changed, 223 insertions(+), 2 deletions(-) diff --git a/easygraph/functions/basic/tests/test_avg_degree.py b/easygraph/functions/basic/tests/test_avg_degree.py index e69de29b..4556bfe2 100644 --- a/easygraph/functions/basic/tests/test_avg_degree.py +++ b/easygraph/functions/basic/tests/test_avg_degree.py @@ -0,0 +1,38 @@ +import pytest +import easygraph as eg +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..6c35b496 100644 --- a/easygraph/functions/basic/tests/test_cluster.py +++ b/easygraph/functions/basic/tests/test_cluster.py @@ -377,3 +377,41 @@ 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..2ea2e934 100644 --- a/easygraph/functions/basic/tests/test_localassort.py +++ b/easygraph/functions/basic/tests/test_localassort.py @@ -1,9 +1,9 @@ import random import sys - -import easygraph as eg import numpy as np import pytest +import easygraph as eg +from easygraph.functions.basic.localassort import localAssort class TestLocalAssort: @@ -34,3 +34,88 @@ 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) + + +def test_localassort_invalid_restart_probability(): + G = eg.path_graph(4) + edgelist = np.array(list(G.edges)) + node_attr = np.array([0, 1, 0, 1]) + with pytest.raises(ValueError): + localAssort(edgelist, node_attr, pr=np.array([1.1])) # invalid pr HAORAN + + #solution: + """ + if np.any(pr > 1.0) or np.any(pr < 0.0): + raise ValueError("Restart probabilities must be between 0 and 1 (inclusive).") + """ \ No newline at end of file diff --git a/easygraph/functions/basic/tests/test_predecessor.py b/easygraph/functions/basic/tests/test_predecessor.py index d880560c..4af67f58 100644 --- a/easygraph/functions/basic/tests/test_predecessor.py +++ b/easygraph/functions/basic/tests/test_predecessor.py @@ -16,3 +16,63 @@ 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]} \ No newline at end of file From c428e61a338906a87c0692cb188563b52978af72 Mon Sep 17 00:00:00 2001 From: sama Date: Thu, 26 Jun 2025 02:04:48 -0600 Subject: [PATCH 02/11] finished classes tests --- easygraph/classes/operation.py | 8 +- easygraph/classes/tests/test_base.py | 141 +++++++++++++++++ .../classes/tests/test_directed_graph.py | 96 ++++++++++++ easygraph/classes/tests/test_graphV2.py | 121 +++++++++++++++ easygraph/classes/tests/test_multidigraph.py | 73 +++++++++ easygraph/classes/tests/test_multigraph.py | 96 ++++++++++++ easygraph/classes/tests/test_operation.py | 144 +++++++++++++++++- easygraph/tests/test_convert.py | 46 ++++++ 8 files changed, 719 insertions(+), 6 deletions(-) create mode 100644 easygraph/classes/tests/test_base.py create mode 100644 easygraph/classes/tests/test_directed_graph.py create mode 100644 easygraph/classes/tests/test_graphV2.py diff --git a/easygraph/classes/operation.py b/easygraph/classes/operation.py index c53d506c..31623da4 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 = [] @@ -274,7 +274,7 @@ def topological_generations(G): except KeyError as err: raise RuntimeError("Graph changed during iteration") from err if indegree_map[child] == 0: - zero_indegree.append(child) + zero_indegree.append(child) del indegree_map[child] yield this_generation @@ -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..7bcc4fed --- /dev/null +++ b/easygraph/classes/tests/test_base.py @@ -0,0 +1,141 @@ +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..1bcb9659 --- /dev/null +++ b/easygraph/classes/tests/test_directed_graph.py @@ -0,0 +1,96 @@ +import unittest +import os +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) \ No newline at end of file diff --git a/easygraph/classes/tests/test_graphV2.py b/easygraph/classes/tests/test_graphV2.py new file mode 100644 index 00000000..a31fc9eb --- /dev/null +++ b/easygraph/classes/tests/test_graphV2.py @@ -0,0 +1,121 @@ +import easygraph as eg +import unittest + +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) \ No newline at end of file diff --git a/easygraph/classes/tests/test_multidigraph.py b/easygraph/classes/tests/test_multidigraph.py index bd10561b..29f25b6d 100644 --- a/easygraph/classes/tests/test_multidigraph.py +++ b/easygraph/classes/tests/test_multidigraph.py @@ -31,7 +31,80 @@ def test_reverse(self): def test_attributes(self): print(self.g.edges) 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() diff --git a/easygraph/classes/tests/test_multigraph.py b/easygraph/classes/tests/test_multigraph.py index 28c36210..aa5ba3d1 100644 --- a/easygraph/classes/tests/test_multigraph.py +++ b/easygraph/classes/tests/test_multigraph.py @@ -59,3 +59,99 @@ 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_number_of_edges_specific_and_total(self): + G = eg.MultiGraph() + G.add_edges_from([(1, 2), (1, 2), (2, 3)]) + assert G.number_of_edges() == 3 + assert G.number_of_edges(1, 2) == 2 + assert G.number_of_edges(2, 1) == 2 # undirected + # invalid self.nodes HAORAN + # solution self.node or self.node.items + + 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..7101e275 100644 --- a/easygraph/classes/tests/test_operation.py +++ b/easygraph/classes/tests/test_operation.py @@ -1,8 +1,7 @@ import easygraph as eg import pytest - from easygraph.utils import edges_equal - +from easygraph.classes import operation @pytest.mark.parametrize( "graph_type", [eg.Graph, eg.DiGraph, eg.MultiGraph, eg.MultiDiGraph] @@ -13,3 +12,144 @@ 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] # HAORAN + # solution: + # def topological_sort(G): + # for generation in topological_generations(G): + # yield from generation + + # 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().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 = [] + # for node in this_generation: + # if node not in G: + # raise RuntimeError("Graph changed during iteration") + # for child in G.neighbors(node): + # try: + # indegree_map[child] -= 1 + # except KeyError as err: + # raise RuntimeError("Graph changed during iteration") from err + # if indegree_map[child] == 0: + # zero_indegree.append(child) + # del indegree_map[child] + # yield this_generation + + # if indegree_map: + # raise AssertionError("Graph contains a cycle or graph changed during iteration") \ No newline at end of file diff --git a/easygraph/tests/test_convert.py b/easygraph/tests/test_convert.py index 24757e9e..a156715e 100644 --- a/easygraph/tests/test_convert.py +++ b/easygraph/tests/test_convert.py @@ -97,3 +97,49 @@ 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()) From b566ea10e20f3b2ddbb89920253e8900d1a77b82 Mon Sep 17 00:00:00 2001 From: sama Date: Sun, 29 Jun 2025 21:41:44 -0600 Subject: [PATCH 03/11] finished tests for community, centrality and path --- .../functions/community/tests/test_LPA.py | 48 ++++++++++++ .../community/tests/test_ego_graph.py | 61 +++++++++++++++ .../functions/community/tests/test_louvian.py | 60 ++++++++++++++ .../community/tests/test_modularity.py | 68 ++++++++++++++++ .../tests/test_modularity_max_detection.py | 78 +++++++++++++++++++ .../functions/community/tests/test_motif.py | 75 +++++++++++++++++- 6 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 easygraph/functions/community/tests/test_LPA.py create mode 100644 easygraph/functions/community/tests/test_ego_graph.py create mode 100644 easygraph/functions/community/tests/test_louvian.py create mode 100644 easygraph/functions/community/tests/test_modularity.py create mode 100644 easygraph/functions/community/tests/test_modularity_max_detection.py diff --git a/easygraph/functions/community/tests/test_LPA.py b/easygraph/functions/community/tests/test_LPA.py new file mode 100644 index 00000000..f138dd73 --- /dev/null +++ b/easygraph/functions/community/tests/test_LPA.py @@ -0,0 +1,48 @@ +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..c04fb112 --- /dev/null +++ b/easygraph/functions/community/tests/test_ego_graph.py @@ -0,0 +1,61 @@ +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..80dd7ecd --- /dev/null +++ b/easygraph/functions/community/tests/test_louvian.py @@ -0,0 +1,60 @@ +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..cd2afdac --- /dev/null +++ b/easygraph/functions/community/tests/test_modularity.py @@ -0,0 +1,68 @@ +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..3705dfb9 --- /dev/null +++ b/easygraph/functions/community/tests/test_modularity_max_detection.py @@ -0,0 +1,78 @@ +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) + # The clique is very tightly connected, should ideally be one community + 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): + # The current implementation exits on empty or edge-less graphs, + # so we expect it not to return normally + 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) + # Each node should appear in only one community + 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..7cd3e2e6 100644 --- a/easygraph/functions/community/tests/test_motif.py +++ b/easygraph/functions/community/tests/test_motif.py @@ -1,5 +1,6 @@ import easygraph as eg - +import random +import unittest class TestMotif: @classmethod @@ -14,3 +15,75 @@ 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), # triangle + (3, 4), (4, 5) # 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] # should be length == k + 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] # Should skip everything + 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) From f363f0aec8f8eb797f48112afa551afa62164290 Mon Sep 17 00:00:00 2001 From: sama Date: Sun, 29 Jun 2025 21:43:24 -0600 Subject: [PATCH 04/11] tests finished for community, centrality and path --- easygraph/classes/tests/test_multigraph.py | 5 +- .../centrality/tests/test_betweenness.py | 76 ++++++++++++ .../centrality/tests/test_closeness.py | 65 ++++++++++- .../functions/centrality/tests/test_degree.py | 46 ++++++++ .../centrality/tests/test_egobetweenness.py | 49 ++++++++ .../centrality/tests/test_flowbetweenness.py | 74 ++++++++++++ .../centrality/tests/test_laplacian.py | 66 +++++++++++ .../centrality/tests/test_pagerank.py | 56 +++++++++ easygraph/functions/community/louvain.py | 5 + easygraph/functions/community/motif.py | 2 +- .../components/tests/test_biconnected.py | 59 ++++++++++ .../components/tests/test_connected.py | 78 ++++++++++++- .../tests/test_strongly_connected.py | 109 ++++++++++++++++-- .../components/tests/test_weakly_connected.py | 78 +++++++++++-- easygraph/functions/core/tests/test_k_core.py | 74 +++++++++++- easygraph/functions/path/bridges.py | 5 +- .../test_average_shortest_path_length.py | 51 +++++++- .../functions/path/tests/test_bridges.py | 61 ++++++++++ .../functions/path/tests/test_diameter.py | 50 ++++++++ easygraph/functions/path/tests/test_mst.py | 80 +++++++++++++ easygraph/functions/path/tests/test_path.py | 60 ++++++++++ easygraph/functions/tests/test_isolate.py | 67 ++++++++++- 22 files changed, 1184 insertions(+), 32 deletions(-) diff --git a/easygraph/classes/tests/test_multigraph.py b/easygraph/classes/tests/test_multigraph.py index aa5ba3d1..5f2f2e3d 100644 --- a/easygraph/classes/tests/test_multigraph.py +++ b/easygraph/classes/tests/test_multigraph.py @@ -101,7 +101,10 @@ def test_remove_edge_arbitrary(self): G.add_edge(1, 2) G.remove_edge(1, 2) assert G.number_of_edges(1, 2) == 1 - +if('Dijkstra') + .... + if('dijkstra') + ... def test_remove_edges_from_mixed(self): G = eg.MultiGraph() keys = G.add_edges_from([(1, 2), (1, 2), (2, 3)]) diff --git a/easygraph/functions/centrality/tests/test_betweenness.py b/easygraph/functions/centrality/tests/test_betweenness.py index 83656038..d826729f 100644 --- a/easygraph/functions/centrality/tests/test_betweenness.py +++ b/easygraph/functions/centrality/tests/test_betweenness.py @@ -16,11 +16,87 @@ 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..5c1dcb71 100644 --- a/easygraph/functions/centrality/tests/test_closeness.py +++ b/easygraph/functions/centrality/tests/test_closeness.py @@ -1,6 +1,7 @@ import unittest - import easygraph as eg +from easygraph.functions.centrality import closeness_centrality +from easygraph.classes.multigraph import MultiGraph class Test_closeness(unittest.TestCase): @@ -17,10 +18,68 @@ 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..562232cf 100644 --- a/easygraph/functions/centrality/tests/test_degree.py +++ b/easygraph/functions/centrality/tests/test_degree.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class Test_degree(unittest.TestCase): @@ -17,6 +18,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 +43,33 @@ 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..56371740 100644 --- a/easygraph/functions/centrality/tests/test_egobetweenness.py +++ b/easygraph/functions/centrality/tests/test_egobetweenness.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class Test_egobetweenness(unittest.TestCase): @@ -17,10 +18,58 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] 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..ecc8d13a 100644 --- a/easygraph/functions/centrality/tests/test_flowbetweenness.py +++ b/easygraph/functions/centrality/tests/test_flowbetweenness.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class Test_flowbetweenness(unittest.TestCase): @@ -17,11 +18,84 @@ 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..43fa65c8 100644 --- a/easygraph/functions/centrality/tests/test_laplacian.py +++ b/easygraph/functions/centrality/tests/test_laplacian.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class Test_laplacian(unittest.TestCase): @@ -16,11 +17,76 @@ 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__": diff --git a/easygraph/functions/centrality/tests/test_pagerank.py b/easygraph/functions/centrality/tests/test_pagerank.py index 8b687898..4d41c180 100644 --- a/easygraph/functions/centrality/tests/test_pagerank.py +++ b/easygraph/functions/centrality/tests/test_pagerank.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class Test_pagerank(unittest.TestCase): @@ -14,6 +15,30 @@ 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()] @@ -29,7 +54,38 @@ def test_google_matrix(self): for g in test_graphs: 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..742351f8 100644 --- a/easygraph/functions/community/motif.py +++ b/easygraph/functions/community/motif.py @@ -117,4 +117,4 @@ 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/components/tests/test_biconnected.py b/easygraph/functions/components/tests/test_biconnected.py index d534c2b1..f49c47cb 100644 --- a/easygraph/functions/components/tests/test_biconnected.py +++ b/easygraph/functions/components/tests/test_biconnected.py @@ -1,6 +1,14 @@ import unittest import easygraph as eg +from easygraph import ( + is_biconnected, + biconnected_components, + generator_biconnected_components_nodes, + generator_biconnected_components_edges, + generator_articulation_points, +) +import pytest class Test_biconnected(unittest.TestCase): @@ -30,5 +38,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..0266792b 100644 --- a/easygraph/functions/components/tests/test_connected.py +++ b/easygraph/functions/components/tests/test_connected.py @@ -2,7 +2,14 @@ import unittest import easygraph as eg - +from easygraph import ( + is_connected, + number_connected_components, + connected_components, + connected_components_directed, + connected_component_of_node, +) +from easygraph.utils.exception import EasyGraphNotImplemented class TestConnected(unittest.TestCase): def setUp(self): @@ -29,7 +36,76 @@ def test_connected_components_directed(self): 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..28207613 100644 --- a/easygraph/functions/components/tests/test_strongly_connected.py +++ b/easygraph/functions/components/tests/test_strongly_connected.py @@ -2,7 +2,13 @@ import unittest import easygraph as eg - +from easygraph import ( + strongly_connected_components, + number_strongly_connected_components, + is_strongly_connected, + condensation, +) +from easygraph.utils.exception import EasyGraphNotImplemented, EasyGraphPointlessConcept class Test_strongly_connected(unittest.TestCase): def setUp(self): @@ -10,17 +16,102 @@ 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..179ad0d7 100644 --- a/easygraph/functions/components/tests/test_weakly_connected.py +++ b/easygraph/functions/components/tests/test_weakly_connected.py @@ -1,20 +1,80 @@ import unittest - import easygraph as eg +from easygraph import ( + weakly_connected_components, + number_weakly_connected_components, + is_weakly_connected, +) +from easygraph.utils.exception import EasyGraphNotImplemented, 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..e4e1b9a2 100644 --- a/easygraph/functions/core/tests/test_k_core.py +++ b/easygraph/functions/core/tests/test_k_core.py @@ -1,5 +1,6 @@ import pytest - +import easygraph as eg +from easygraph import k_core @pytest.mark.parametrize( "edges,k", @@ -27,3 +28,74 @@ def test_k_core(edges, k): H = k_core(G) H_nx = nx.core_number(G_nx) # type: ignore 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) + # Path graph has lowest core values + 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) + # No nodes should have high core number + assert max(result) <= 2 \ No newline at end of file diff --git a/easygraph/functions/path/bridges.py b/easygraph/functions/path/bridges.py index b5ef42fa..7e50e598 100644 --- a/easygraph/functions/path/bridges.py +++ b/easygraph/functions/path/bridges.py @@ -3,8 +3,6 @@ import easygraph as eg from easygraph.utils.decorators import * - - __all__ = ["bridges", "has_bridges"] @@ -57,6 +55,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: @@ -105,6 +105,7 @@ def has_bridges(G, root=None): graph and $m$ is the number of edges. """ + # HAORAN , root is never used try: next(bridges(G)) except StopIteration: 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..33aeba6a 100644 --- a/easygraph/functions/path/tests/test_average_shortest_path_length.py +++ b/easygraph/functions/path/tests/test_average_shortest_path_length.py @@ -1,11 +1,54 @@ import unittest - import easygraph as eg +from easygraph import average_shortest_path_length +from easygraph.utils.exception import EasyGraphError, EasyGraphPointlessConcept + + +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") # HAORAN dijkstra and Dijkstra + # Expected paths: 0→1 (1), 0→2 (3), 0→3 (6), etc... + # Sum = 1+3+6 + 1+2+5 + 2+3+3 + 3+5+6 = 40, avg = 40 / 12 = 3.333... + 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) -class test_average_shortested_path_length(unittest.TestCase): - def setUp(self): - pass + 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..950923eb 100644 --- a/easygraph/functions/path/tests/test_bridges.py +++ b/easygraph/functions/path/tests/test_bridges.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphNotImplemented class test_bridges(unittest.TestCase): @@ -89,6 +90,66 @@ 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__": 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..b83e7ffe 100644 --- a/easygraph/functions/path/tests/test_mst.py +++ b/easygraph/functions/path/tests/test_mst.py @@ -76,6 +76,57 @@ 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 +161,35 @@ 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..a4674391 100644 --- a/easygraph/functions/path/tests/test_path.py +++ b/easygraph/functions/path/tests/test_path.py @@ -131,6 +131,66 @@ def test_multi_source_dijkstra(self): except ValueError as e: 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__": diff --git a/easygraph/functions/tests/test_isolate.py b/easygraph/functions/tests/test_isolate.py index 9d1a2d67..ea32e977 100644 --- a/easygraph/functions/tests/test_isolate.py +++ b/easygraph/functions/tests/test_isolate.py @@ -1,7 +1,7 @@ """Unit tests for the :mod:`easygraph.functions.isolates` module.""" import easygraph as eg - +import pytest def test_is_isolate(): G = eg.Graph() @@ -24,3 +24,68 @@ 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) \ No newline at end of file From 79f595d52654e22530ebccb4fcb4deb1f9568a63 Mon Sep 17 00:00:00 2001 From: sama Date: Sun, 29 Jun 2025 23:57:04 -0600 Subject: [PATCH 05/11] finished graph_generator and geometry tests --- .../functions/drawing/tests/test_geometry.py | 79 +++++++++++++++++++ easygraph/functions/graph_embedding/NOBE.py | 1 + easygraph/functions/graph_embedding/line.py | 8 +- .../functions/graph_embedding/tests/test.emb | 4 + .../graph_embedding/tests/test_deepwalk.py | 79 +++++++++++++++++++ .../graph_embedding/tests/test_line.py | 57 +++++++++++-- .../graph_embedding/tests/test_nobe.py | 24 ++++++ .../graph_embedding/tests/test_node2vec.py | 30 ++++++- .../graph_embedding/tests/test_sdne.py | 77 +++++++++++++++++- .../tests/test_Random_Network.py | 34 ++++++++ .../graph_generator/tests/test_classic.py | 66 ++++++++++++++++ 11 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 easygraph/functions/drawing/tests/test_geometry.py create mode 100644 easygraph/functions/graph_embedding/tests/test.emb diff --git a/easygraph/functions/drawing/tests/test_geometry.py b/easygraph/functions/drawing/tests/test_geometry.py new file mode 100644 index 00000000..051159b4 --- /dev/null +++ b/easygraph/functions/drawing/tests/test_geometry.py @@ -0,0 +1,79 @@ +import unittest +import math +import numpy as np + +from easygraph.functions.drawing.geometry import ( + radian_from_atan, + vlen, + common_tangent_radian, + polar_position, + rad_2_deg, +) + +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): + # r1 = r2 should give alpha = π/2 + self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2) + + def test_common_tangent_radian_invalid(self): + # when |r1 - r2| > d, acos gets invalid domain + with self.assertRaises(ValueError): + common_tangent_radian(5, 1, 2) # |5-1| > 2 → acos > 1 + + 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..4eceae4a 100644 --- a/easygraph/functions/graph_embedding/tests/test_deepwalk.py +++ b/easygraph/functions/graph_embedding/tests/test_deepwalk.py @@ -12,10 +12,89 @@ 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..09e8555f 100644 --- a/easygraph/functions/graph_embedding/tests/test_line.py +++ b/easygraph/functions/graph_embedding/tests/test_line.py @@ -1,13 +1,60 @@ import unittest - import easygraph as eg 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) + + 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)) -if __name__ == "__main__": - unittest.main() + 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..e4dccedf 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) @@ -24,7 +31,24 @@ def test_NOBE_GA(self): print(i) """ 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..d1af7683 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: @@ -23,7 +28,30 @@ def test_NOBE(self): 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..f503d145 100644 --- a/easygraph/functions/graph_embedding/tests/test_sdne.py +++ b/easygraph/functions/graph_embedding/tests/test_sdne.py @@ -1,5 +1,6 @@ import unittest - +import torch +import numpy as np import easygraph as eg @@ -16,6 +17,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 +33,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) \ No newline at end of file diff --git a/easygraph/functions/graph_generator/tests/test_Random_Network.py b/easygraph/functions/graph_generator/tests/test_Random_Network.py index c9c1ed04..7f770eaf 100644 --- a/easygraph/functions/graph_generator/tests/test_Random_Network.py +++ b/easygraph/functions/graph_generator/tests/test_Random_Network.py @@ -22,6 +22,40 @@ 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..ccf1208a 100644 --- a/easygraph/functions/graph_generator/tests/test_classic.py +++ b/easygraph/functions/graph_generator/tests/test_classic.py @@ -17,6 +17,72 @@ 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() From f3242af6ac72046bceaef71db271da87e07c9957 Mon Sep 17 00:00:00 2001 From: sama Date: Mon, 30 Jun 2025 21:14:27 -0600 Subject: [PATCH 06/11] finished tests for structural holes --- easygraph/functions/structural_holes/HAM.py | 6 ++ easygraph/functions/structural_holes/HIS.py | 7 ++ easygraph/functions/structural_holes/ICC.py | 4 + easygraph/functions/structural_holes/NOBE.py | 8 ++ .../functions/structural_holes/__init__.py | 1 + .../structural_holes/tests/test_AP_Greedy.py | 84 +++++++++++++++++++ .../structural_holes/tests/test_HAM.py | 50 +++++++++++ .../structural_holes/tests/test_HIS.py | 80 ++++++++++++++++++ .../structural_holes/tests/test_ICC.py | 71 ++++++++++++++++ .../structural_holes/tests/test_MaxD.py | 56 +++++++++++++ .../structural_holes/tests/test_NOBE.py | 74 ++++++++++++++++ .../tests/test_SHII_metric.py | 49 +++++++++++ .../structural_holes/tests/test_evaluation.py | 71 ++++++++++++++++ .../structural_holes/tests/test_maxBlock.py | 49 +++++++++++ .../structural_holes/tests/test_metrics.py | 80 ++++++++++++++++++ .../structural_holes/tests/test_weakTie.py | 57 +++++++++++++ 16 files changed, 747 insertions(+) create mode 100644 easygraph/functions/structural_holes/tests/test_AP_Greedy.py create mode 100644 easygraph/functions/structural_holes/tests/test_HAM.py create mode 100644 easygraph/functions/structural_holes/tests/test_HIS.py create mode 100644 easygraph/functions/structural_holes/tests/test_ICC.py create mode 100644 easygraph/functions/structural_holes/tests/test_MaxD.py create mode 100644 easygraph/functions/structural_holes/tests/test_NOBE.py create mode 100644 easygraph/functions/structural_holes/tests/test_SHII_metric.py create mode 100644 easygraph/functions/structural_holes/tests/test_evaluation.py create mode 100644 easygraph/functions/structural_holes/tests/test_maxBlock.py create mode 100644 easygraph/functions/structural_holes/tests/test_metrics.py create mode 100644 easygraph/functions/structural_holes/tests/test_weakTie.py diff --git a/easygraph/functions/structural_holes/HAM.py b/easygraph/functions/structural_holes/HAM.py index 6a5fe808..d9b44033 100644 --- a/easygraph/functions/structural_holes/HAM.py +++ b/easygraph/functions/structural_holes/HAM.py @@ -193,6 +193,12 @@ 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..ef200acf 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..0aaaa422 100644 --- a/easygraph/functions/structural_holes/__init__.py +++ b/easygraph/functions/structural_holes/__init__.py @@ -6,3 +6,4 @@ from .MaxD import * from .metrics import * from .NOBE import * +from .maxBlock import * \ No newline at end of file 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..13fc67ea --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_AP_Greedy.py @@ -0,0 +1,84 @@ +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..a087f7d0 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_HAM.py @@ -0,0 +1,50 @@ +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..4e39d8e8 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_HIS.py @@ -0,0 +1,80 @@ +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..be578c86 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_ICC.py @@ -0,0 +1,71 @@ +import unittest +import easygraph as eg +from easygraph.functions.structural_holes.ICC import ICC, BICC, AP_BICC + + +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..95b8b3f6 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_MaxD.py @@ -0,0 +1,56 @@ +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): + # Same test as above, graph is unweighted by default + 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..0e982ddd --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_NOBE.py @@ -0,0 +1,74 @@ +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..7beb1fda --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_SHII_metric.py @@ -0,0 +1,49 @@ +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..9880a056 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_evaluation.py @@ -0,0 +1,71 @@ +import unittest +import math +import easygraph as eg + +from easygraph.functions.structural_holes import ( + effective_size, + efficiency, + constraint, + 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..ee8a66e7 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_maxBlock.py @@ -0,0 +1,49 @@ +import unittest +import easygraph as eg +import random + +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..f54239ee --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_metrics.py @@ -0,0 +1,80 @@ +import unittest +import easygraph as eg +from easygraph.functions.structural_holes.metrics import ( + sum_of_shortest_paths, + nodes_of_max_cc_without_shs, + structural_hole_influence_index, +) + + +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..361b6c24 --- /dev/null +++ b/easygraph/functions/structural_holes/tests/test_weakTie.py @@ -0,0 +1,57 @@ +import unittest +import easygraph as eg +from easygraph.functions.structural_holes.weakTie import weakTie, 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_k_greater_than_nodes(self): + SHS_list, _ = weakTie(self.G.copy(), self.threshold, 100) + self.assertLessEqual(len(SHS_list), len(self.G.nodes)) + + def test_weak_tie_local_add_then_remove(self): + _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) + SHS_list = weakTieLocal(self.G.copy(), [[2, 7]], [[2, 7]], self.threshold, score_dict, self.k) + self.assertEqual(len(SHS_list), self.k) + + def test_weak_tie_local_add_only(self): + _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) + SHS_list = weakTieLocal(self.G.copy(), [[2, 7]], [], self.threshold, score_dict, self.k) + self.assertEqual(len(SHS_list), self.k) + + def test_weak_tie_local_remove_only(self): + _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) + SHS_list = weakTieLocal(self.G.copy(), [], [[2, 7]], self.threshold, score_dict, self.k) + self.assertEqual(len(SHS_list), self.k) + + 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)) + + def test_empty_graph(self): + G_empty = eg.DiGraph() + SHS_list, score_dict = weakTie(G_empty, self.threshold, self.k) + self.assertEqual(SHS_list, []) + self.assertEqual(score_dict, {}) From b81784a6555e49e14fd88fcaff857e4cbd34c461 Mon Sep 17 00:00:00 2001 From: sama Date: Mon, 30 Jun 2025 22:12:55 -0600 Subject: [PATCH 07/11] finished hypergraph tests --- .../hypergraph/centrality/s_centrality.py | 2 +- .../centrality/tests/test_cycle_ratio.py | 71 +++++++++++++++++++ .../centrality/tests/test_degree.py | 40 +++++++++++ .../centrality/tests/test_hypercoreness.py | 50 +++++++++++++ .../centrality/tests/test_s_centrality.py | 37 ++++++++++ .../tests/test_vector_centrality.py | 41 +++++++++++ .../centrality/vector_centrality.py | 26 ++++++- .../null_model/tests/test_classic.py | 38 ++++++++++ .../null_model/tests/test_lattice.py | 47 ++++++++++++ .../null_model/tests/test_simple.py | 61 ++++++++++++++++ .../hypergraph/tests/test_assortativity.py | 61 +++++++++++++++- .../tests/test_hypergraph_clustering.py | 57 +++++++++++++++ .../tests/test_hypergraph_operation.py | 38 ++++++++++ 13 files changed, 566 insertions(+), 3 deletions(-) create mode 100644 easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py create mode 100644 easygraph/functions/hypergraph/centrality/tests/test_degree.py create mode 100644 easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py create mode 100644 easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py create mode 100644 easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py create mode 100644 easygraph/functions/hypergraph/null_model/tests/test_lattice.py create mode 100644 easygraph/functions/hypergraph/null_model/tests/test_simple.py 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..8d28b6a4 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py @@ -0,0 +1,71 @@ +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..76ee04de --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_degree.py @@ -0,0 +1,40 @@ +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..0133e93b --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py @@ -0,0 +1,50 @@ +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..65db4354 --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py @@ -0,0 +1,37 @@ +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..36fc180f --- /dev/null +++ b/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py @@ -0,0 +1,41 @@ +import unittest +import numpy as np +import easygraph as eg +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..cbad21c2 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,27 @@ 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 \ No newline at end of file diff --git a/easygraph/functions/hypergraph/null_model/tests/test_classic.py b/easygraph/functions/hypergraph/null_model/tests/test_classic.py index dbb81a98..2c43c5f5 100644 --- a/easygraph/functions/hypergraph/null_model/tests/test_classic.py +++ b/easygraph/functions/hypergraph/null_model/tests/test_classic.py @@ -1,5 +1,6 @@ import easygraph as eg import pytest +from easygraph.utils.exception import EasyGraphError class TestClassic: @@ -46,3 +47,40 @@ 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) \ No newline at end of file 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..d563e478 --- /dev/null +++ b/easygraph/functions/hypergraph/null_model/tests/test_lattice.py @@ -0,0 +1,47 @@ +import pytest +import easygraph as eg +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..46a06659 --- /dev/null +++ b/easygraph/functions/hypergraph/null_model/tests/test_simple.py @@ -0,0 +1,61 @@ +import pytest +import easygraph as eg +from itertools import combinations + +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..df419079 100644 --- a/easygraph/functions/hypergraph/tests/test_assortativity.py +++ b/easygraph/functions/hypergraph/tests/test_assortativity.py @@ -2,6 +2,7 @@ import unittest from itertools import combinations +from easygraph.utils.exception import EasyGraphError import easygraph as eg import numpy as np @@ -15,7 +16,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 +69,32 @@ 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..285051ba 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphError class test_hypergraph_operation(unittest.TestCase): @@ -24,6 +25,62 @@ def test_hypergraph_two_node_clustering_coefficient(self): for i in self.hg: 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..4e44f2f6 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py @@ -2,6 +2,7 @@ import unittest import easygraph as eg +from easygraph.utils.exception import EasyGraphError class test_hypergraph_operation(unittest.TestCase): @@ -19,6 +20,43 @@ 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, 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() From 81c2a09231de713bb69901c2dc61282773c6ea15 Mon Sep 17 00:00:00 2001 From: sama Date: Mon, 30 Jun 2025 22:29:06 -0600 Subject: [PATCH 08/11] fixed failing tests --- easygraph/classes/tests/test_multigraph.py | 16 ++-------- easygraph/classes/tests/test_operation.py | 30 +------------------ .../functions/basic/tests/test_localassort.py | 16 +--------- .../path/average_shortest_path_length.py | 2 +- easygraph/functions/path/bridges.py | 3 +- .../test_average_shortest_path_length.py | 4 +-- 6 files changed, 7 insertions(+), 64 deletions(-) diff --git a/easygraph/classes/tests/test_multigraph.py b/easygraph/classes/tests/test_multigraph.py index 5f2f2e3d..a2c413a6 100644 --- a/easygraph/classes/tests/test_multigraph.py +++ b/easygraph/classes/tests/test_multigraph.py @@ -101,10 +101,7 @@ def test_remove_edge_arbitrary(self): G.add_edge(1, 2) G.remove_edge(1, 2) assert G.number_of_edges(1, 2) == 1 -if('Dijkstra') - .... - if('dijkstra') - ... + def test_remove_edges_from_mixed(self): G = eg.MultiGraph() keys = G.add_edges_from([(1, 2), (1, 2), (2, 3)]) @@ -136,16 +133,7 @@ def test_has_edge_variants(self): 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_number_of_edges_specific_and_total(self): - G = eg.MultiGraph() - G.add_edges_from([(1, 2), (1, 2), (2, 3)]) - assert G.number_of_edges() == 3 - assert G.number_of_edges(1, 2) == 2 - assert G.number_of_edges(2, 1) == 2 # undirected - # invalid self.nodes HAORAN - # solution self.node or self.node.items - + def test_get_edge_data_defaults(self): G = eg.MultiGraph() assert G.get_edge_data(10, 20) is None diff --git a/easygraph/classes/tests/test_operation.py b/easygraph/classes/tests/test_operation.py index 7101e275..f7b4d990 100644 --- a/easygraph/classes/tests/test_operation.py +++ b/easygraph/classes/tests/test_operation.py @@ -124,32 +124,4 @@ def test_topological_generations_branching(): # Valid topological generations: [1], [2, 3], [4] assert generations[0] == [1] assert set(generations[1]) == {2, 3} - assert generations[2] == [4] # HAORAN - # solution: - # def topological_sort(G): - # for generation in topological_generations(G): - # yield from generation - - # 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().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 = [] - # for node in this_generation: - # if node not in G: - # raise RuntimeError("Graph changed during iteration") - # for child in G.neighbors(node): - # try: - # indegree_map[child] -= 1 - # except KeyError as err: - # raise RuntimeError("Graph changed during iteration") from err - # if indegree_map[child] == 0: - # zero_indegree.append(child) - # del indegree_map[child] - # yield this_generation - - # if indegree_map: - # raise AssertionError("Graph contains a cycle or graph changed during iteration") \ No newline at end of file + assert generations[2] == [4] \ No newline at end of file diff --git a/easygraph/functions/basic/tests/test_localassort.py b/easygraph/functions/basic/tests/test_localassort.py index 2ea2e934..3ee925e3 100644 --- a/easygraph/functions/basic/tests/test_localassort.py +++ b/easygraph/functions/basic/tests/test_localassort.py @@ -104,18 +104,4 @@ 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) - - -def test_localassort_invalid_restart_probability(): - G = eg.path_graph(4) - edgelist = np.array(list(G.edges)) - node_attr = np.array([0, 1, 0, 1]) - with pytest.raises(ValueError): - localAssort(edgelist, node_attr, pr=np.array([1.1])) # invalid pr HAORAN - - #solution: - """ - if np.any(pr > 1.0) or np.any(pr < 0.0): - raise ValueError("Restart probabilities must be between 0 and 1 (inclusive).") - """ \ No newline at end of file + localAssort(edgelist, node_attr) \ No newline at end of file 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 7e50e598..8e4fbe88 100644 --- a/easygraph/functions/path/bridges.py +++ b/easygraph/functions/path/bridges.py @@ -105,9 +105,8 @@ def has_bridges(G, root=None): graph and $m$ is the number of edges. """ - # HAORAN , root is never used 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 33aeba6a..caa00171 100644 --- a/easygraph/functions/path/tests/test_average_shortest_path_length.py +++ b/easygraph/functions/path/tests/test_average_shortest_path_length.py @@ -15,9 +15,7 @@ def test_weighted_graph(self): 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") # HAORAN dijkstra and Dijkstra - # Expected paths: 0→1 (1), 0→2 (3), 0→3 (6), etc... - # Sum = 1+3+6 + 1+2+5 + 2+3+3 + 3+5+6 = 40, avg = 40 / 12 = 3.333... + result = average_shortest_path_length(G, weight="weight", method="dijkstra") self.assertAlmostEqual(result, 3.333, places=3) def test_trivial_graph(self): From 6f97ffeee2d068012987392099f4a2ee8022b4c6 Mon Sep 17 00:00:00 2001 From: sama Date: Mon, 30 Jun 2025 23:29:20 -0600 Subject: [PATCH 09/11] cleaned up tests --- .../structural_holes/tests/test_weakTie.py | 28 +++---------------- easygraph/test.emb | 4 +++ 2 files changed, 8 insertions(+), 24 deletions(-) create mode 100644 easygraph/test.emb diff --git a/easygraph/functions/structural_holes/tests/test_weakTie.py b/easygraph/functions/structural_holes/tests/test_weakTie.py index 361b6c24..9ec07165 100644 --- a/easygraph/functions/structural_holes/tests/test_weakTie.py +++ b/easygraph/functions/structural_holes/tests/test_weakTie.py @@ -2,6 +2,7 @@ import easygraph as eg from easygraph.functions.structural_holes.weakTie import weakTie, weakTieLocal + class TestWeakTieFunctions(unittest.TestCase): def setUp(self): @@ -25,33 +26,12 @@ def test_weak_tie_zero_k(self): SHS_list, _ = weakTie(self.G.copy(), self.threshold, 0) self.assertEqual(SHS_list, []) - def test_k_greater_than_nodes(self): - SHS_list, _ = weakTie(self.G.copy(), self.threshold, 100) - self.assertLessEqual(len(SHS_list), len(self.G.nodes)) - - def test_weak_tie_local_add_then_remove(self): - _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) - SHS_list = weakTieLocal(self.G.copy(), [[2, 7]], [[2, 7]], self.threshold, score_dict, self.k) - self.assertEqual(len(SHS_list), self.k) - - def test_weak_tie_local_add_only(self): - _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) - SHS_list = weakTieLocal(self.G.copy(), [[2, 7]], [], self.threshold, score_dict, self.k) - self.assertEqual(len(SHS_list), self.k) - - def test_weak_tie_local_remove_only(self): - _, score_dict = weakTie(self.G.copy(), self.threshold, self.k) - SHS_list = weakTieLocal(self.G.copy(), [], [[2, 7]], self.threshold, score_dict, self.k) - self.assertEqual(len(SHS_list), self.k) - 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)) - def test_empty_graph(self): - G_empty = eg.DiGraph() - SHS_list, score_dict = weakTie(G_empty, self.threshold, self.k) - self.assertEqual(SHS_list, []) - self.assertEqual(score_dict, {}) + +if __name__ == "__main__": + unittest.main() 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 From 2a2e25cced1426568ab065ea9af567277dc7d623 Mon Sep 17 00:00:00 2001 From: sama Date: Tue, 1 Jul 2025 00:29:44 -0600 Subject: [PATCH 10/11] removed extra comments --- .../community/tests/test_modularity_max_detection.py | 4 ---- easygraph/functions/community/tests/test_motif.py | 4 ++-- easygraph/functions/core/tests/test_k_core.py | 6 +----- easygraph/functions/drawing/tests/test_geometry.py | 4 +--- easygraph/functions/structural_holes/tests/test_MaxD.py | 1 - 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/easygraph/functions/community/tests/test_modularity_max_detection.py b/easygraph/functions/community/tests/test_modularity_max_detection.py index 3705dfb9..66a41e46 100644 --- a/easygraph/functions/community/tests/test_modularity_max_detection.py +++ b/easygraph/functions/community/tests/test_modularity_max_detection.py @@ -43,7 +43,6 @@ def test_communities_weighted(self): def test_communities_clique(self): result = eg.functions.community.greedy_modularity_communities(self.graph_clique) - # The clique is very tightly connected, should ideally be one community self.assertEqual(len(result), 1) self.assertSetEqual(result[0], set(self.graph_clique.nodes)) @@ -53,8 +52,6 @@ def test_communities_disconnected(self): self.assertSetEqual(flat_nodes, set(self.graph_disconnected.nodes)) def test_communities_single_node(self): - # The current implementation exits on empty or edge-less graphs, - # so we expect it not to return normally with self.assertRaises(SystemExit): eg.functions.community.greedy_modularity_communities(self.graph_single_node) @@ -64,7 +61,6 @@ def test_communities_empty_graph(self): def test_correct_partition_disjoint(self): result = eg.functions.community.greedy_modularity_communities(self.graph_disconnected) - # Each node should appear in only one community all_nodes = [node for group in result for node in group] self.assertEqual(len(all_nodes), len(set(all_nodes))) diff --git a/easygraph/functions/community/tests/test_motif.py b/easygraph/functions/community/tests/test_motif.py index 7cd3e2e6..5c0bacc0 100644 --- a/easygraph/functions/community/tests/test_motif.py +++ b/easygraph/functions/community/tests/test_motif.py @@ -64,12 +64,12 @@ def test_random_enumerate_cut_prob_valid(self): self.assertEqual(len(m), 3) def test_random_enumerate_cut_prob_invalid_length(self): - cut_prob = [1.0, 0.9] # should be length == k + 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] # Should skip everything + cut_prob = [0.0, 0.0, 0.0] motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob) self.assertEqual(motifs, []) diff --git a/easygraph/functions/core/tests/test_k_core.py b/easygraph/functions/core/tests/test_k_core.py index e4e1b9a2..2dd7e23b 100644 --- a/easygraph/functions/core/tests/test_k_core.py +++ b/easygraph/functions/core/tests/test_k_core.py @@ -18,15 +18,13 @@ 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(): @@ -77,7 +75,6 @@ def test_k_core_disconnected_components(): def test_k_core_all_zero_core(): G = eg.path_graph(5) result = k_core(G) - # Path graph has lowest core values assert all(isinstance(v, int) or isinstance(v, float) for v in result) assert max(result) <= 2 @@ -97,5 +94,4 @@ def test_k_core_large_k(): G = eg.Graph() G.add_edges_from([(1, 2), (2, 3)]) result = k_core(G) - # No nodes should have high core number assert max(result) <= 2 \ No newline at end of file diff --git a/easygraph/functions/drawing/tests/test_geometry.py b/easygraph/functions/drawing/tests/test_geometry.py index 051159b4..60c0ce79 100644 --- a/easygraph/functions/drawing/tests/test_geometry.py +++ b/easygraph/functions/drawing/tests/test_geometry.py @@ -49,13 +49,11 @@ def test_common_tangent_radian_reversed(self): self.assertAlmostEqual(angle, expected) def test_common_tangent_radian_touching(self): - # r1 = r2 should give alpha = π/2 self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2) def test_common_tangent_radian_invalid(self): - # when |r1 - r2| > d, acos gets invalid domain with self.assertRaises(ValueError): - common_tangent_radian(5, 1, 2) # |5-1| > 2 → acos > 1 + common_tangent_radian(5, 1, 2) def test_polar_position_origin(self): pos = polar_position(0, 0, np.array([5, 5])) diff --git a/easygraph/functions/structural_holes/tests/test_MaxD.py b/easygraph/functions/structural_holes/tests/test_MaxD.py index 95b8b3f6..789e7422 100644 --- a/easygraph/functions/structural_holes/tests/test_MaxD.py +++ b/easygraph/functions/structural_holes/tests/test_MaxD.py @@ -25,7 +25,6 @@ def test_top_k_greater_than_1(self): self.assertIn(node, self.G.nodes) def test_unweighted_graph(self): - # Same test as above, graph is unweighted by default result = eg.get_structural_holes_MaxD(self.G, k=2, C=self.communities) self.assertEqual(len(result), 2) From eeef0a2f5dfb28aab7093e10505b099dd8845e6c Mon Sep 17 00:00:00 2001 From: sama Date: Tue, 1 Jul 2025 02:03:52 -0600 Subject: [PATCH 11/11] fixed linter errors --- easygraph/classes/operation.py | 2 +- easygraph/classes/tests/test_base.py | 30 +++++---- .../classes/tests/test_directed_graph.py | 13 ++-- easygraph/classes/tests/test_graphV2.py | 9 +-- easygraph/classes/tests/test_multidigraph.py | 9 ++- easygraph/classes/tests/test_multigraph.py | 51 ++++++++-------- easygraph/classes/tests/test_operation.py | 14 +++-- .../functions/basic/tests/test_avg_degree.py | 5 +- .../functions/basic/tests/test_cluster.py | 9 +-- .../functions/basic/tests/test_localassort.py | 7 ++- .../functions/basic/tests/test_predecessor.py | 5 +- .../centrality/tests/test_betweenness.py | 11 ++-- .../centrality/tests/test_closeness.py | 8 +-- .../functions/centrality/tests/test_degree.py | 3 + .../centrality/tests/test_egobetweenness.py | 10 ++- .../centrality/tests/test_flowbetweenness.py | 51 ++++++---------- .../centrality/tests/test_laplacian.py | 61 +++++++++++-------- .../centrality/tests/test_pagerank.py | 9 ++- easygraph/functions/community/motif.py | 4 +- .../functions/community/tests/test_LPA.py | 43 +++++++++---- .../community/tests/test_ego_graph.py | 18 +++--- .../functions/community/tests/test_louvian.py | 19 +++--- .../community/tests/test_modularity.py | 5 +- .../tests/test_modularity_max_detection.py | 25 +++++--- .../functions/community/tests/test_motif.py | 18 +++--- .../components/tests/test_biconnected.py | 17 +++--- .../components/tests/test_connected.py | 17 +++--- .../tests/test_strongly_connected.py | 27 ++++---- .../components/tests/test_weakly_connected.py | 13 ++-- easygraph/functions/core/tests/test_k_core.py | 10 ++- .../functions/drawing/tests/test_geometry.py | 19 +++--- .../graph_embedding/tests/test_deepwalk.py | 3 +- .../graph_embedding/tests/test_line.py | 33 +++++++--- .../graph_embedding/tests/test_nobe.py | 3 + .../graph_embedding/tests/test_node2vec.py | 3 +- .../graph_embedding/tests/test_sdne.py | 7 ++- .../tests/test_Random_Network.py | 2 + .../graph_generator/tests/test_classic.py | 19 +++--- .../centrality/tests/test_cycle_ratio.py | 1 + .../centrality/tests/test_degree.py | 8 +-- .../centrality/tests/test_hypercoreness.py | 1 + .../centrality/tests/test_s_centrality.py | 3 + .../tests/test_vector_centrality.py | 10 +-- .../centrality/vector_centrality.py | 5 +- .../null_model/tests/test_classic.py | 23 ++++--- .../null_model/tests/test_lattice.py | 6 +- .../null_model/tests/test_simple.py | 11 ++-- .../hypergraph/tests/test_assortativity.py | 5 +- .../tests/test_hypergraph_clustering.py | 3 + .../tests/test_hypergraph_operation.py | 18 ++++-- easygraph/functions/path/bridges.py | 6 +- .../test_average_shortest_path_length.py | 5 +- .../functions/path/tests/test_bridges.py | 10 +-- easygraph/functions/path/tests/test_mst.py | 33 +++------- easygraph/functions/path/tests/test_path.py | 5 +- easygraph/functions/structural_holes/HAM.py | 4 +- easygraph/functions/structural_holes/NOBE.py | 2 +- .../functions/structural_holes/__init__.py | 2 +- .../structural_holes/tests/test_AP_Greedy.py | 3 +- .../structural_holes/tests/test_HAM.py | 43 +++++++++---- .../structural_holes/tests/test_HIS.py | 21 ++++--- .../structural_holes/tests/test_ICC.py | 11 ++-- .../structural_holes/tests/test_MaxD.py | 23 ++++--- .../structural_holes/tests/test_NOBE.py | 1 + .../tests/test_SHII_metric.py | 24 ++++++-- .../structural_holes/tests/test_evaluation.py | 29 ++++----- .../structural_holes/tests/test_maxBlock.py | 29 +++++++-- .../structural_holes/tests/test_metrics.py | 51 ++++++++++++---- .../structural_holes/tests/test_weakTie.py | 40 +++++++++--- easygraph/functions/tests/test_isolate.py | 4 +- easygraph/tests/test_convert.py | 24 +++++--- 71 files changed, 655 insertions(+), 421 deletions(-) diff --git a/easygraph/classes/operation.py b/easygraph/classes/operation.py index 31623da4..cb7b2d0e 100644 --- a/easygraph/classes/operation.py +++ b/easygraph/classes/operation.py @@ -274,7 +274,7 @@ def topological_generations(G): except KeyError as err: raise RuntimeError("Graph changed during iteration") from err if indegree_map[child] == 0: - zero_indegree.append(child) + zero_indegree.append(child) del indegree_map[child] yield this_generation diff --git a/easygraph/classes/tests/test_base.py b/easygraph/classes/tests/test_base.py index 7bcc4fed..808a35ba 100644 --- a/easygraph/classes/tests/test_base.py +++ b/easygraph/classes/tests/test_base.py @@ -1,11 +1,14 @@ 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 * @@ -48,11 +51,13 @@ def assert_equal(self, G1, G2): 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}), - ]) + 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) @@ -77,14 +82,15 @@ 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") + @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) @@ -107,15 +113,13 @@ 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) + 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] - }) + 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)] diff --git a/easygraph/classes/tests/test_directed_graph.py b/easygraph/classes/tests/test_directed_graph.py index 1bcb9659..568286ff 100644 --- a/easygraph/classes/tests/test_directed_graph.py +++ b/easygraph/classes/tests/test_directed_graph.py @@ -1,10 +1,10 @@ -import unittest import os +import unittest + from easygraph import DiGraph class TestDiGraph(unittest.TestCase): - def setUp(self): self.G = DiGraph() @@ -40,17 +40,18 @@ def test_remove_edge(self): 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}]) + 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(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"))) @@ -93,4 +94,4 @@ def test_file_add_edges(self): 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) \ No newline at end of file + 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 index a31fc9eb..f3a4a233 100644 --- a/easygraph/classes/tests/test_graphV2.py +++ b/easygraph/classes/tests/test_graphV2.py @@ -1,8 +1,9 @@ -import easygraph as eg import unittest -class TestEasyGraph(unittest.TestCase): +import easygraph as eg + +class TestEasyGraph(unittest.TestCase): def setUp(self): self.G = eg.Graph() @@ -66,7 +67,7 @@ def test_ego_subgraph(self): self.assertIn(4, ego.nodes) def test_to_index_node_graph(self): - self.G.add_edges([('a', 'b'), ('b', 'c')]) + 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)) @@ -118,4 +119,4 @@ def test_clear_cache(self): _ = self.G.edges self.assertIn("edge", self.G.cache) self.G._clear_cache() - self.assertEqual(len(self.G.cache), 0) \ No newline at end of file + self.assertEqual(len(self.G.cache), 0) diff --git a/easygraph/classes/tests/test_multidigraph.py b/easygraph/classes/tests/test_multidigraph.py index 29f25b6d..7609d529 100644 --- a/easygraph/classes/tests/test_multidigraph.py +++ b/easygraph/classes/tests/test_multidigraph.py @@ -31,9 +31,9 @@ def test_reverse(self): def test_attributes(self): print(self.g.edges) print(self.g.in_edges) - -class TestMultiDiGraph(unittest.TestCase): + +class TestMultiDiGraph(unittest.TestCase): def setUp(self): self.G = eg.MultiDiGraph() @@ -85,7 +85,9 @@ def test_in_out_degree(self): 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() + d.get("weight", 1) + for key_dict in preds.values() + for d in key_dict.values() ) self.assertEqual(in_deg["B"], 5) @@ -106,6 +108,7 @@ 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 a2c413a6..963a0613 100644 --- a/easygraph/classes/tests/test_multigraph.py +++ b/easygraph/classes/tests/test_multigraph.py @@ -59,8 +59,9 @@ 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: + +class TestMultiGraphExtended: def test_add_multiple_edges_and_keys(self): G = eg.MultiGraph() k0 = G.add_edge(1, 2) @@ -71,29 +72,29 @@ def test_add_multiple_edges_and_keys(self): 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'} + 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 + (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 + 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') + 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() @@ -116,33 +117,33 @@ def test_to_directed_graph(self): 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 + 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) + G.add_edge(1, 2, key="x", weight=9) H = G.copy() - assert H.get_edge_data(1, 2, 'x') == {'weight': 9} + 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') + 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') + 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') - + 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' + 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) + 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 f7b4d990..2f653fac 100644 --- a/easygraph/classes/tests/test_operation.py +++ b/easygraph/classes/tests/test_operation.py @@ -1,7 +1,9 @@ import easygraph as eg import pytest + +from easygraph.classes import operation from easygraph.utils import edges_equal -from easygraph.classes import operation + @pytest.mark.parametrize( "graph_type", [eg.Graph, eg.DiGraph, eg.MultiGraph, eg.MultiDiGraph] @@ -13,6 +15,7 @@ def test_selfloops(graph_type): 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") @@ -80,8 +83,8 @@ def test_topological_sort_cycle(): 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') + 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)) @@ -89,7 +92,7 @@ def test_selfloop_edges_variants(): 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]) + assert "x" in [k for _, _, k, _ in full] def test_number_of_selfloops(): @@ -110,6 +113,7 @@ def test_density_directed(): 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)]) @@ -124,4 +128,4 @@ def test_topological_generations_branching(): # Valid topological generations: [1], [2, 3], [4] assert generations[0] == [1] assert set(generations[1]) == {2, 3} - assert generations[2] == [4] \ No newline at end of file + 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 4556bfe2..adf19b5e 100644 --- a/easygraph/functions/basic/tests/test_avg_degree.py +++ b/easygraph/functions/basic/tests/test_avg_degree.py @@ -1,5 +1,6 @@ -import pytest import easygraph as eg +import pytest + from easygraph.functions.basic import average_degree @@ -14,6 +15,7 @@ def test_average_degree_empty_graph(): with pytest.raises(ZeroDivisionError): average_degree(G) + def test_average_degree_self_loop(): G = eg.Graph() G.add_edge(1, 1) # self-loop @@ -33,6 +35,7 @@ def test_average_degree_directed_graph(): 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 6c35b496..548bb7ae 100644 --- a/easygraph/functions/basic/tests/test_cluster.py +++ b/easygraph/functions/basic/tests/test_cluster.py @@ -377,8 +377,9 @@ 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: + +class TestAdditionalClusteringCases: def test_self_loops_ignored(self): G = eg.Graph() G.add_edges_from([(0, 1), (1, 2), (2, 0)]) @@ -405,9 +406,9 @@ def test_custom_weight_name(self): 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 + 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): diff --git a/easygraph/functions/basic/tests/test_localassort.py b/easygraph/functions/basic/tests/test_localassort.py index 3ee925e3..86ddc4af 100644 --- a/easygraph/functions/basic/tests/test_localassort.py +++ b/easygraph/functions/basic/tests/test_localassort.py @@ -1,8 +1,10 @@ import random import sys + +import easygraph as eg import numpy as np import pytest -import easygraph as eg + from easygraph.functions.basic.localassort import localAssort @@ -35,6 +37,7 @@ def test_karateclub(self): 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)) @@ -104,4 +107,4 @@ 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) \ No newline at end of file + localAssort(edgelist, node_attr) diff --git a/easygraph/functions/basic/tests/test_predecessor.py b/easygraph/functions/basic/tests/test_predecessor.py index 4af67f58..6c772c78 100644 --- a/easygraph/functions/basic/tests/test_predecessor.py +++ b/easygraph/functions/basic/tests/test_predecessor.py @@ -16,6 +16,7 @@ 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) @@ -67,7 +68,7 @@ def test_no_path_to_target_with_return_seen(self): def test_cycle_graph(self): G = eg.Graph() - G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) # cycled 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) @@ -75,4 +76,4 @@ 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]} \ No newline at end of file + 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 d826729f..82d6a556 100644 --- a/easygraph/functions/centrality/tests/test_betweenness.py +++ b/easygraph/functions/centrality/tests/test_betweenness.py @@ -16,7 +16,7 @@ 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)]) @@ -38,7 +38,7 @@ def setUp(self): 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)) @@ -90,13 +90,10 @@ def test_multigraph_error(self): def test_all_nodes_type_mix(self): G = eg.Graph() - G.add_edges_from([ - (1, 2), - ("A", "B"), - ((1, 2), (3, 4)) - ]) + 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 5c1dcb71..af048602 100644 --- a/easygraph/functions/centrality/tests/test_closeness.py +++ b/easygraph/functions/centrality/tests/test_closeness.py @@ -1,7 +1,9 @@ import unittest + import easygraph as eg -from easygraph.functions.centrality import closeness_centrality + from easygraph.classes.multigraph import MultiGraph +from easygraph.functions.centrality import closeness_centrality class Test_closeness(unittest.TestCase): @@ -36,9 +38,7 @@ def setUp(self): 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)) - ]) + 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: diff --git a/easygraph/functions/centrality/tests/test_degree.py b/easygraph/functions/centrality/tests/test_degree.py index 562232cf..24a3079d 100644 --- a/easygraph/functions/centrality/tests/test_degree.py +++ b/easygraph/functions/centrality/tests/test_degree.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -71,5 +72,7 @@ def test_in_out_degree_centrality_single_node(self): 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 56371740..48e7024f 100644 --- a/easygraph/functions/centrality/tests/test_egobetweenness.py +++ b/easygraph/functions/centrality/tests/test_egobetweenness.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -18,7 +19,7 @@ def setUp(self): self.test_graphs = [eg.Graph(), eg.DiGraph()] 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)]) @@ -26,11 +27,7 @@ def setUp(self): 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.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) @@ -71,5 +68,6 @@ 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 ecc8d13a..82759aa5 100644 --- a/easygraph/functions/centrality/tests/test_flowbetweenness.py +++ b/easygraph/functions/centrality/tests/test_flowbetweenness.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -19,49 +20,34 @@ def setUp(self): 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.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.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.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.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.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) - ]) + self.multigraph.add_edges_from([(0, 1), (0, 1)]) + def test_flowbetweenness_centrality(self): for i in self.test_graphs: print(i.edges) @@ -70,7 +56,9 @@ def test_flowbetweenness_centrality(self): 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())) + 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) @@ -97,5 +85,6 @@ 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 43fa65c8..d7dda7ea 100644 --- a/easygraph/functions/centrality/tests/test_laplacian.py +++ b/easygraph/functions/centrality/tests/test_laplacian.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -18,48 +19,60 @@ 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.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.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.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.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.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)) diff --git a/easygraph/functions/centrality/tests/test_pagerank.py b/easygraph/functions/centrality/tests/test_pagerank.py index 4d41c180..6b1cfe8a 100644 --- a/easygraph/functions/centrality/tests/test_pagerank.py +++ b/easygraph/functions/centrality/tests/test_pagerank.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -28,11 +29,7 @@ def setUp(self): 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.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") @@ -54,6 +51,7 @@ def test_google_matrix(self): for g in test_graphs: 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)) @@ -87,5 +85,6 @@ 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/motif.py b/easygraph/functions/community/motif.py index 742351f8..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, cut_prob) + 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 index f138dd73..34c96fb0 100644 --- a/easygraph/functions/community/tests/test_LPA.py +++ b/easygraph/functions/community/tests/test_LPA.py @@ -1,19 +1,22 @@ import unittest + import easygraph as eg -class TestLabelPropagationAlgorithms(unittest.TestCase): +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_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)]) @@ -30,19 +33,33 @@ def test_lpa(self): 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.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)) + 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)) + 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.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 index c04fb112..e0cdbb70 100644 --- a/easygraph/functions/community/tests/test_ego_graph.py +++ b/easygraph/functions/community/tests/test_ego_graph.py @@ -1,6 +1,8 @@ import unittest + import easygraph as eg + class TestEgoGraph(unittest.TestCase): def setUp(self): self.simple_graph = eg.Graph() @@ -10,11 +12,9 @@ def setUp(self): 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.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)]) @@ -35,7 +35,9 @@ def test_directed_graph(self): 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") + 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): @@ -47,7 +49,9 @@ def test_single_node_graph(self): 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) + 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): diff --git a/easygraph/functions/community/tests/test_louvian.py b/easygraph/functions/community/tests/test_louvian.py index 80dd7ecd..31f11ad4 100644 --- a/easygraph/functions/community/tests/test_louvian.py +++ b/easygraph/functions/community/tests/test_louvian.py @@ -1,17 +1,17 @@ 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_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)]) @@ -30,12 +30,16 @@ def test_louvain_communities_simple(self): 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") + 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) + 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)) @@ -56,5 +60,6 @@ def test_louvain_partitions_progressive_size(self): 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 index cd2afdac..5ca1fd6e 100644 --- a/easygraph/functions/community/tests/test_modularity.py +++ b/easygraph/functions/community/tests/test_modularity.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg @@ -32,7 +33,9 @@ def test_directed_modularity(self): def test_weighted_graph(self): communities = [{0, 1}, {2}] - q = eg.functions.community.modularity(self.G_weighted, communities, weight="weight") + q = eg.functions.community.modularity( + self.G_weighted, communities, weight="weight" + ) self.assertIsInstance(q, float) def test_self_loops(self): diff --git a/easygraph/functions/community/tests/test_modularity_max_detection.py b/easygraph/functions/community/tests/test_modularity_max_detection.py index 66a41e46..a7ffbe1f 100644 --- a/easygraph/functions/community/tests/test_modularity_max_detection.py +++ b/easygraph/functions/community/tests/test_modularity_max_detection.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg @@ -10,11 +11,9 @@ def setUp(self): # 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}) - ]) + 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() @@ -37,7 +36,9 @@ def test_communities_simple(self): self.assertSetEqual(flat_nodes, set(self.graph_simple.nodes)) def test_communities_weighted(self): - result = eg.functions.community.greedy_modularity_communities(self.graph_weighted) + 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)) @@ -47,7 +48,9 @@ def test_communities_clique(self): self.assertSetEqual(result[0], set(self.graph_clique.nodes)) def test_communities_disconnected(self): - result = eg.functions.community.greedy_modularity_communities(self.graph_disconnected) + 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)) @@ -60,12 +63,16 @@ def test_communities_empty_graph(self): 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) + 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) + result = eg.functions.community.greedy_modularity_communities( + self.graph_disconnected + ) sizes = [len(group) for group in result] self.assertEqual(sizes, sorted(sizes, reverse=True)) diff --git a/easygraph/functions/community/tests/test_motif.py b/easygraph/functions/community/tests/test_motif.py index 5c0bacc0..5b5fca9f 100644 --- a/easygraph/functions/community/tests/test_motif.py +++ b/easygraph/functions/community/tests/test_motif.py @@ -1,7 +1,9 @@ -import easygraph as eg import random import unittest +import easygraph as eg + + class TestMotif: @classmethod def setup_class(self): @@ -16,21 +18,19 @@ def test_esu(self): 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), # triangle - (3, 4), (4, 5) # tail - ]) + 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 = [{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: @@ -69,7 +69,7 @@ def test_random_enumerate_cut_prob_invalid_length(self): 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] + cut_prob = [0.0, 0.0, 0.0] motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob) self.assertEqual(motifs, []) diff --git a/easygraph/functions/components/tests/test_biconnected.py b/easygraph/functions/components/tests/test_biconnected.py index f49c47cb..ae542675 100644 --- a/easygraph/functions/components/tests/test_biconnected.py +++ b/easygraph/functions/components/tests/test_biconnected.py @@ -1,15 +1,14 @@ import unittest import easygraph as eg -from easygraph import ( - is_biconnected, - biconnected_components, - generator_biconnected_components_nodes, - generator_biconnected_components_edges, - generator_articulation_points, -) 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): def setUp(self): @@ -76,7 +75,7 @@ def test_cycle_plus_leaf(self): 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 + 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)) @@ -86,7 +85,7 @@ def test_multiple_biconnected_components(self): 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]) + self.assertEqual(aps, [3, 2, 1]) if __name__ == "__main__": diff --git a/easygraph/functions/components/tests/test_connected.py b/easygraph/functions/components/tests/test_connected.py index 0266792b..4d7db5b7 100644 --- a/easygraph/functions/components/tests/test_connected.py +++ b/easygraph/functions/components/tests/test_connected.py @@ -2,15 +2,15 @@ import unittest import easygraph as eg -from easygraph import ( - is_connected, - number_connected_components, - connected_components, - connected_components_directed, - connected_component_of_node, -) + +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): self.edges = [(1, 2), (2, 3), (0, 4), (2, 1), (0, 0), (-99, 256)] @@ -36,7 +36,7 @@ def test_connected_components_directed(self): 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): @@ -107,5 +107,6 @@ def test_multigraph_blocked(self): 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 28207613..904f3456 100644 --- a/easygraph/functions/components/tests/test_strongly_connected.py +++ b/easygraph/functions/components/tests/test_strongly_connected.py @@ -2,13 +2,14 @@ import unittest import easygraph as eg -from easygraph import ( - strongly_connected_components, - number_strongly_connected_components, - is_strongly_connected, - condensation, -) -from easygraph.utils.exception import EasyGraphNotImplemented, EasyGraphPointlessConcept + +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): @@ -58,16 +59,18 @@ def test_scc_with_self_loops(self): 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)]) + 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 @@ -80,9 +83,9 @@ def visit(node): 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): diff --git a/easygraph/functions/components/tests/test_weakly_connected.py b/easygraph/functions/components/tests/test_weakly_connected.py index 179ad0d7..43941f46 100644 --- a/easygraph/functions/components/tests/test_weakly_connected.py +++ b/easygraph/functions/components/tests/test_weakly_connected.py @@ -1,11 +1,12 @@ import unittest + import easygraph as eg -from easygraph import ( - weakly_connected_components, - number_weakly_connected_components, - is_weakly_connected, -) -from easygraph.utils.exception import EasyGraphNotImplemented, EasyGraphPointlessConcept + +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): diff --git a/easygraph/functions/core/tests/test_k_core.py b/easygraph/functions/core/tests/test_k_core.py index 2dd7e23b..55efcd69 100644 --- a/easygraph/functions/core/tests/test_k_core.py +++ b/easygraph/functions/core/tests/test_k_core.py @@ -1,7 +1,9 @@ -import pytest import easygraph as eg +import pytest + from easygraph import k_core + @pytest.mark.parametrize( "edges,k", [ @@ -24,9 +26,10 @@ def test_k_core(edges, k): G_nx.add_edges_from(edges) H = k_core(G) - H_nx = nx.core_number(G_nx) + 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) @@ -78,6 +81,7 @@ def test_k_core_all_zero_core(): 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)] @@ -94,4 +98,4 @@ 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 \ No newline at end of file + assert max(result) <= 2 diff --git a/easygraph/functions/drawing/tests/test_geometry.py b/easygraph/functions/drawing/tests/test_geometry.py index 60c0ce79..4b155dc3 100644 --- a/easygraph/functions/drawing/tests/test_geometry.py +++ b/easygraph/functions/drawing/tests/test_geometry.py @@ -1,14 +1,14 @@ -import unittest import math +import unittest + import numpy as np -from easygraph.functions.drawing.geometry import ( - radian_from_atan, - vlen, - common_tangent_radian, - polar_position, - rad_2_deg, -) +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): @@ -53,7 +53,7 @@ def test_common_tangent_radian_touching(self): def test_common_tangent_radian_invalid(self): with self.assertRaises(ValueError): - common_tangent_radian(5, 1, 2) + common_tangent_radian(5, 1, 2) def test_polar_position_origin(self): pos = polar_position(0, 0, np.array([5, 5])) @@ -73,5 +73,6 @@ def test_rad_2_deg(self): 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/tests/test_deepwalk.py b/easygraph/functions/graph_embedding/tests/test_deepwalk.py index 4eceae4a..29770164 100644 --- a/easygraph/functions/graph_embedding/tests/test_deepwalk.py +++ b/easygraph/functions/graph_embedding/tests/test_deepwalk.py @@ -24,7 +24,6 @@ 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, @@ -96,5 +95,7 @@ def test_deepwalk_walk_length_zero(self): 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 09e8555f..80bf7fb5 100644 --- a/easygraph/functions/graph_embedding/tests/test_line.py +++ b/easygraph/functions/graph_embedding/tests/test_line.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg import numpy as np @@ -10,25 +11,33 @@ def setUp(self): 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) + 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) def test_output_as_matrix(self): - model = eg.functions.graph_embedding.LINE(dimension=8, walk_length=5, walk_num=3, order=1) + 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) + 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) + 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) @@ -36,25 +45,33 @@ def test_output_with_order_3_combination(self): 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) + 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) + 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) + 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) + 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 e4dccedf..0b8bd781 100644 --- a/easygraph/functions/graph_embedding/tests/test_nobe.py +++ b/easygraph/functions/graph_embedding/tests/test_nobe.py @@ -31,6 +31,7 @@ def test_NOBE_GA(self): print(i) """ 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) @@ -50,5 +51,7 @@ 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 d1af7683..f579295b 100644 --- a/easygraph/functions/graph_embedding/tests/test_node2vec.py +++ b/easygraph/functions/graph_embedding/tests/test_node2vec.py @@ -28,6 +28,7 @@ def test_NOBE(self): 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) @@ -42,7 +43,6 @@ 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) @@ -53,5 +53,6 @@ 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 f503d145..1464cd53 100644 --- a/easygraph/functions/graph_embedding/tests/test_sdne.py +++ b/easygraph/functions/graph_embedding/tests/test_sdne.py @@ -1,7 +1,8 @@ import unittest -import torch -import numpy as np + import easygraph as eg +import numpy as np +import torch class Test_Sdne(unittest.TestCase): @@ -103,4 +104,4 @@ def test_training_on_empty_graph(self): beta=5.0, ) with self.assertRaises(ValueError): - model.train(model=model, epochs=5, device=self.device) \ No newline at end of file + 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 7f770eaf..fca17174 100644 --- a/easygraph/functions/graph_generator/tests/test_Random_Network.py +++ b/easygraph/functions/graph_generator/tests/test_Random_Network.py @@ -27,6 +27,7 @@ def test_erdos_renyi_M_max_edges(self): 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) @@ -57,5 +58,6 @@ def test_graph_Gnm_invalid_inputs(self): 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 ccf1208a..36868614 100644 --- a/easygraph/functions/graph_generator/tests/test_classic.py +++ b/easygraph/functions/graph_generator/tests/test_classic.py @@ -28,8 +28,8 @@ def test_empty_graph_with_n(self): 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'}) + 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): @@ -47,15 +47,13 @@ def test_path_graph_basic(self): 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']) + 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')} + 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) @@ -68,13 +66,12 @@ def test_complete_graph_directed(self): 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'}) + 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')} + 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) @@ -84,5 +81,7 @@ 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/tests/test_cycle_ratio.py b/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py index 8d28b6a4..90e39d78 100644 --- a/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py +++ b/easygraph/functions/hypergraph/centrality/tests/test_cycle_ratio.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg diff --git a/easygraph/functions/hypergraph/centrality/tests/test_degree.py b/easygraph/functions/hypergraph/centrality/tests/test_degree.py index 76ee04de..85aaaef3 100644 --- a/easygraph/functions/hypergraph/centrality/tests/test_degree.py +++ b/easygraph/functions/hypergraph/centrality/tests/test_degree.py @@ -1,13 +1,11 @@ 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)] - ) + 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) @@ -15,7 +13,7 @@ def test_basic_degree_centrality(self): def test_empty_hypergraph(self): hg = eg.Hypergraph(num_v=1, e_list=[]) result = eg.hyepergraph_degree_centrality(hg) - self.assertEqual(result, {0:0}) + self.assertEqual(result, {0: 0}) def test_single_edge(self): hg = eg.Hypergraph(num_v=3, e_list=[(0, 1, 2)]) diff --git a/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py b/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py index 0133e93b..69b56120 100644 --- a/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py +++ b/easygraph/functions/hypergraph/centrality/tests/test_hypercoreness.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg diff --git a/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py b/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py index 65db4354..a8c3450c 100644 --- a/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py +++ b/easygraph/functions/hypergraph/centrality/tests/test_s_centrality.py @@ -1,7 +1,9 @@ import unittest + import easygraph as eg import numpy as np + class TestHypergraphSCentrality(unittest.TestCase): def setUp(self): # Simple test hypergraph @@ -33,5 +35,6 @@ 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 index 36fc180f..d674ca09 100644 --- a/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py +++ b/easygraph/functions/hypergraph/centrality/tests/test_vector_centrality.py @@ -1,10 +1,12 @@ import unittest -import numpy as np + import easygraph as eg +import numpy as np + from easygraph.exception import EasyGraphError -class TestVectorCentrality(unittest.TestCase): +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) @@ -12,7 +14,6 @@ def test_single_edge(self): 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) @@ -31,11 +32,12 @@ def test_non_consecutive_node_ids(self): result = eg.vector_centrality(hg) self.assertEqual(len(result), 5) for val in result.values(): - self.assertEqual(len(val), 2) + 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 cbad21c2..1cff7b9e 100644 --- a/easygraph/functions/hypergraph/centrality/vector_centrality.py +++ b/easygraph/functions/hypergraph/centrality/vector_centrality.py @@ -65,6 +65,7 @@ def vector_centrality(H): return vc + def eigenvector_centrality(G, max_iter=100, tol=1.0e-6): from collections import defaultdict @@ -79,7 +80,7 @@ def eigenvector_centrality(G, max_iter=100, tol=1.0e-6): x_new[v] += x[nbr] # Normalize - norm = sum(v ** 2 for v in x_new.values()) ** 0.5 + 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()} @@ -87,4 +88,4 @@ def eigenvector_centrality(G, max_iter=100, tol=1.0e-6): # Check convergence if all(abs(x_new[v] - x[v]) < tol for v in nodes): return x_new - x = x_new \ No newline at end of file + 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 2c43c5f5..3da8b2a5 100644 --- a/easygraph/functions/hypergraph/null_model/tests/test_classic.py +++ b/easygraph/functions/hypergraph/null_model/tests/test_classic.py @@ -1,5 +1,6 @@ import easygraph as eg import pytest + from easygraph.utils.exception import EasyGraphError @@ -47,8 +48,9 @@ def test_uniform_hypergraph(self): H3 = eg.uniform_HPPM(10, 6, 0.9, 10, 0.9) print("H3:", H3) -class TestHypergraphGenerators: + +class TestHypergraphGenerators: def test_empty_hypergraph_default(self): hg = eg.empty_hypergraph() assert hg.num_v == 1 @@ -70,17 +72,14 @@ def test_complete_hypergraph_n_1_excludes_singletons(self): 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) + 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) \ No newline at end of file + 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 index d563e478..66410370 100644 --- a/easygraph/functions/hypergraph/null_model/tests/test_lattice.py +++ b/easygraph/functions/hypergraph/null_model/tests/test_lattice.py @@ -1,7 +1,9 @@ -import pytest 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) @@ -40,7 +42,7 @@ 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 diff --git a/easygraph/functions/hypergraph/null_model/tests/test_simple.py b/easygraph/functions/hypergraph/null_model/tests/test_simple.py index 46a06659..eae3f2bc 100644 --- a/easygraph/functions/hypergraph/null_model/tests/test_simple.py +++ b/easygraph/functions/hypergraph/null_model/tests/test_simple.py @@ -1,9 +1,10 @@ -import pytest -import easygraph as eg from itertools import combinations -class TestStarCliqueHypergraph: +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) @@ -47,9 +48,7 @@ def test_clique_hyperedges_match_combinations(self): 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) + tuple(sorted(e)) for e in H.e[0] if all(node in clique_nodes for node in e) } assert expected.issubset(actual) diff --git a/easygraph/functions/hypergraph/tests/test_assortativity.py b/easygraph/functions/hypergraph/tests/test_assortativity.py index df419079..452aec9a 100644 --- a/easygraph/functions/hypergraph/tests/test_assortativity.py +++ b/easygraph/functions/hypergraph/tests/test_assortativity.py @@ -2,11 +2,12 @@ import unittest from itertools import combinations -from easygraph.utils.exception import EasyGraphError import easygraph as eg import numpy as np +from easygraph.utils.exception import EasyGraphError + class test_assortativity(unittest.TestCase): def setUp(self): @@ -96,5 +97,7 @@ def test_degree_assortativity_raises_on_singleton(self): 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 285051ba..a0318e60 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_clustering.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphError @@ -25,6 +26,7 @@ def test_hypergraph_two_node_clustering_coefficient(self): for i in self.hg: 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)] @@ -82,5 +84,6 @@ def test_nan_safety_in_two_node_coefficient(self): 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 4e44f2f6..2188ddb2 100644 --- a/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py +++ b/easygraph/functions/hypergraph/tests/test_hypergraph_operation.py @@ -2,6 +2,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphError @@ -28,14 +29,18 @@ def test_basic_density(self): 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) + 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) + 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=[]) @@ -47,8 +52,12 @@ def test_single_node_single_edge(self): def test_density_max_possible_edges(self): n = 4 - from itertools import chain, combinations - powerset = list(chain.from_iterable(combinations(range(n), r) for r in range(1, n + 1))) + 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) @@ -58,5 +67,6 @@ def test_density_zero_division_guard(self): result = eg.hypergraph_density(hg, ignore_singletons=True) self.assertEqual(result, 0.0) + if __name__ == "__main__": unittest.main() diff --git a/easygraph/functions/path/bridges.py b/easygraph/functions/path/bridges.py index 8e4fbe88..e38a1690 100644 --- a/easygraph/functions/path/bridges.py +++ b/easygraph/functions/path/bridges.py @@ -3,6 +3,8 @@ import easygraph as eg from easygraph.utils.decorators import * + + __all__ = ["bridges", "has_bridges"] @@ -55,8 +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.") + 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: 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 caa00171..00867f78 100644 --- a/easygraph/functions/path/tests/test_average_shortest_path_length.py +++ b/easygraph/functions/path/tests/test_average_shortest_path_length.py @@ -1,7 +1,10 @@ import unittest + import easygraph as eg + from easygraph import average_shortest_path_length -from easygraph.utils.exception import EasyGraphError, EasyGraphPointlessConcept +from easygraph.utils.exception import EasyGraphError +from easygraph.utils.exception import EasyGraphPointlessConcept class TestAverageShortestPathLength(unittest.TestCase): diff --git a/easygraph/functions/path/tests/test_bridges.py b/easygraph/functions/path/tests/test_bridges.py index 950923eb..29a2edb1 100644 --- a/easygraph/functions/path/tests/test_bridges.py +++ b/easygraph/functions/path/tests/test_bridges.py @@ -1,6 +1,7 @@ import unittest import easygraph as eg + from easygraph.utils.exception import EasyGraphNotImplemented @@ -90,7 +91,7 @@ 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)) @@ -146,9 +147,10 @@ def test_multigraph_exception(self): 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} - ]) + 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)) diff --git a/easygraph/functions/path/tests/test_mst.py b/easygraph/functions/path/tests/test_mst.py index b83e7ffe..39271f54 100644 --- a/easygraph/functions/path/tests/test_mst.py +++ b/easygraph/functions/path/tests/test_mst.py @@ -78,11 +78,7 @@ def setUp(self): ) self.nan_graph = eg.Graph() self.nan_graph.add_edges( - [(0, 1), (1, 2)], - edges_attr=[ - {"weight": float('nan')}, - {"weight": 1} - ] + [(0, 1), (1, 2)], edges_attr=[{"weight": float("nan")}, {"weight": 1}] ) self.no_weight_graph = eg.Graph() @@ -91,41 +87,24 @@ def setUp(self): 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} - ] + 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} - ] + 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} - ] + [(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} - ] + edges_attr=[{"weight": 1}, {"weight": 2}, {"weight": 3}, {"weight": 4}], ) def helper(self, g: eg.Graph, func): @@ -191,5 +170,7 @@ def test_maximum_vs_minimum_edges(self): 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 a4674391..97ffac38 100644 --- a/easygraph/functions/path/tests/test_path.py +++ b/easygraph/functions/path/tests/test_path.py @@ -131,6 +131,7 @@ def test_multi_source_dijkstra(self): except ValueError as e: print(e) print() + def test_dijkstra_negative_weights_raises(self): with self.assertRaises(ValueError): eg.Dijkstra(self.g4, node=0) @@ -156,7 +157,9 @@ def test_prim_disconnected_graph(self): 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 + self.assertLess( + count, len(g.nodes) - 1 + ) # not enough edges to connect all nodes def test_kruskal_disconnected_graph(self): g = eg.Graph() diff --git a/easygraph/functions/structural_holes/HAM.py b/easygraph/functions/structural_holes/HAM.py index d9b44033..8e30cdb0 100644 --- a/easygraph/functions/structural_holes/HAM.py +++ b/easygraph/functions/structural_holes/HAM.py @@ -198,7 +198,9 @@ def get_structural_holes_HAM(G, k, c, ground_truth_labels): 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.") + 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/NOBE.py b/easygraph/functions/structural_holes/NOBE.py index ef200acf..62f5f3f8 100644 --- a/easygraph/functions/structural_holes/NOBE.py +++ b/easygraph/functions/structural_holes/NOBE.py @@ -108,7 +108,7 @@ def NOBE_GA_SH(G, K, topk): 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.") + 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 0aaaa422..ec4af9e7 100644 --- a/easygraph/functions/structural_holes/__init__.py +++ b/easygraph/functions/structural_holes/__init__.py @@ -3,7 +3,7 @@ from .HAM import * from .HIS import * from .ICC import * +from .maxBlock import * from .MaxD import * from .metrics import * from .NOBE import * -from .maxBlock import * \ No newline at end of file diff --git a/easygraph/functions/structural_holes/tests/test_AP_Greedy.py b/easygraph/functions/structural_holes/tests/test_AP_Greedy.py index 13fc67ea..38524f31 100644 --- a/easygraph/functions/structural_holes/tests/test_AP_Greedy.py +++ b/easygraph/functions/structural_holes/tests/test_AP_Greedy.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg @@ -73,7 +74,7 @@ def test_ap_greedy_unweighted_vs_weighted(self): 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) diff --git a/easygraph/functions/structural_holes/tests/test_HAM.py b/easygraph/functions/structural_holes/tests/test_HAM.py index a087f7d0..3757a78e 100644 --- a/easygraph/functions/structural_holes/tests/test_HAM.py +++ b/easygraph/functions/structural_holes/tests/test_HAM.py @@ -1,16 +1,26 @@ 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.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): @@ -23,28 +33,39 @@ def test_output_structure(self): 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.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) + 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) + 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) + 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) + 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 index 4e39d8e8..9464cff0 100644 --- a/easygraph/functions/structural_holes/tests/test_HIS.py +++ b/easygraph/functions/structural_holes/tests/test_HIS.py @@ -1,17 +1,24 @@ 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.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): @@ -67,7 +74,7 @@ def test_weighted_graph(self): 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') + 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): diff --git a/easygraph/functions/structural_holes/tests/test_ICC.py b/easygraph/functions/structural_holes/tests/test_ICC.py index be578c86..d8efd0e5 100644 --- a/easygraph/functions/structural_holes/tests/test_ICC.py +++ b/easygraph/functions/structural_holes/tests/test_ICC.py @@ -1,15 +1,16 @@ import unittest + import easygraph as eg -from easygraph.functions.structural_holes.ICC import ICC, BICC, AP_BICC + +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) - ]) + 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) diff --git a/easygraph/functions/structural_holes/tests/test_MaxD.py b/easygraph/functions/structural_holes/tests/test_MaxD.py index 789e7422..fe7adf30 100644 --- a/easygraph/functions/structural_holes/tests/test_MaxD.py +++ b/easygraph/functions/structural_holes/tests/test_MaxD.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg @@ -6,11 +7,15 @@ 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.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): @@ -35,7 +40,9 @@ def test_disconnected_communities(self): 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])]) + 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): @@ -47,7 +54,9 @@ def test_disconnected_graph(self): 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])]) + result = eg.get_structural_holes_MaxD( + self.G, k=2, C=[frozenset([1, 2, 3]), frozenset([3, 4, 5])] + ) self.assertEqual(len(result), 2) diff --git a/easygraph/functions/structural_holes/tests/test_NOBE.py b/easygraph/functions/structural_holes/tests/test_NOBE.py index 0e982ddd..a350f89e 100644 --- a/easygraph/functions/structural_holes/tests/test_NOBE.py +++ b/easygraph/functions/structural_holes/tests/test_NOBE.py @@ -1,4 +1,5 @@ import unittest + import easygraph as eg diff --git a/easygraph/functions/structural_holes/tests/test_SHII_metric.py b/easygraph/functions/structural_holes/tests/test_SHII_metric.py index 7beb1fda..1f7d8699 100644 --- a/easygraph/functions/structural_holes/tests/test_SHII_metric.py +++ b/easygraph/functions/structural_holes/tests/test_SHII_metric.py @@ -1,9 +1,10 @@ import unittest + import easygraph as eg import numpy as np -class TestStructuralHoleInfluenceIndex(unittest.TestCase): +class TestStructuralHoleInfluenceIndex(unittest.TestCase): def setUp(self): self.G = eg.datasets.get_graph_karateclub() self.Com = [ @@ -13,16 +14,22 @@ def setUp(self): 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) + 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) + 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) + 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): @@ -35,15 +42,20 @@ def test_seed_not_in_community(self): def test_invalid_model(self): with self.assertRaises(Exception): - eg.structural_hole_influence_index(self.G, self.valid_seeds, self.Com, "XYZ") + 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) + 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 index 9880a056..b1d18ac3 100644 --- a/easygraph/functions/structural_holes/tests/test_evaluation.py +++ b/easygraph/functions/structural_holes/tests/test_evaluation.py @@ -1,25 +1,26 @@ -import unittest import math +import unittest + import easygraph as eg -from easygraph.functions.structural_holes import ( - effective_size, - efficiency, - constraint, - hierarchy, -) +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_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): diff --git a/easygraph/functions/structural_holes/tests/test_maxBlock.py b/easygraph/functions/structural_holes/tests/test_maxBlock.py index ee8a66e7..d503a166 100644 --- a/easygraph/functions/structural_holes/tests/test_maxBlock.py +++ b/easygraph/functions/structural_holes/tests/test_maxBlock.py @@ -1,14 +1,22 @@ +import random import unittest + import easygraph as eg -import random + 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 - ]) + 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 @@ -27,7 +35,15 @@ def test_maxBlockFast_disconnected_graph(self): 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) + 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): @@ -45,5 +61,6 @@ 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 index f54239ee..de5417ad 100644 --- a/easygraph/functions/structural_holes/tests/test_metrics.py +++ b/easygraph/functions/structural_holes/tests/test_metrics.py @@ -1,10 +1,10 @@ import unittest + import easygraph as eg -from easygraph.functions.structural_holes.metrics import ( - sum_of_shortest_paths, - nodes_of_max_cc_without_shs, - structural_hole_influence_index, -) + +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): @@ -35,8 +35,14 @@ def test_nodes_of_max_cc_without_all_nodes(self): 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.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)) @@ -44,15 +50,28 @@ def test_structural_hole_influence_index_IC(self): 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.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.G, + self.shs, + self.communities, + model="LT", + variant=True, + Directed=False, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, ) self.assertIsInstance(result, dict) @@ -64,8 +83,14 @@ def test_structural_hole_influence_index_empty_shs(self): 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.G, + self.shs, + self.communities, + model="IC", + Directed=True, + seedRatio=0.1, + randSeedIter=2, + countIterations=5, ) self.assertIsInstance(result, dict) diff --git a/easygraph/functions/structural_holes/tests/test_weakTie.py b/easygraph/functions/structural_holes/tests/test_weakTie.py index 9ec07165..6a3861fc 100644 --- a/easygraph/functions/structural_holes/tests/test_weakTie.py +++ b/easygraph/functions/structural_holes/tests/test_weakTie.py @@ -1,19 +1,41 @@ import unittest + import easygraph as eg -from easygraph.functions.structural_holes.weakTie import weakTie, weakTieLocal +from easygraph.functions.structural_holes.weakTie import weakTie +from easygraph.functions.structural_holes.weakTie import weakTieLocal -class TestWeakTieFunctions(unittest.TestCase): +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.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 diff --git a/easygraph/functions/tests/test_isolate.py b/easygraph/functions/tests/test_isolate.py index ea32e977..89a88b95 100644 --- a/easygraph/functions/tests/test_isolate.py +++ b/easygraph/functions/tests/test_isolate.py @@ -3,6 +3,7 @@ import easygraph as eg import pytest + def test_is_isolate(): G = eg.Graph() G.add_edge(0, 1) @@ -25,6 +26,7 @@ def test_number_of_isolates(): 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)) == [] @@ -88,4 +90,4 @@ def test_mixed_isolates_and_edges(): for node in [0, 1, 2]: assert not eg.is_isolate(G, node) for node in [3, 4]: - assert eg.is_isolate(G, node) \ No newline at end of file + assert eg.is_isolate(G, node) diff --git a/easygraph/tests/test_convert.py b/easygraph/tests/test_convert.py index a156715e..a4bc021b 100644 --- a/easygraph/tests/test_convert.py +++ b/easygraph/tests/test_convert.py @@ -98,48 +98,56 @@ def test_from_scipy(self): 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) + 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] - }) + 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 + class NotGraph: + pass + with pytest.raises(eg.EasyGraphError): eg.to_easygraph_graph(NotGraph())