diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c86af76e..1c48b356 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: stable + rev: 19.10b0 hooks: - id: black - language_version: python3.7 + language_version: python3.8 diff --git a/dev-environment.yml b/dev-environment.yml index 2db24ce9..c3e823a0 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -32,3 +32,5 @@ dependencies: - conda-forge::grblas - conda-forge::python-louvain - conda-forge::websockets + - katanagraph/label/dev::katana-cpp + - katanagraph/label/dev::katana-python diff --git a/metagraph/plugins/__init__.py b/metagraph/plugins/__init__.py index 2e1faf36..74b1dbb1 100644 --- a/metagraph/plugins/__init__.py +++ b/metagraph/plugins/__init__.py @@ -48,6 +48,13 @@ except ImportError: # pragma: no cover has_grblas = False +try: + import katanagraph as _ + + has_katanagraph = True +except ImportError: # pragma: no cover + has_katanagraph = False + try: import numba as _ @@ -66,11 +73,12 @@ def find_plugins(): - from . import core, graphblas, networkx, numpy, pandas, python, scipy + from . import core, graphblas, katanagraph, networkx, numpy, pandas, python, scipy # Default Plugins registry.register_from_modules(core) registry.register_from_modules(graphblas, name="core_graphblas") + registry.register_from_modules(katanagraph, name="core_katanagraph") registry.register_from_modules(networkx, name="core_networkx") registry.register_from_modules(numpy, name="core_numpy") registry.register_from_modules(pandas, name="core_pandas") diff --git a/metagraph/plugins/katanagraph/__init__.py b/metagraph/plugins/katanagraph/__init__.py new file mode 100644 index 00000000..d104c1fb --- /dev/null +++ b/metagraph/plugins/katanagraph/__init__.py @@ -0,0 +1 @@ +from . import algorithms, translators, types diff --git a/metagraph/plugins/katanagraph/algorithms.py b/metagraph/plugins/katanagraph/algorithms.py new file mode 100644 index 00000000..020cfb41 --- /dev/null +++ b/metagraph/plugins/katanagraph/algorithms.py @@ -0,0 +1,117 @@ +from typing import Tuple + +import numpy as np +from metagraph import NodeID, abstract_algorithm, concrete_algorithm +from metagraph.plugins.core.types import Graph, Vector +from metagraph.plugins.networkx.types import NetworkXGraph +from metagraph.plugins.numpy.types import NumpyNodeMap, NumpyVectorType + +from katana.local.analytics import bfs, jaccard, local_clustering_coefficient + +from .types import KatanaGraph + + +def has_node_prop(kg, node_prop_name): + nschema = kg.loaded_node_schema() + for i in range(len(nschema)): + if nschema[i].name == node_prop_name: + return True + return False + + +# breadth-first search, +@concrete_algorithm("traversal.bfs_iter") +def kg_bfs_iter( + graph: KatanaGraph, source_node: NodeID, depth_limit: int +) -> NumpyVectorType: + """ + .. py:function:: metagraph.algos.traversal.bfs_iter(graph, source_node, depth_limit) + + Use BFS to traverse a graph given a source node and BFS depth limit (implemented by a Katana Graph API) + + :param KatanaGraph graph: The origianl graph to traverse + :param NodeID source_node: The starting node for BFS + :param int depth: The BFS depth + :return: the BFS traversal result in order + :rtype: NumpyVectorType + """ + g = graph.value + edges = [ + (src, dest) + for src in g + for dest in [g.get_edge_dest(e) for e in g.edge_ids(src)] + ] + edge_weights = g.get_edge_property(graph.edge_weight_prop_name).to_pandas() + bfs_prop_name = "bfs_prop_start_from_" + str(source_node) + depth_limit_internal = ( + 2 ** 30 - 1 if depth_limit == -1 else depth_limit + ) # return all the reachable nodes for the default value of depth_limit (-1) + start_node = source_node + if not has_node_prop(graph.value, bfs_prop_name): + bfs(graph.value, start_node, bfs_prop_name) + bfs_list_1st = graph.value.get_node_property(bfs_prop_name).to_numpy() + pg_bfs_list = ( + graph.value.get_node_property(bfs_prop_name).to_pandas().values.tolist() + ) + new_list = [ + [i, pg_bfs_list[i]] + for i in range(len(pg_bfs_list)) + if pg_bfs_list[i] < depth_limit_internal + ] + sorted_list = sorted(new_list, key=lambda each: (each[1], each[0])) + bfs_arr = np.array([each[0] for each in sorted_list]) + return bfs_arr + + +# TODO(pengfei): +# single-source shortest path +# connected components +# PageRank +# betweenness centrality +# triangle counting +# Louvain community detection +# subgraph extraction +# community detection using label propagation\ + + +@abstract_algorithm("traversal.jaccard") +def jaccard_similarity( + graph: Graph( + is_directed=False, + edge_type="map", + edge_dtype={"int", "float"}, + edge_has_negative_weights=False, + ), + compare_node: NodeID, +) -> Vector: + pass + + +@concrete_algorithm("traversal.jaccard") +def jaccard_similarity_kg(graph: KatanaGraph, compare_node: NodeID) -> NumpyVectorType: + jaccard_prop_name = "jaccard_prop_with_" + str(compare_node) + if not has_node_prop(graph.value, jaccard_prop_name): + jaccard(graph.value, compare_node, jaccard_prop_name) + jaccard_similarities = graph.value.get_node_property(jaccard_prop_name).to_numpy() + return jaccard_similarities + + +@abstract_algorithm("clustering.local_clustering_coefficient") +def local_clustering( + graph: Graph( + is_directed=False, + edge_type="map", + edge_dtype={"int", "float"}, + edge_has_negative_weights=False, + ), + prop_name: str = "output", +) -> Vector: + pass + + +@concrete_algorithm("clustering.local_clustering_coefficient") +def local_clustering_kg(graph: KatanaGraph, prop_name: str) -> NumpyVectorType: + if not has_node_prop(graph.value, prop_name): + local_clustering_coefficient(graph.value, prop_name) + out = graph.value.get_node_property(prop_name) + return out.to_pandas().values diff --git a/metagraph/plugins/katanagraph/translators.py b/metagraph/plugins/katanagraph/translators.py new file mode 100644 index 00000000..9155918d --- /dev/null +++ b/metagraph/plugins/katanagraph/translators.py @@ -0,0 +1,177 @@ +from collections import OrderedDict + +import metagraph as mg +import networkx as nx +import numpy as np +import pyarrow +import katana.local + +from metagraph import translator +from metagraph.plugins.networkx.types import NetworkXGraph +from scipy.sparse import csr_matrix + +from katana.local.import_data import from_csr + +from .types import KatanaGraph + + +@translator +def networkx_to_katanagraph(x: NetworkXGraph, **props) -> KatanaGraph: + nlist = sorted(list(x.value.nodes(data=True)), key=lambda each: each[0]) + ranks = np.arange(0, len(nlist)) + nodes = [each[0] for each in nlist] + mapping = dict(zip(nodes, ranks)) + # relabel Node IDs without changing the original graph + xval_map = nx.relabel_nodes(x.value, mapping) + aprops = NetworkXGraph.Type.compute_abstract_properties( + x, + { + "node_dtype", + "node_type", + "edge_dtype", + "edge_type", + "edge_has_negative_weights", + "is_directed", + }, + ) + is_weighted = aprops["edge_type"] == "map" + # get the edge list directly from the NetworkX Graph + elist_raw = list(xval_map.edges(data=True)) + # sort the eddge list and node list + if aprops["is_directed"]: + elist = sorted(elist_raw, key=lambda each: (each[0], each[1])) + else: + inv_elist = [ + (each[1], each[0], each[2]) for each in elist_raw if each[0] != each[1] + ] + elist = sorted(elist_raw + inv_elist, key=lambda each: (each[0], each[1])) + # build the CSR format from the edge list (weight, (src, dst)) + row = np.array([each_edge[0] for each_edge in elist]) + col = np.array([each_edge[1] for each_edge in elist]) + if is_weighted: + data = np.array([each_edge[2]["weight"] for each_edge in elist]) + else: + # data = np.array([None for each_edge in elist]) + data = np.array([0 for each_edge in elist]) + csr = csr_matrix((data, (row, col)), shape=(len(nlist), len(nlist))) + # call the katana api to build a Graph (unweighted) from the CSR format + # noting that the first 0 in csr.indptr is excluded + katana.local.initialize() + pg = from_csr(csr.indptr[1:], csr.indices) + # add the edge weight as a new property + t = pyarrow.table(dict(edge_value_from_translator=data)) + pg.add_edge_property(t) + node_list = [nid for nid in nodes] + node_rmap = pyarrow.table(dict(node_id_reverse_map=node_list)) + pg.add_node_property(node_rmap) + node_id_map_prop_name = "node_id_reverse_map" + + node_attributes = nx.get_node_attributes(x.value, "weight") + node_weight_prop_name = None + if node_attributes: + weights = [node_attributes[node] for node in node_list] + node_weight_prop = pyarrow.table(dict(node_value_from_translator=weights)) + node_weight_prop_name = "node_value_from_translator" + pg.add_node_property(node_weight_prop) + + # use the metagraph's Graph warpper to wrap the katana.local.Graph + return KatanaGraph( + pg_graph=pg, + is_weighted=is_weighted, + edge_weight_prop_name="edge_value_from_translator", + node_weight_prop_name=node_weight_prop_name, + node_id_map_prop_name=node_id_map_prop_name, + is_directed=aprops["is_directed"], + node_weight_index=0, + node_dtype=aprops["node_dtype"], + edge_dtype=aprops["edge_dtype"], + node_type=aprops["node_type"], + edge_type=aprops["edge_type"], + has_neg_weight=aprops["edge_has_negative_weights"], + ) + + +@translator +def katanagraph_to_networkx(x: KatanaGraph, **props) -> NetworkXGraph: + pg = x.value + node_list = [src for src in pg] + # dest_list = [ + # dest for src in pg for dest in [pg.get_edge_dest(e) for e in pg.edge_ids(src)] + # ] + # for src in pg: + # print("src:", src, "id:", pg.edge_ids(src)) + # if pg.edge_ids(src) == range(0, 0): + # if src not in dest_list: + # raise ValueError("NetworkX does not support graph with isolated nodes") + edge_dict_count = { + (src, dest): 0 + for src in pg + for dest in [pg.get_edge_dest(e) for e in pg.edge_ids(src)] + } + for src in pg: + for dest in [pg.get_edge_dest(e) for e in pg.edge_ids(src)]: + edge_dict_count[(src, dest)] += 1 + if edge_dict_count[(src, dest)] > 1: + raise ValueError( + "NetworkX does not support graph with duplicated edges" + ) + elist = [] + edge_weights = pg.get_edge_property(x.edge_weight_prop_name).to_pandas() + if isinstance(edge_weights[0], np.int64): + elist = [ + (nid, pg.get_edge_dest(j), int(edge_weights[j])) + for nid in pg + for j in pg.edge_ids(nid) + ] + elif isinstance(edge_weights[0], pyarrow.lib.Int64Scalar): + elist = [ + (nid, pg.get_edge_dest(j), edge_weights[j].as_py()) + for nid in pg + for j in pg.edge_ids(nid) + ] + elif isinstance(edge_weights[0], np.float64): + elist = [ + (nid, pg.get_edge_dest(j), float(edge_weights[j])) + for nid in pg + for j in pg.edge_ids(nid) + ] + elif isinstance(edge_weights[0], np.bool_): + elist = [ + (nid, pg.get_edge_dest(j), bool(edge_weights[j])) + for nid in pg + for j in pg.edge_ids(nid) + ] + elist = list(OrderedDict.fromkeys(elist)) + if x.is_directed: + graph = nx.DiGraph() + else: + graph = nx.Graph() + # add node list first for the same order as weights + graph.add_weighted_edges_from(elist) + graph.add_nodes_from(node_list) + + # remap Node IDs if needed + if x.node_id_map_prop_name: + nodeid_map = pg.get_node_property(x.node_id_map_prop_name).to_pandas() + ranks = np.arange(0, len(nodeid_map)) + mapping = dict(zip(ranks, nodeid_map)) + graph = nx.relabel_nodes(graph, mapping) + + # retrieve node weights and set the graph + if x.node_weight_prop_name: + nodes = graph.nodes() + nlist = [] + node_weights = pg.get_node_property(x.node_weight_prop_name).to_pandas() + if isinstance(node_weights[0], np.int64): + nlist = [int(wgt) for wgt in node_weights] + elif isinstance(node_weights[0], pyarrow.lib.Int64Scalar): + nlist = [wgt.as_py() for wgt in node_weights] + elif isinstance(node_weights[0], np.float64): + nlist = [float(wgt) for wgt in node_weights] + elif isinstance(node_weights[0], np.bool_): + nlist = [bool(wgt) for wgt in node_weights] + nx.set_node_attributes( + graph, {node: wgt for node, wgt in zip(nodeid_map, nlist)}, name="weight" + ) + + return mg.wrappers.Graph.NetworkXGraph(graph) diff --git a/metagraph/plugins/katanagraph/types.py b/metagraph/plugins/katanagraph/types.py new file mode 100644 index 00000000..455e7209 --- /dev/null +++ b/metagraph/plugins/katanagraph/types.py @@ -0,0 +1,102 @@ +import copy +import math +from typing import Any, Dict, List, Set + +import numpy as np +from metagraph.plugins.core.types import Graph +from metagraph.plugins.core.wrappers import GraphWrapper + +import katana.local + + +class KatanaGraph(GraphWrapper, abstract=Graph): + def __init__( + self, + pg_graph, + is_weighted=True, + edge_weight_prop_name="value", + node_weight_prop_name=None, + node_id_map_prop_name=None, + is_directed=True, + node_weight_index=None, + node_dtype=None, + edge_dtype="int", + node_type=None, + edge_type=None, + has_neg_weight=False, + ): + super().__init__() + self._assert_instance(pg_graph, katana.local.Graph) + self.value = pg_graph + self.is_weighted = is_weighted + self.edge_weight_prop_name = edge_weight_prop_name + self.node_weight_prop_name = node_weight_prop_name + self.node_id_map_prop_name = node_id_map_prop_name + self.is_directed = is_directed + self.node_weight_index = node_weight_index + self.node_dtype = node_dtype + self.edge_dtype = edge_dtype + self.node_type = node_type + self.edge_type = edge_type + self.has_neg_weight = has_neg_weight + + def copy(self): + return KatanaGraph( + copy.deepcopy(self.value), self.is_weighted, self.is_directed + ) + + class TypeMixin: + @classmethod + def _compute_abstract_properties( + cls, obj, props: Set[str], known_props: Dict[str, Any] + ) -> Dict[str, Any]: + ret = known_props.copy() + # fast props + for prop in { + "is_directed", + "node_type", + "node_dtype", + "edge_type", + "edge_dtype", + "edge_has_negative_weights", + } - ret.keys(): + if prop == "is_directed": + ret[prop] = obj.is_directed + if prop == "node_type": + if obj.node_type is None: + ret[prop] = "set" if obj.node_weight_index is None else "map" + else: + ret[prop] = obj.node_type + if prop == "node_dtype": + ret[prop] = ( + None if obj.node_weight_index is None else obj.node_dtype + ) + if prop == "edge_type": + ret[prop] = "map" if obj.is_weighted else "set" + if prop == "edge_dtype": + ret[prop] = obj.edge_dtype if obj.is_weighted else None + if prop == "edge_has_negative_weights": + ret[ + prop + ] = ( + obj.has_neg_weight + ) # TODO(pengfei): cover the neg-weight case, and add neg-weight test cases. + return ret + + @classmethod + def assert_equal( + cls, + obj1, + obj2, + aprops1, + aprops2, + cprops1, + cprops2, + *, + rel_tal=1e-9, + abs_tol=0.0, + ): + assert aprops1 == aprops2, f"proterty mismatch: {aprops1} != {aprops2}" + pg1 = obj1.value + pg2 = obj2.value + assert pg1 == pg2, f"the two graphs does not match" diff --git a/metagraph/plugins/networkx/types.py b/metagraph/plugins/networkx/types.py index 91f1a4bd..47f206e4 100644 --- a/metagraph/plugins/networkx/types.py +++ b/metagraph/plugins/networkx/types.py @@ -85,7 +85,7 @@ def _compute_abstract_properties( except KeyError: edge_values = None break - if edge_values: + if edge_values and edge_values != {0}: ret["edge_type"] = "map" if ( "edge_dtype" in slow_props diff --git a/metagraph/tests/plugins/katanagraph/conftest.py b/metagraph/tests/plugins/katanagraph/conftest.py new file mode 100644 index 00000000..7f68dca2 --- /dev/null +++ b/metagraph/tests/plugins/katanagraph/conftest.py @@ -0,0 +1,204 @@ +import io +import metagraph as mg +import numpy as np +import pandas as pd +import pyarrow +import pytest +from scipy.sparse import csr_matrix + +import katana.local +from katana.example_data import get_rdg_dataset +from katana.local import Graph +from katana.local.import_data import from_csr + +katana.local.initialize() + +data_8_12 = """ +Source,Destination,Weight +0,1,4 +0,3,2 +0,4,7 +1,3,3 +1,4,5 +2,4,5 +2,5,2 +2,6,8 +3,4,1 +4,7,4 +5,6,4 +5,7,6 +""" + + +data_8_11 = """ +Source,Destination,Weight +0,3,1 +1,0,2 +1,4,3 +2,4,4 +2,5,5 +2,7,6 +3,4,8 +4,5,9 +5,6,10 +6,2,11 +""" + + +# Currently Graph does not support undirected graphs +# we are using directed graphs with symmetric edges to denote undirected graphs. +@pytest.fixture(autouse=True) +def pg_rmat15_cleaned_symmetric(): + # katana.local.initialize() + pg = Graph(get_rdg_dataset("rmat15_cleaned_symmetric")) + return pg + + +@pytest.fixture(autouse=True) +def katanagraph_rmat15_cleaned_di(pg_rmat15_cleaned_symmetric): + katana_graph = mg.wrappers.Graph.KatanaGraph(pg_rmat15_cleaned_symmetric) + return katana_graph + + +@pytest.fixture(autouse=True) +def katanagraph_rmat15_cleaned_ud(pg_rmat15_cleaned_symmetric): + katana_graph = mg.wrappers.Graph.KatanaGraph( + pg_rmat15_cleaned_symmetric, + is_weighted=True, + edge_weight_prop_name="value", + is_directed=False, + ) + return katana_graph + + +def gen_pg_cleaned_8_12_from_csr(is_directed): + """ + A helper function for the test, generating Katana's Graph from an edge list + """ + # katana.local.initialize() + elist_raw = [ + (0, 1, 4), + (0, 3, 2), + (0, 4, 7), + (1, 3, 3), + (1, 4, 5), + (2, 4, 5), + (2, 5, 2), + (2, 6, 8), + (3, 4, 1), + (4, 7, 4), + (5, 6, 4), + (5, 7, 6), + ] + src_list = [each[0] for each in elist_raw] + dest_list = [each[1] for each in elist_raw] + nlist_raw = list(set(src_list) | set(dest_list)) + # sort the eddge list and node list + if is_directed: + elist = sorted(elist_raw, key=lambda each: (each[0], each[1])) + else: + inv_elist = [ + (each[1], each[0], each[2]) for each in elist_raw if each[0] != each[1] + ] + elist = sorted(elist_raw + inv_elist, key=lambda each: (each[0], each[1])) + nlist = sorted(nlist_raw, key=lambda each: each) + # build the CSR format from the edge list (weight, (src, dst)) + row = np.array([each_edge[0] for each_edge in elist]) + col = np.array([each_edge[1] for each_edge in elist]) + data = np.array([each_edge[2] for each_edge in elist]) + csr = csr_matrix((data, (row, col)), shape=(len(nlist), len(nlist))) + # call the katana api to build a Graph (unweighted) from the CSR format + # noting that the first 0 in csr.indptr is excluded + pg = from_csr(csr.indptr[1:], csr.indices) + t = pyarrow.table(dict(value=data)) + pg.add_edge_property(t) + return pg + + +@pytest.fixture(autouse=True) +def katanagraph_cleaned_8_12_di(): + pg_cleaned_8_12_from_csr_di = gen_pg_cleaned_8_12_from_csr(is_directed=True) + katana_graph = mg.wrappers.Graph.KatanaGraph(pg_cleaned_8_12_from_csr_di) + return katana_graph + + +@pytest.fixture(autouse=True) +def katanagraph_cleaned_8_12_ud(): + pg_cleaned_8_12_from_csr_ud = gen_pg_cleaned_8_12_from_csr(is_directed=False) + katana_graph = mg.wrappers.Graph.KatanaGraph( + pg_cleaned_8_12_from_csr_ud, + is_weighted=True, + edge_weight_prop_name="value", + is_directed=False, + ) + return katana_graph + + +@pytest.fixture(autouse=True) +def networkx_weighted_undirected_8_12(): + csv_file = io.StringIO(data_8_12) + df = pd.read_csv(csv_file) + em = mg.wrappers.EdgeMap.PandasEdgeMap( + df, "Source", "Destination", "Weight", is_directed=False + ) + graph1 = mg.algos.util.graph.build(em) + return graph1 + + +@pytest.fixture(autouse=True) +def networkx_weighted_directed_8_12(): + csv_file = io.StringIO(data_8_12) + df = pd.read_csv(csv_file) + em = mg.wrappers.EdgeMap.PandasEdgeMap( + df, "Source", "Destination", "Weight", is_directed=True + ) + graph1 = mg.algos.util.graph.build(em) + return graph1 + + +@pytest.fixture(autouse=True) +def networkx_weighted_directed_bfs(): + csv_file = io.StringIO(data_8_11) + df = pd.read_csv(csv_file) + em = mg.wrappers.EdgeMap.PandasEdgeMap( + df, "Source", "Destination", "Weight", is_directed=True + ) + graph1 = mg.algos.util.graph.build(em) + return graph1 + + +# directed graph +@pytest.fixture(autouse=True) +def kg_from_nx_di_bfs(networkx_weighted_directed_bfs): + pg_test_case = mg.translate( + networkx_weighted_directed_bfs, mg.wrappers.Graph.KatanaGraph + ) + return pg_test_case + + +# directed graph +@pytest.fixture(autouse=True) +def kg_from_nx_di_8_12(networkx_weighted_directed_8_12): + pg_test_case = mg.translate( + networkx_weighted_directed_8_12, mg.wrappers.Graph.KatanaGraph + ) + return pg_test_case + + +# undirected graph +@pytest.fixture(autouse=True) +def kg_from_nx_ud_8_12(networkx_weighted_undirected_8_12): + pg_test_case = mg.translate( + networkx_weighted_undirected_8_12, mg.wrappers.Graph.KatanaGraph + ) + return pg_test_case + + +@pytest.fixture(autouse=True) +def nx_from_kg_di_8_12(katanagraph_cleaned_8_12_di): + return mg.translate(katanagraph_cleaned_8_12_di, mg.wrappers.Graph.NetworkXGraph) + + +@pytest.fixture(autouse=True) +def nx_from_kg_ud_8_12(katanagraph_cleaned_8_12_ud): + return mg.translate(katanagraph_cleaned_8_12_ud, mg.wrappers.Graph.NetworkXGraph) diff --git a/metagraph/tests/plugins/katanagraph/test_algorithms.py b/metagraph/tests/plugins/katanagraph/test_algorithms.py new file mode 100644 index 00000000..1497b570 --- /dev/null +++ b/metagraph/tests/plugins/katanagraph/test_algorithms.py @@ -0,0 +1,322 @@ +import metagraph as mg +import numpy as np +import pytest + + +@pytest.mark.xfail(reason="until BFS fix") +def test_bfs_iter(networkx_weighted_directed_bfs, kg_from_nx_di_bfs): + bfs1_nx = mg.algos.traversal.bfs_iter(networkx_weighted_directed_bfs, 0) + bfs1_kg = mg.algos.traversal.bfs_iter(kg_from_nx_di_bfs, 0) + assert bfs1_kg.tolist() == bfs1_nx.tolist() + assert bfs1_kg.tolist() == [0, 3, 4, 5, 6, 2, 7] + + +def test_bfs(networkx_weighted_directed_8_12, kg_from_nx_di_8_12): + bfs1_nx = mg.algos.traversal.bfs_iter(networkx_weighted_directed_8_12, 0) + bfs2_nx = mg.algos.traversal.bfs_iter(networkx_weighted_directed_8_12, 2) + bfs1_kg = mg.algos.traversal.bfs_iter(kg_from_nx_di_8_12, 0) + bfs2_kg = mg.algos.traversal.bfs_iter(kg_from_nx_di_8_12, 2) + assert bfs1_kg.tolist() == bfs1_nx.tolist() + assert bfs2_kg.tolist() == bfs2_nx.tolist() + assert bfs1_kg.tolist() == [0, 1, 3, 4, 7] + assert bfs2_kg.tolist() == [2, 4, 5, 6, 7] + + +def test_bfs_kg(katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + src_node = 2 + bfs1_kg = mg.algos.traversal.bfs_iter(katanagraph_cleaned_8_12_di, src_node) + bfs2_kg = mg.algos.traversal.bfs_iter(katanagraph_cleaned_8_12_di, src_node) + bfs_nx = mg.algos.traversal.bfs_iter(nx_from_kg_di_8_12, src_node) + assert bfs1_kg.tolist() == bfs2_kg.tolist() + assert len(bfs1_kg.tolist()) > 0 + assert bfs1_kg.tolist() == bfs_nx.tolist() + + +def test_sssp_bellman_ford(networkx_weighted_directed_8_12, kg_from_nx_di_8_12): + src_node = 0 + sssp_nx = mg.algos.traversal.bellman_ford( + networkx_weighted_directed_8_12, src_node + ) # source node is 0 + parents_nx = sssp_nx[0] + distances_nx = sssp_nx[1] + assert isinstance(parents_nx, dict) + assert isinstance(distances_nx, dict) + assert parents_nx == {0: 0, 1: 0, 3: 0, 4: 3, 7: 4} + assert distances_nx == {0: 0, 1: 4, 3: 2, 4: 3, 7: 7} + parents_kg, distances_kg = mg.algos.traversal.bellman_ford( + kg_from_nx_di_8_12, src_node + ) + assert parents_nx == parents_kg + assert distances_nx == distances_kg + + +def test_sssp_bellman_ford_kg(katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + src_node = 0 + sssp1_kg = mg.algos.traversal.bellman_ford(katanagraph_cleaned_8_12_di, src_node) + sssp2_kg = mg.algos.traversal.bellman_ford(katanagraph_cleaned_8_12_di, src_node) + sssp_nx = mg.algos.traversal.bellman_ford(nx_from_kg_di_8_12, src_node) + assert sssp1_kg[0] == sssp2_kg[0] + assert sssp1_kg[1] == sssp2_kg[1] + assert sssp1_kg[0] == sssp_nx[0] + assert sssp1_kg[1] == sssp_nx[1] + + +def test_sssp_dijkstra(networkx_weighted_directed_8_12, kg_from_nx_di_8_12): + src_node = 1 + sssp_nx = mg.algos.traversal.dijkstra( + networkx_weighted_directed_8_12, src_node + ) # source node is 1 + parents_nx = sssp_nx[0] + distances_nx = sssp_nx[1] + assert isinstance(parents_nx, dict) + assert isinstance(distances_nx, dict) + assert parents_nx == {1: 1, 3: 1, 4: 3, 7: 4} + assert distances_nx == {1: 0, 3: 3, 4: 4, 7: 8} + parents_kg, distances_kg = mg.algos.traversal.dijkstra(kg_from_nx_di_8_12, src_node) + assert parents_nx == parents_kg + assert distances_nx == distances_kg + + +def test_sssp_dijkstra_kg(katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + src_node = 1 + sssp1_kg = mg.algos.traversal.dijkstra(katanagraph_cleaned_8_12_di, src_node) + sssp2_kg = mg.algos.traversal.dijkstra(katanagraph_cleaned_8_12_di, src_node) + sssp_nx = mg.algos.traversal.dijkstra(nx_from_kg_di_8_12, src_node) + assert sssp1_kg[0] == sssp2_kg[0] + assert sssp1_kg[1] == sssp2_kg[1] + assert sssp1_kg[0] == sssp_nx[0] + assert sssp1_kg[1] == sssp_nx[1] + + +def test_connected_components(networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12): + cc_nx = mg.algos.clustering.connected_components(networkx_weighted_undirected_8_12) + cc_kg = mg.algos.clustering.connected_components(kg_from_nx_ud_8_12) + assert isinstance(cc_kg, dict) + assert isinstance(cc_kg, dict) + assert cc_kg == cc_nx + + +def test_connected_components_kg(katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + cc_kg1 = mg.algos.clustering.connected_components(katanagraph_cleaned_8_12_ud) + cc_kg2 = mg.algos.clustering.connected_components(katanagraph_cleaned_8_12_ud) + cc_nx = mg.algos.clustering.connected_components(nx_from_kg_ud_8_12) + assert cc_kg1 == cc_kg2 + assert cc_kg1 == cc_nx + + +def test_pagerank(networkx_weighted_directed_8_12, kg_from_nx_di_8_12): + pr_nx = mg.algos.centrality.pagerank(networkx_weighted_directed_8_12) + pr_kg = mg.algos.centrality.pagerank(kg_from_nx_di_8_12) + assert isinstance(pr_nx, dict) + assert isinstance(pr_kg, dict) + assert pr_nx == pr_kg + + +def test_pagerank_kg(katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs + """ + pr_kg1 = mg.algos.centrality.pagerank(katanagraph_cleaned_8_12_di) + pr_kg2 = mg.algos.centrality.pagerank(katanagraph_cleaned_8_12_di) + pr_nx = mg.algos.centrality.pagerank(nx_from_kg_di_8_12) + assert pr_kg1 == pr_kg2 + assert pr_kg1 == pr_nx + + +def test_betweenness_centrality(networkx_weighted_directed_8_12, kg_from_nx_di_8_12): + bc_nx = mg.algos.centrality.betweenness(networkx_weighted_directed_8_12) + bc_kg = mg.algos.centrality.betweenness(kg_from_nx_di_8_12) + assert isinstance(bc_nx, dict) + assert isinstance(bc_kg, dict) + assert bc_nx == bc_kg + + +def test_betweenness_centrality_kg(katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + Notice for large graphs, mg.algos.centrality.betweenness is extremely slow (not because of our translator) + """ + bc_kg1 = mg.algos.centrality.betweenness(katanagraph_cleaned_8_12_di) + bc_kg2 = mg.algos.centrality.betweenness(katanagraph_cleaned_8_12_di) + assert bc_kg1 == bc_kg2 + bc_nx = mg.algos.centrality.betweenness(nx_from_kg_di_8_12) + assert bc_kg1 == bc_nx + + +def test_triangle_counting(networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12): + tc_nx = mg.algos.clustering.triangle_count(networkx_weighted_undirected_8_12) + tc_kg = mg.algos.clustering.triangle_count(kg_from_nx_ud_8_12) + assert isinstance(tc_nx, int) + assert isinstance(tc_kg, int) + assert tc_nx == tc_kg + + +def test_triangle_counting_kg(katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + tc_kg1 = mg.algos.clustering.triangle_count(katanagraph_cleaned_8_12_ud) + tc_kg2 = mg.algos.clustering.triangle_count(katanagraph_cleaned_8_12_ud) + tc_nx = mg.algos.clustering.triangle_count(nx_from_kg_ud_8_12) + assert tc_kg1 == tc_kg2 + assert tc_kg1 == tc_nx + + +def test_louvain_community_detection( + networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12 +): + lc_nx = mg.algos.clustering.louvain_community(networkx_weighted_undirected_8_12) + lc_kg = mg.algos.clustering.louvain_community(kg_from_nx_ud_8_12) + assert isinstance(lc_nx[0], dict) + assert isinstance(lc_kg[0], dict) + assert isinstance(lc_nx[1], float) + assert isinstance(lc_kg[1], float) + assert lc_nx[0] == lc_kg[0] + assert lc_nx[1] == lc_kg[1] + + +def test_louvain_community_detection_kg( + katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12 +): + """ + test for katana graph which is directly loaded rather than translated from nettworkx. + We cannot expect two consecutive runs with the same source code give the same results. + The reason is two runs use different random seeds. + Besides, we cannot set the random seed cause the metagraph wrapper hide that option in network's community_louvain.best_partition function + """ + lc_kg1 = mg.algos.clustering.louvain_community(katanagraph_cleaned_8_12_ud) + lc_kg2 = mg.algos.clustering.louvain_community(katanagraph_cleaned_8_12_ud) + lc_nx = mg.algos.clustering.louvain_community(nx_from_kg_ud_8_12) + # assert lc_kg1[0] == lc_kg2[0] # failed cause two runs get different results (different random seeds from network's community_louvain.best_partition function). + assert abs(lc_kg1[1] - lc_kg2[1]) < 0.1 + assert lc_kg1[0] == lc_nx[0] + assert lc_kg1[1] == lc_nx[1] + + +def test_translation_subgraph_extraction( + networkx_weighted_directed_8_12, kg_from_nx_di_8_12 +): + se_nx = mg.algos.subgraph.extract_subgraph( + networkx_weighted_directed_8_12, {0, 2, 3} + ) + se_kg = mg.algos.subgraph.extract_subgraph(kg_from_nx_di_8_12, {0, 2, 3}) + assert isinstance(se_nx, mg.wrappers.Graph.NetworkXGraph) + assert isinstance(se_kg, mg.wrappers.Graph.NetworkXGraph) + assert list(se_nx.value.edges(data=True)) == list(se_kg.value.edges(data=True)) + + +def test_translation_subgraph_extraction_kg( + katanagraph_cleaned_8_12_di, nx_from_kg_di_8_12 +): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + ids = {0, 4, 5} + se_kg1 = mg.algos.subgraph.extract_subgraph(katanagraph_cleaned_8_12_di, ids) + se_kg2 = mg.algos.subgraph.extract_subgraph(katanagraph_cleaned_8_12_di, ids) + se_nx = mg.algos.subgraph.extract_subgraph(nx_from_kg_di_8_12, ids) + assert list(se_kg1.value.edges(data=True)) == list(se_kg2.value.edges(data=True)) + assert list(se_kg1.value.edges(data=True)) == list(se_nx.value.edges(data=True)) + + +def test_labal_propagation(networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12): + cd_nx = mg.algos.clustering.label_propagation_community( + networkx_weighted_undirected_8_12 + ) + cd_kg = mg.algos.clustering.label_propagation_community(kg_from_nx_ud_8_12) + assert isinstance(cd_nx, dict) + assert isinstance(cd_kg, dict) + assert cd_nx == cd_kg + + +def test_labal_propagation_kg(katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + cd_kg1 = mg.algos.clustering.label_propagation_community( + katanagraph_cleaned_8_12_ud + ) + cd_kg2 = mg.algos.clustering.label_propagation_community( + katanagraph_cleaned_8_12_ud + ) + cd_nx = mg.algos.clustering.label_propagation_community(nx_from_kg_ud_8_12) + assert cd_kg1 == cd_kg2 + assert cd_kg1 == cd_nx + + +def test_jaccard_similarity(networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12): + compare_node = 0 + prop_name = "jaccard_prop_with_" + str(compare_node) + jcd_nx = mg.algos.traversal.jaccard(networkx_weighted_undirected_8_12, compare_node) + jcd_kg = mg.algos.traversal.jaccard(kg_from_nx_ud_8_12, compare_node) + assert isinstance(jcd_nx, np.ndarray) + assert isinstance(jcd_kg, np.ndarray) + assert jcd_nx.tolist() == jcd_kg.tolist() + assert jcd_kg[compare_node] == 1 + + +def test_jaccard_similarity_kg(katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + compare_node = 3 + prop_name = "jaccard_prop_with_" + str(compare_node) + jcd_kg1 = mg.algos.traversal.jaccard(katanagraph_cleaned_8_12_ud, compare_node) + jcd_kg2 = mg.algos.traversal.jaccard(katanagraph_cleaned_8_12_ud, compare_node) + assert jcd_kg1.tolist() == jcd_kg2.tolist() + assert jcd_kg1[compare_node] == 1 + assert jcd_kg2[compare_node] == 1 + jcd_nx = mg.algos.traversal.jaccard(nx_from_kg_ud_8_12, compare_node) + assert jcd_nx[compare_node] == 1 + assert jcd_kg1.tolist() == jcd_nx.tolist() + + +def test_local_clustering_coefficient( + networkx_weighted_undirected_8_12, kg_from_nx_ud_8_12 +): + prop_name = "output_prop" + lcc_nx = mg.algos.clustering.local_clustering_coefficient( + networkx_weighted_undirected_8_12, prop_name + ) + lcc_kg = mg.algos.clustering.local_clustering_coefficient( + kg_from_nx_ud_8_12, prop_name + ) + assert isinstance(lcc_nx, np.ndarray) + assert isinstance(lcc_kg, np.ndarray) + assert lcc_kg.tolist() == lcc_nx.tolist() + assert lcc_kg[-1] == 0 + assert not np.any(np.isnan(lcc_kg)) + + +def test_local_clustering_coefficient_kg( + katanagraph_cleaned_8_12_ud, nx_from_kg_ud_8_12 +): + """ + test for katana graph which is directly loaded rather than translated from nettworkx, also test two consecutive runs with the same source code + """ + prop_name = "output_prop" + lcc_kg1 = mg.algos.clustering.local_clustering_coefficient( + katanagraph_cleaned_8_12_ud, prop_name + ) + lcc_kg2 = mg.algos.clustering.local_clustering_coefficient( + katanagraph_cleaned_8_12_ud, prop_name + ) + assert lcc_kg1.tolist() == lcc_kg2.tolist() + assert lcc_kg1[-1] == 0 + assert not np.any(np.isnan(lcc_kg1)) + lcc_nx = mg.algos.clustering.local_clustering_coefficient( + nx_from_kg_ud_8_12, prop_name + ) + assert lcc_kg1.tolist() == lcc_nx.tolist() diff --git a/metagraph/tests/plugins/katanagraph/test_translators.py b/metagraph/tests/plugins/katanagraph/test_translators.py new file mode 100644 index 00000000..4456434e --- /dev/null +++ b/metagraph/tests/plugins/katanagraph/test_translators.py @@ -0,0 +1,97 @@ +import metagraph as mg + + +def test_num_nodes(kg_from_nx_di_8_12): + nodes_total = 0 + for nid in kg_from_nx_di_8_12.value: + nodes_total += 1 + assert kg_from_nx_di_8_12.value.num_nodes() == nodes_total + assert kg_from_nx_di_8_12.value.num_nodes() == 8 + + +def test_num_edges(kg_from_nx_di_8_12): + edges_total = 0 + for nid in kg_from_nx_di_8_12.value: + edges_total += len(kg_from_nx_di_8_12.value.edge_ids(nid)) + assert kg_from_nx_di_8_12.value.num_edges() == edges_total + assert kg_from_nx_di_8_12.value.num_edges() == 12 + + +def test_topology(kg_from_nx_di_8_12): + assert kg_from_nx_di_8_12.value.edge_ids(0) == range(0, 3) + assert kg_from_nx_di_8_12.value.edge_ids(1) == range(3, 5) + assert kg_from_nx_di_8_12.value.edge_ids(2) == range(5, 8) + assert kg_from_nx_di_8_12.value.edge_ids(3) == range(8, 9) + assert kg_from_nx_di_8_12.value.edge_ids(4) == range(9, 10) + assert kg_from_nx_di_8_12.value.edge_ids(5) == range(10, 12) + assert [ + kg_from_nx_di_8_12.value.get_edge_dest(i) + for i in kg_from_nx_di_8_12.value.edge_ids(0) + ] == [1, 3, 4] + assert [ + kg_from_nx_di_8_12.value.get_edge_dest(i) + for i in kg_from_nx_di_8_12.value.edge_ids(2) + ] == [4, 5, 6] + assert [ + kg_from_nx_di_8_12.value.get_edge_dest(i) + for i in kg_from_nx_di_8_12.value.edge_ids(4) + ] == [7] + assert [ + kg_from_nx_di_8_12.value.get_edge_dest(i) + for i in kg_from_nx_di_8_12.value.edge_ids(5) + ] == [6, 7] + + +def test_schema(kg_from_nx_di_8_12): + assert len(kg_from_nx_di_8_12.value.loaded_node_schema()) == 1 + assert len(kg_from_nx_di_8_12.value.loaded_edge_schema()) == 1 + + +def test_edge_property_directed(kg_from_nx_di_8_12): + assert ( + kg_from_nx_di_8_12.value.loaded_edge_schema()[0].name + == "edge_value_from_translator" + ) + assert kg_from_nx_di_8_12.value.get_edge_property( + 0 + ) == kg_from_nx_di_8_12.value.get_edge_property("edge_value_from_translator") + assert kg_from_nx_di_8_12.value.get_edge_property( + "edge_value_from_translator" + ).tolist() == [4, 2, 7, 3, 5, 5, 2, 8, 1, 4, 4, 6,] + + +def test_compare_node_count(nx_from_kg_di_8_12, katanagraph_cleaned_8_12_di): + nlist = [ + each_node[0] for each_node in list(nx_from_kg_di_8_12.value.nodes(data=True)) + ] + num_no_edge_nodes = 0 + for nid in katanagraph_cleaned_8_12_di.value: + if nid not in nlist: + assert katanagraph_cleaned_8_12_di.value.edge_ids(nid) == range(0, 0) + num_no_edge_nodes += 1 + assert ( + num_no_edge_nodes + len(nlist) == katanagraph_cleaned_8_12_di.value.num_nodes() + ) + assert num_no_edge_nodes == 0 + + +def test_compare_edge_count(nx_from_kg_di_8_12, katanagraph_cleaned_8_12_di): + edge_dict_count = { + (each_e[0], each_e[1]): 0 + for each_e in list(nx_from_kg_di_8_12.value.edges(data=True)) + } + for src in katanagraph_cleaned_8_12_di.value: + for dest in [ + katanagraph_cleaned_8_12_di.value.get_edge_dest(e) + for e in katanagraph_cleaned_8_12_di.value.edge_ids(src) + ]: + if (src, dest) in edge_dict_count: + edge_dict_count[(src, dest)] += 1 + assert ( + sum([edge_dict_count[i] for i in edge_dict_count]) + == katanagraph_cleaned_8_12_di.value.num_edges() + ) + assert ( + len(list(nx_from_kg_di_8_12.value.edges(data=True))) + == katanagraph_cleaned_8_12_di.value.num_edges() + ) diff --git a/metagraph/tests/plugins/katanagraph/test_types.py b/metagraph/tests/plugins/katanagraph/test_types.py new file mode 100644 index 00000000..83990626 --- /dev/null +++ b/metagraph/tests/plugins/katanagraph/test_types.py @@ -0,0 +1,115 @@ +import metagraph as mg +import pytest + + +def test_num_nodes(katanagraph_rmat15_cleaned_di): + cnt = 0 + for nid in katanagraph_rmat15_cleaned_di.value: + cnt += 1 + assert katanagraph_rmat15_cleaned_di.value.num_nodes() == 32768 + assert katanagraph_rmat15_cleaned_di.value.num_nodes() == cnt + + +def test_num_edges(katanagraph_rmat15_cleaned_di): + cnt = 0 + for nid in katanagraph_rmat15_cleaned_di.value: + cnt += len(katanagraph_rmat15_cleaned_di.value.edge_ids(nid)) + assert katanagraph_rmat15_cleaned_di.value.num_edges() == 363194 + assert katanagraph_rmat15_cleaned_di.value.num_edges() == cnt + + +def test_node_schema(katanagraph_rmat15_cleaned_di): + assert "names" in dir(katanagraph_rmat15_cleaned_di.value.loaded_node_schema()) + assert "types" in dir(katanagraph_rmat15_cleaned_di.value.loaded_node_schema()) + assert len(katanagraph_rmat15_cleaned_di.value.loaded_node_schema()) == 0 + + +def test_edge_schema(katanagraph_rmat15_cleaned_di): + assert "names" in dir(katanagraph_rmat15_cleaned_di.value.loaded_edge_schema()) + assert "types" in dir(katanagraph_rmat15_cleaned_di.value.loaded_edge_schema()) + assert len(katanagraph_rmat15_cleaned_di.value.loaded_edge_schema()) == 1 + + +def test_edge_property(katanagraph_rmat15_cleaned_di): + assert katanagraph_rmat15_cleaned_di.value.loaded_edge_schema()[0].name == "value" + assert katanagraph_rmat15_cleaned_di.value.get_edge_property( + 0 + ) == katanagraph_rmat15_cleaned_di.value.get_edge_property("value") + assert ( + katanagraph_rmat15_cleaned_di.value.get_edge_property("value").to_pandas()[0] + == 339302416426 + ) + + +def test_topology(katanagraph_rmat15_cleaned_di): + assert katanagraph_rmat15_cleaned_di.value.edge_ids(0) == range(0, 20767) + assert [ + katanagraph_rmat15_cleaned_di.value.get_edge_dest(i) + for i in katanagraph_rmat15_cleaned_di.value.edge_ids(0) + ][0:5] == [1, 2, 3, 4, 5,] + + assert katanagraph_rmat15_cleaned_di.value.edge_ids(8) == range(36475, 41133) + assert [ + katanagraph_rmat15_cleaned_di.value.get_edge_dest(i) + for i in katanagraph_rmat15_cleaned_di.value.edge_ids(8) + ][0:5] == [0, 9, 10, 11, 12,] + + +def test_num_nodes_networkx( + networkx_weighted_undirected_8_12, networkx_weighted_directed_8_12 +): + assert len(list(networkx_weighted_undirected_8_12.value.nodes(data=True))) == 8 + assert len(list(networkx_weighted_directed_8_12.value.nodes(data=True))) == 8 + + +def test_num_edges_networkx( + networkx_weighted_undirected_8_12, networkx_weighted_directed_8_12 +): + assert len(list(networkx_weighted_undirected_8_12.value.edges(data=True))) == 12 + assert len(list(networkx_weighted_directed_8_12.value.edges(data=True))) == 12 + + +def test_topology_networkx( + networkx_weighted_undirected_8_12, networkx_weighted_directed_8_12 +): + assert list(networkx_weighted_undirected_8_12.value.nodes(data=True)) == list( + networkx_weighted_directed_8_12.value.nodes(data=True) + ) + assert list(networkx_weighted_undirected_8_12.value.nodes(data=True)) == [ + (0, {}), + (1, {}), + (3, {}), + (4, {}), + (2, {}), + (5, {}), + (6, {}), + (7, {}), + ] + assert list(networkx_weighted_undirected_8_12.value.edges(data=True)) == [ + (0, 1, {"weight": 4}), + (0, 3, {"weight": 2}), + (0, 4, {"weight": 7}), + (1, 3, {"weight": 3}), + (1, 4, {"weight": 5}), + (3, 4, {"weight": 1}), + (4, 2, {"weight": 5}), + (4, 7, {"weight": 4}), + (2, 5, {"weight": 2}), + (2, 6, {"weight": 8}), + (5, 6, {"weight": 4}), + (5, 7, {"weight": 6}), + ] + assert list(networkx_weighted_directed_8_12.value.edges(data=True)) == [ + (0, 1, {"weight": 4}), + (0, 3, {"weight": 2}), + (0, 4, {"weight": 7}), + (1, 3, {"weight": 3}), + (1, 4, {"weight": 5}), + (3, 4, {"weight": 1}), + (4, 7, {"weight": 4}), + (2, 4, {"weight": 5}), + (2, 5, {"weight": 2}), + (2, 6, {"weight": 8}), + (5, 6, {"weight": 4}), + (5, 7, {"weight": 6}), + ]