From 5fad4356729482ae5d4843a6e74330f3aa81a59c Mon Sep 17 00:00:00 2001 From: Joseph Nke <76006812+jnke2016@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:56:46 +0100 Subject: [PATCH] Refactor the python function symmetrizing the edgelist (#4649) This PR updates the python API to symmetrize the edge list through the CAPI for PLC algorithms. This PR also deprecates the legacy python function symmetrizing the edge list closes #4588 --- cpp/include/cugraph_c/graph.h | 14 +- cpp/src/c_api/graph_mg.cpp | 30 ++++ cpp/src/c_api/graph_sg.cpp | 61 ++++++++ cpp/tests/c_api/create_graph_test.c | 6 + cpp/tests/c_api/mg_create_graph_test.c | 3 + .../cugraph/structure/graph_classes.py | 27 +++- .../simpleDistributedGraph.py | 56 +++++--- .../graph_implementation/simpleGraph.py | 131 +++++++++++++----- .../cugraph/cugraph/structure/hypergraph.py | 27 ++++ .../cugraph/structure/property_graph.py | 28 ++++ .../cugraph/cugraph/structure/symmetrize.py | 5 + .../tests/sampling/test_random_walks_mg.py | 15 +- .../sampling/test_uniform_neighbor_sample.py | 28 +++- .../test_uniform_neighbor_sample_mg.py | 10 ++ .../cugraph/tests/structure/test_graph.py | 13 ++ .../tests/structure/test_multigraph.py | 4 +- .../cugraph/tests/utils/test_dataset.py | 24 ++++ .../cugraph/cugraph/utilities/nx_factory.py | 41 +++++- python/cugraph/pytest.ini | 1 + .../pylibcugraph/_cugraph_c/graph.pxd | 3 + python/pylibcugraph/pylibcugraph/graphs.pyx | 21 ++- 21 files changed, 477 insertions(+), 71 deletions(-) diff --git a/cpp/include/cugraph_c/graph.h b/cpp/include/cugraph_c/graph.h index 00fce0493a3..d812b503778 100644 --- a/cpp/include/cugraph_c/graph.h +++ b/cpp/include/cugraph_c/graph.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2021-2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,6 +105,8 @@ cugraph_error_code_t cugraph_sg_graph_create( weights, * or take the maximum weight), the caller should remove specific edges themselves and not rely * on this flag. + * @param [in] symmetrize If true, symmetrize the edgelist. The symmetrization of edges + * with edge_ids and/or edge_type_ids is currently not supported. * @param [in] do_expensive_check If true, do expensive checks to validate the input data * is consistent with software assumptions. If false bypass these checks. * @param [out] graph A pointer to the graph object @@ -126,6 +128,7 @@ cugraph_error_code_t cugraph_graph_create_sg( bool_t renumber, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error); @@ -150,6 +153,8 @@ cugraph_error_code_t cugraph_graph_create_sg( * If false, do not renumber. Renumbering enables some significant optimizations within * the graph primitives library, so it is strongly encouraged. Renumbering is required if * the vertices are not sequential integer values from 0 to num_vertices. + * @param [in] symmetrize If true, symmetrize the edgelist. The symmetrization of edges + * with edge_ids and/or edge_type_ids is currently not supported. * @param [in] do_expensive_check If true, do expensive checks to validate the input data * is consistent with software assumptions. If false bypass these checks. * @param [out] graph A pointer to the graph object @@ -168,6 +173,7 @@ cugraph_error_code_t cugraph_sg_graph_create_from_csr( const cugraph_type_erased_device_array_view_t* edge_type_ids, bool_t store_transposed, bool_t renumber, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error); @@ -190,6 +196,8 @@ cugraph_error_code_t cugraph_sg_graph_create_from_csr( * If false, do not renumber. Renumbering enables some significant optimizations within * the graph primitives library, so it is strongly encouraged. Renumbering is required if * the vertices are not sequential integer values from 0 to num_vertices. + * @param [in] symmetrize If true, symmetrize the edgelist. The symmetrization of edges + * with edge_ids and/or edge_type_ids is currently not supported. * @param [in] do_expensive_check If true, do expensive checks to validate the input data * is consistent with software assumptions. If false bypass these checks. * @param [out] graph A pointer to the graph object @@ -208,6 +216,7 @@ cugraph_error_code_t cugraph_graph_create_sg_from_csr( const cugraph_type_erased_device_array_view_t* edge_type_ids, bool_t store_transposed, bool_t renumber, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error); @@ -289,6 +298,8 @@ cugraph_error_code_t cugraph_mg_graph_create( * Note that setting this flag will arbitrarily select one instance of a multi edge to be the * edge that survives. If the edges have properties that should be honored (e.g. sum the * weights, or take the maximum weight), the caller should do that on not rely on this flag. + * @param [in] symmetrize If true, symmetrize the edgelist. The symmetrization of edges + * with edge_ids and/or edge_type_ids is currently not supported. * @param [in] do_expensive_check If true, do expensive checks to validate the input data * is consistent with software assumptions. If false bypass these checks. * @param [out] graph A pointer to the graph object @@ -309,6 +320,7 @@ cugraph_error_code_t cugraph_graph_create_mg( size_t num_arrays, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error); diff --git a/cpp/src/c_api/graph_mg.cpp b/cpp/src/c_api/graph_mg.cpp index cc4acd31743..fc8014a5dd8 100644 --- a/cpp/src/c_api/graph_mg.cpp +++ b/cpp/src/c_api/graph_mg.cpp @@ -71,6 +71,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { bool_t renumber_; bool_t drop_self_loops_; bool_t drop_multi_edges_; + bool_t symmetrize_; bool_t do_expensive_check_; cugraph::c_api::cugraph_graph_t* result_{}; @@ -91,6 +92,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { bool_t renumber, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t do_expensive_check) : abstract_functor(), properties_(properties), @@ -109,6 +111,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { renumber_(renumber), drop_self_loops_(drop_self_loops), drop_multi_edges_(drop_multi_edges), + symmetrize_(symmetrize), do_expensive_check_(do_expensive_check) { } @@ -224,6 +227,22 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { : false); } + if (symmetrize_) { + if (edgelist_edge_ids || edgelist_edge_types) { + // Currently doesn't support the symmetrization of edgelist with edge_ids and edge_types + unsupported(); + } + + // Symmetrize the edgelist + std::tie(edgelist_srcs, edgelist_dsts, edgelist_weights) = + cugraph::symmetrize_edgelist( + handle_, + std::move(edgelist_srcs), + std::move(edgelist_dsts), + std::move(edgelist_weights), + false); + } + std::tie(*graph, new_edge_weights, new_edge_ids, new_edge_types, new_number_map) = cugraph::create_graph_from_edgelisttype_; } + if (symmetrize == TRUE) { + CAPI_EXPECTS((properties->is_symmetric == TRUE), + CUGRAPH_INVALID_INPUT, + "Invalid input arguments: The graph property must be symmetric if 'symmetrize' " + "is set to True.", + *error); + } + CAPI_EXPECTS(p_src[i]->type_ == vertex_type, CUGRAPH_INVALID_INPUT, "Invalid input arguments: all vertex types must match", @@ -488,6 +516,7 @@ extern "C" cugraph_error_code_t cugraph_graph_create_mg( bool_t::TRUE, drop_self_loops, drop_multi_edges, + symmetrize, do_expensive_check); try { @@ -534,6 +563,7 @@ extern "C" cugraph_error_code_t cugraph_mg_graph_create( 1, FALSE, FALSE, + FALSE, do_expensive_check, graph, error); diff --git a/cpp/src/c_api/graph_sg.cpp b/cpp/src/c_api/graph_sg.cpp index ff71471a8d0..f6ea8e4142e 100644 --- a/cpp/src/c_api/graph_sg.cpp +++ b/cpp/src/c_api/graph_sg.cpp @@ -43,6 +43,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { bool_t renumber_; bool_t drop_self_loops_; bool_t drop_multi_edges_; + bool_t symmetrize_; bool_t do_expensive_check_; cugraph_data_type_id_t edge_type_; cugraph::c_api::cugraph_graph_t* result_{}; @@ -58,6 +59,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { bool_t renumber, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t do_expensive_check, cugraph_data_type_id_t edge_type) : abstract_functor(), @@ -72,6 +74,7 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { renumber_(renumber), drop_self_loops_(drop_self_loops), drop_multi_edges_(drop_multi_edges), + symmetrize_(symmetrize), do_expensive_check_(do_expensive_check), edge_type_(edge_type) { @@ -207,6 +210,22 @@ struct create_graph_functor : public cugraph::c_api::abstract_functor { : false); } + if (symmetrize_) { + if (edgelist_edge_ids || edgelist_edge_types) { + // Currently doesn't support the symmetrization with edge_ids and edge_types + unsupported(); + } + + // Symmetrize the edgelist + std::tie(edgelist_srcs, edgelist_dsts, edgelist_weights) = + cugraph::symmetrize_edgelist( + handle_, + std::move(edgelist_srcs), + std::move(edgelist_dsts), + std::move(edgelist_weights), + false); + } + std::tie(*graph, new_edge_weights, new_edge_ids, new_edge_types, new_number_map) = cugraph::create_graph_from_edgelist, edge_type_id_t>(handle_); + if (symmetrize_) { + if (edgelist_edge_ids || edgelist_edge_types) { + // Currently doesn't support the symmetrization with edge_ids and edge_types + unsupported(); + } + + // Symmetrize the edgelist + std::tie(edgelist_srcs, edgelist_dsts, edgelist_weights) = + cugraph::symmetrize_edgelist( + handle_, + std::move(edgelist_srcs), + std::move(edgelist_dsts), + std::move(edgelist_weights), + false); + } + std::tie(*graph, new_edge_weights, new_edge_ids, new_edge_types, new_number_map) = cugraph::create_graph_from_edgelist(edge_type_ids); + if (symmetrize == TRUE) { + CAPI_EXPECTS((properties->is_symmetric == TRUE), + CUGRAPH_INVALID_INPUT, + "Invalid input arguments: The graph property must be symmetric if 'symmetrize' is " + "set to True.", + *error); + } + CAPI_EXPECTS(p_src->size_ == p_dst->size_, CUGRAPH_INVALID_INPUT, "Invalid input arguments: src size != dst size.", @@ -606,6 +653,7 @@ extern "C" cugraph_error_code_t cugraph_graph_create_sg( renumber, drop_self_loops, drop_multi_edges, + symmetrize, do_expensive_check, edge_type); @@ -658,6 +706,7 @@ extern "C" cugraph_error_code_t cugraph_sg_graph_create( renumber, FALSE, FALSE, + FALSE, do_expensive_check, graph, error); @@ -673,6 +722,7 @@ cugraph_error_code_t cugraph_graph_create_sg_from_csr( const cugraph_type_erased_device_array_view_t* edge_type_ids, bool_t store_transposed, bool_t renumber, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error) @@ -707,6 +757,14 @@ cugraph_error_code_t cugraph_graph_create_sg_from_csr( weight_type = cugraph_data_type_id_t::FLOAT32; } + if (symmetrize == TRUE) { + CAPI_EXPECTS((properties->is_symmetric == TRUE), + CUGRAPH_INVALID_INPUT, + "Invalid input arguments: The graph property must be symmetric if 'symmetrize' is " + "set to True.", + *error); + } + CAPI_EXPECTS( (edge_type_ids == nullptr && edge_ids == nullptr) || (edge_type_ids != nullptr && edge_ids != nullptr), @@ -735,6 +793,7 @@ cugraph_error_code_t cugraph_graph_create_sg_from_csr( p_edge_ids, p_edge_type_ids, renumber, + FALSE, // symmetrize do_expensive_check); try { @@ -770,6 +829,7 @@ cugraph_error_code_t cugraph_sg_graph_create_from_csr( const cugraph_type_erased_device_array_view_t* edge_type_ids, bool_t store_transposed, bool_t renumber, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error) @@ -783,6 +843,7 @@ cugraph_error_code_t cugraph_sg_graph_create_from_csr( edge_type_ids, store_transposed, renumber, + symmetrize, do_expensive_check, graph, error); diff --git a/cpp/tests/c_api/create_graph_test.c b/cpp/tests/c_api/create_graph_test.c index 41b8691e79c..104787e4c7b 100644 --- a/cpp/tests/c_api/create_graph_test.c +++ b/cpp/tests/c_api/create_graph_test.c @@ -104,6 +104,7 @@ int test_create_sg_graph_simple() FALSE, FALSE, FALSE, + FALSE, &graph, &ret_error); TEST_ASSERT(test_ret_value, ret_code == CUGRAPH_SUCCESS, "graph creation failed."); @@ -213,6 +214,7 @@ int test_create_sg_graph_csr() FALSE, FALSE, FALSE, + FALSE, &graph, &ret_error); TEST_ASSERT(test_ret_value, ret_code == CUGRAPH_SUCCESS, "graph creation failed."); @@ -408,6 +410,7 @@ int test_create_sg_graph_symmetric_error() FALSE, FALSE, FALSE, + FALSE, TRUE, &graph, &ret_error); @@ -526,6 +529,7 @@ int test_create_sg_graph_with_isolated_vertices() FALSE, FALSE, FALSE, + FALSE, &graph, &ret_error); TEST_ASSERT(test_ret_value, ret_code == CUGRAPH_SUCCESS, "graph creation failed."); @@ -675,6 +679,7 @@ int test_create_sg_graph_csr_with_isolated() FALSE, FALSE, FALSE, + FALSE, &graph, &ret_error); TEST_ASSERT(test_ret_value, ret_code == CUGRAPH_SUCCESS, "graph creation failed."); @@ -840,6 +845,7 @@ int test_create_sg_graph_with_isolated_vertices_multi_input() TRUE, TRUE, FALSE, + FALSE, &graph, &ret_error); TEST_ASSERT(test_ret_value, ret_code == CUGRAPH_SUCCESS, "graph creation failed."); diff --git a/cpp/tests/c_api/mg_create_graph_test.c b/cpp/tests/c_api/mg_create_graph_test.c index dd817881325..12579f26d06 100644 --- a/cpp/tests/c_api/mg_create_graph_test.c +++ b/cpp/tests/c_api/mg_create_graph_test.c @@ -109,6 +109,7 @@ int test_create_mg_graph_simple(const cugraph_resource_handle_t* handle) 1, FALSE, FALSE, + FALSE, TRUE, &graph, &ret_error); @@ -251,6 +252,7 @@ int test_create_mg_graph_multiple_edge_lists(const cugraph_resource_handle_t* ha num_local_arrays, FALSE, FALSE, + FALSE, TRUE, &graph, &ret_error); @@ -446,6 +448,7 @@ int test_create_mg_graph_multiple_edge_lists_multi_edge(const cugraph_resource_h num_local_arrays, TRUE, TRUE, + FALSE, TRUE, &graph, &ret_error); diff --git a/python/cugraph/cugraph/structure/graph_classes.py b/python/cugraph/cugraph/structure/graph_classes.py index e90c0576f55..84234f7e904 100644 --- a/python/cugraph/cugraph/structure/graph_classes.py +++ b/python/cugraph/cugraph/structure/graph_classes.py @@ -116,6 +116,7 @@ def from_cudf_edgelist( renumber=True, store_transposed=False, legacy_renum_only=False, + symmetrize=None, ): """ Initialize a graph from the edge list. It is an error to call this @@ -174,6 +175,15 @@ def from_cudf_edgelist( This parameter is deprecated and will be removed. + symmetrize: bool, optional (default=None) + If True, symmetrize the edge list for an undirected graph. Setting + this flag to True for a directed graph returns an error. The default + behavior symmetrizes the edges if the graph is undirected. This flag + cannot be set to True if the edgelist contains edge IDs or edge Types. + If the incoming edgelist is intended for an undirected graph and it is + known to be symmetric, this flag can be set to False to skip the + symmetrization step for better performance. + Examples -------- >>> df = cudf.read_csv(datasets_path / 'karate.csv', delimiter=' ', @@ -201,6 +211,7 @@ def from_cudf_edgelist( renumber=renumber, store_transposed=store_transposed, legacy_renum_only=legacy_renum_only, + symmetrize=symmetrize, ) def from_cudf_adjlist( @@ -210,6 +221,7 @@ def from_cudf_adjlist( value_col=None, renumber=True, store_transposed=False, + symmetrize=None, ): """ Initialize a graph from the adjacency list. It is an error to call this @@ -247,6 +259,14 @@ def from_cudf_adjlist( store_transposed : bool, optional (default=False) If True, stores the transpose of the adjacency matrix. Required for certain algorithms. + symmetrize: bool, optional (default=None) + If True, symmetrize the edge list for an undirected graph. Setting + this flag to True for a directed graph returns an error. The default + behavior symmetrizes the edges if the graph is undirected. This flag + cannot be set to True if the edgelist contains edge IDs or edge Types. + If the incoming edgelist is intended for an undirected graph and it is + known to be symmetric, this flag can be set to False to skip the + symmetrization step for better performance. Examples -------- @@ -268,7 +288,12 @@ def from_cudf_adjlist( raise RuntimeError("Graph is already initialized") elif self._Impl.edgelist is not None or self._Impl.adjlist is not None: raise RuntimeError("Graph already has values") - self._Impl._simpleGraphImpl__from_adjlist(offset_col, index_col, value_col) + self._Impl._simpleGraphImpl__from_adjlist( + offset_col=offset_col, + index_col=index_col, + value_col=value_col, + symmetrize=symmetrize, + ) def from_dask_cudf_edgelist( self, diff --git a/python/cugraph/cugraph/structure/graph_implementation/simpleDistributedGraph.py b/python/cugraph/cugraph/structure/graph_implementation/simpleDistributedGraph.py index 7f3f7e83e59..83dad234287 100644 --- a/python/cugraph/cugraph/structure/graph_implementation/simpleDistributedGraph.py +++ b/python/cugraph/cugraph/structure/graph_implementation/simpleDistributedGraph.py @@ -34,7 +34,6 @@ ) from cugraph.structure.number_map import NumberMap -from cugraph.structure.symmetrize import symmetrize from cugraph.dask.common.part_utils import ( persist_dask_df_equal_parts_per_worker, ) @@ -98,6 +97,7 @@ def _make_plc_graph( edge_id_type, edge_type_id, drop_multi_edges, + symmetrize, ): weights = None edge_ids = None @@ -151,6 +151,7 @@ def _make_plc_graph( else ([cudf.Series(dtype=edge_type_id)] if edge_type_id else None), num_arrays=num_arrays, store_transposed=store_transposed, + symmetrize=symmetrize, do_expensive_check=False, drop_multi_edges=drop_multi_edges, ) @@ -172,6 +173,7 @@ def __from_edgelist( renumber=True, store_transposed=False, legacy_renum_only=False, + symmetrize=None, ): if not isinstance(input_ddf, dask_cudf.DataFrame): raise TypeError("input should be a dask_cudf dataFrame") @@ -184,6 +186,35 @@ def __from_edgelist( ].dtype not in [np.int32, np.int64]: raise ValueError("set renumber to True for non integer columns ids") + if self.properties.directed and symmetrize: + raise ValueError( + "The edgelist can only be symmetrized for undirected graphs." + ) + + if self.properties.directed: + if symmetrize: + raise ValueError( + "The edgelist can only be symmetrized for undirected graphs." + ) + else: + if symmetrize or symmetrize is None: + unsupported = False + if edge_id is not None or edge_type is not None: + unsupported = True + if isinstance(edge_attr, list): + if len(edge_attr) > 1: + unsupported = True + if unsupported: + raise ValueError( + "Edge list containing Edge Ids or Types can't be symmetrized. " + "If the edges are already symmetric, set the 'symmetrize' " + "flag to False" + ) + + if symmetrize is None: + # default behavior + symmetrize = not self.properties.directed + s_col = source d_col = destination if not isinstance(s_col, list): @@ -266,27 +297,11 @@ def __from_edgelist( ddf_columns += value_col_names input_ddf = input_ddf[ddf_columns] - if len(value_col_names) == 0: - source_col, dest_col = symmetrize( - input_ddf, - source, - destination, - multi=True, # Deprecated parameter - symmetrize=not self.properties.directed, - ) - value_col = None - else: - source_col, dest_col, value_col = symmetrize( - input_ddf, - source, - destination, - value_col_names, - multi=True, # Deprecated parameter - symmetrize=not self.properties.directed, - ) - # Create a dask_cudf dataframe from the cudf series # or dataframe objects obtained from symmetrization + source_col = input_ddf[source] + dest_col = input_ddf[destination] + value_col = input_ddf[value_col_names] if isinstance(source_col, dask_cudf.Series): frames = [ source_col.to_frame(name=source), @@ -370,6 +385,7 @@ def __from_edgelist( self.edge_id_type, self.edge_type_id_type, not self.properties.multi_edge, + not self.properties.directed, ) for w, edata in persisted_keys_d.items() } diff --git a/python/cugraph/cugraph/structure/graph_implementation/simpleGraph.py b/python/cugraph/cugraph/structure/graph_implementation/simpleGraph.py index bc5cca67c2e..858b114ebdc 100644 --- a/python/cugraph/cugraph/structure/graph_implementation/simpleGraph.py +++ b/python/cugraph/cugraph/structure/graph_implementation/simpleGraph.py @@ -13,7 +13,7 @@ from cugraph.structure import graph_primtypes_wrapper from cugraph.structure.replicate_edgelist import replicate_cudf_dataframe -from cugraph.structure.symmetrize import symmetrize +from cugraph.structure.symmetrize import symmetrize as symmetrize_df from cugraph.structure.number_map import NumberMap import cugraph.dask.common.mg_utils as mg_utils import cudf @@ -134,6 +134,7 @@ def __from_edgelist( renumber=True, legacy_renum_only=False, store_transposed=False, + symmetrize=None, ): if legacy_renum_only: warning_msg = ( @@ -143,6 +144,35 @@ def __from_edgelist( warning_msg, ) + if self.properties.directed and symmetrize: + raise ValueError( + "The edgelist can only be symmetrized for undirected graphs." + ) + + if self.properties.directed: + if symmetrize: + raise ValueError( + "The edgelist can only be symmetrized for undirected graphs." + ) + else: + if symmetrize or symmetrize is None: + unsupported = False + if edge_id is not None or edge_type is not None: + unsupported = True + if isinstance(edge_attr, list): + if len(edge_attr) > 1: + unsupported = True + if unsupported: + raise ValueError( + "Edge list containing Edge Ids or Types can't be symmetrized. " + "If the edges are already symmetric, set the 'symmetrize' " + "flag to False" + ) + + if symmetrize is None: + # default behavior + symmetrize = not self.properties.directed + # Verify column names present in input DataFrame s_col = source d_col = destination @@ -264,45 +294,27 @@ def __from_edgelist( ) raise ValueError("set renumber to True for non integer columns ids") - # The dataframe will be symmetrized iff the graph is undirected - # otherwise the inital dataframe will be returned. Duplicated edges - # will be dropped unless the graph is a MultiGraph(Not Implemented yet) - # TODO: Update Symmetrize to work on Graph and/or DataFrame + # The dataframe will be symmetrized iff the graph is undirected with the + # symmetrize flag set to None or True otherwise, the inital dataframe will + # be returned. If set to False, the API will assume that the edges are already + # symmetric. Duplicated edges will be dropped unless the graph is a + # MultiGraph(Not Implemented yet) + if edge_attr is not None: - source_col, dest_col, value_col = symmetrize( - elist, - source, - destination, - edge_attr, - multi=self.properties.multi_edge, # Deprecated parameter - symmetrize=not self.properties.directed, - ) + value_col = { + self.edgeWeightCol: elist[weight] if weight in edge_attr else None, + self.edgeIdCol: elist[edge_id] if edge_id in edge_attr else None, + self.edgeTypeCol: elist[edge_type] if edge_type in edge_attr else None, + } - if isinstance(value_col, cudf.DataFrame): - value_dict = {} - for i in value_col.columns: - value_dict[i] = value_col[i] - value_col = value_dict else: value_col = None - source_col, dest_col = symmetrize( - elist, - source, - destination, - multi=self.properties.multi_edge, # Deprecated parameter - symmetrize=not self.properties.directed, - ) - - if isinstance(value_col, dict): - value_col = { - self.edgeWeightCol: value_col[weight] if weight in value_col else None, - self.edgeIdCol: value_col[edge_id] if edge_id in value_col else None, - self.edgeTypeCol: value_col[edge_type] - if edge_type in value_col - else None, - } - self.edgelist = simpleGraphImpl.EdgeList(source_col, dest_col, value_col) + # FIXME: if the user calls self.edgelist.edgelist_df after creating a + # symmetric graph, return the symmetric edgelist? + self.edgelist = simpleGraphImpl.EdgeList( + elist[source], elist[destination], value_col + ) if self.batch_enabled: self._replicate_edgelist() @@ -312,6 +324,7 @@ def __from_edgelist( store_transposed=store_transposed, renumber=renumber, drop_multi_edges=not self.properties.multi_edge, + symmetrize=symmetrize, ) def to_pandas_edgelist( @@ -549,13 +562,23 @@ def __from_adjlist( value_col=None, renumber=True, store_transposed=False, + symmetrize=None, ): self.adjlist = simpleGraphImpl.AdjList(offset_col, index_col, value_col) + + if self.properties.directed and symmetrize: + raise ValueError("The edges can only be symmetrized for undirected graphs.") + if value_col is not None: self.properties.weighted = True self._make_plc_graph( - value_col=value_col, store_transposed=store_transposed, renumber=renumber + value_col=value_col, + store_transposed=store_transposed, + renumber=renumber, + symmetrize=not self.properties.directed + if symmetrize is None + else symmetrize, ) if self.batch_enabled: @@ -1146,6 +1169,7 @@ def _make_plc_graph( store_transposed: bool = False, renumber: bool = True, drop_multi_edges: bool = False, + symmetrize: bool = False, ): """ Parameters @@ -1164,6 +1188,8 @@ def _make_plc_graph( int32 or int64 type. drop_multi_edges: bool (default=False) Whether to drop multi edges + symmetrize: bool (default=False) + Whether to symmetrize """ if value_col is None: @@ -1228,6 +1254,7 @@ def _make_plc_graph( do_expensive_check=True, input_array_format=input_array_format, drop_multi_edges=drop_multi_edges, + symmetrize=symmetrize, ) def to_directed(self, DiG, store_transposed=False): @@ -1253,12 +1280,18 @@ def to_directed(self, DiG, store_transposed=False): DiG._make_plc_graph(value_col, store_transposed) def to_undirected(self, G, store_transposed=False): + """ Return an undirected copy of the graph. Note: This will discard any edge ids or edge types but will preserve edge weights if present. """ + # FIXME: Update this function to not call the deprecated + # symmetrize function. + # 1) Import the C++ function that symmetrize a graph + # 2) decompress the edgelist to update 'simpleGraphImpl.EdgeList' + # Doesn't work for edgelists with edge_ids and edge_types. G.properties.renumbered = self.properties.renumbered G.renumber_map = self.renumber_map if self.properties.directed is False: @@ -1268,14 +1301,14 @@ def to_undirected(self, G, store_transposed=False): else: df = self.edgelist.edgelist_df if self.edgelist.weights: - source_col, dest_col, value_col = symmetrize( + source_col, dest_col, value_col = symmetrize_df( df, simpleGraphImpl.srcCol, simpleGraphImpl.dstCol, simpleGraphImpl.edgeWeightCol, ) else: - source_col, dest_col = symmetrize( + source_col, dest_col = symmetrize_df( df, simpleGraphImpl.srcCol, simpleGraphImpl.dstCol ) value_col = None @@ -1310,6 +1343,28 @@ def has_edge(self, u, v): v = tmp["id"][1] df = self.edgelist.edgelist_df + + if self.edgelist.weights: + # FIXME: Update this function to not call the deprecated + # symmetrize function. + source_col, dest_col, value_col = symmetrize_df( + df, + simpleGraphImpl.srcCol, + simpleGraphImpl.dstCol, + simpleGraphImpl.edgeWeightCol, + symmetrize=not self.properties.directed, + ) + else: + source_col, dest_col = symmetrize_df( + df, + simpleGraphImpl.srcCol, + simpleGraphImpl.dstCol, + symmetrize=not self.properties.directed, + ) + value_col = None + + self.edgelist = simpleGraphImpl.EdgeList(source_col, dest_col, value_col) + return ( (df[simpleGraphImpl.srcCol] == u) & (df[simpleGraphImpl.dstCol] == v) ).any() diff --git a/python/cugraph/cugraph/structure/hypergraph.py b/python/cugraph/cugraph/structure/hypergraph.py index bdc98333da0..55e6bbcca3d 100644 --- a/python/cugraph/cugraph/structure/hypergraph.py +++ b/python/cugraph/cugraph/structure/hypergraph.py @@ -37,6 +37,7 @@ import cudf import numpy as np from cugraph.structure.graph_classes import Graph +from cugraph.structure.symmetrize import symmetrize def hypergraph( @@ -277,6 +278,32 @@ def hypergraph( renumber=True, ) + df = cudf.DataFrame() + + # Need to refactor this code as it uses the + # deprecated symmetrize call. + if "weights" in graph.edgelist.edgelist_df: + source_col, dest_col, value_col = symmetrize( + graph.edgelist.edgelist_df, + "src", + "dst", + "weights", + symmetrize=not graph.is_directed(), + ) + + df["src"] = source_col + df["dst"] = dest_col + df["weights"] = value_col + else: + source_col, dest_col = symmetrize( + graph.edgelist.edgelist_df, "src", "dst", symmetrize=not graph.is_directed() + ) + + df["src"] = source_col + df["dst"] = dest_col + + graph.edgelist.edgelist_df = df + return { "nodes": nodes, "edges": edges, diff --git a/python/cugraph/cugraph/structure/property_graph.py b/python/cugraph/cugraph/structure/property_graph.py index 53c1bf778c7..5f55a15888a 100644 --- a/python/cugraph/cugraph/structure/property_graph.py +++ b/python/cugraph/cugraph/structure/property_graph.py @@ -15,6 +15,7 @@ import numpy as np import cugraph +from cugraph.structure.symmetrize import symmetrize from cugraph.utilities.utils import ( import_optional, MissingModule, @@ -2005,6 +2006,33 @@ def edge_props_to_graph( else: G.from_pandas_edgelist(edge_prop_df.reset_index(), **create_args) + # FIXME: Property_graph does not fully leverage the PLC API yet. + # It still relies on the edges being symmetrized by the deprecated + # symmetrize function. + + # Symmetrize the internal representation of the edgelists + + if edge_attr is not None: + source_col, dest_col, value_col = symmetrize( + G.edgelist.edgelist_df, + "src", + "dst", + "weights", + symmetrize=not G.is_directed(), + ) + else: + source_col, dest_col = symmetrize( + G.edgelist.edgelist_df, "src", "dst", symmetrize=not G.is_directed() + ) + + renumbered_edge_prop_df = cudf.DataFrame() + renumbered_edge_prop_df["src"] = source_col + renumbered_edge_prop_df["dst"] = dest_col + if edge_attr: + renumbered_edge_prop_df["weights"] = value_col + + G.edgelist.edgelist_df = renumbered_edge_prop_df + if add_edge_data: # Set the edge_data on the resulting Graph to a DataFrame # containing the edges and the edge ID for each. Edge IDs are diff --git a/python/cugraph/cugraph/structure/symmetrize.py b/python/cugraph/cugraph/structure/symmetrize.py index 3e46d81b6ff..b59661b1cd4 100644 --- a/python/cugraph/cugraph/structure/symmetrize.py +++ b/python/cugraph/cugraph/structure/symmetrize.py @@ -257,6 +257,11 @@ def symmetrize( >>> df['values'] = cudf.Series(M['2']) >>> src, dst, val = symmetrize(df, 'sources', 'destinations', 'values', multi=True) """ + warnings.warn( + "This method is deprecated and will no longer be supported. The symmetrization " + "of the edges are only supported by setting the 'symmetrize' flag to 'True'", + FutureWarning, + ) # FIXME: Redundant check that should be done at the graph creation if "edge_id" in input_df.columns and symmetrize: diff --git a/python/cugraph/cugraph/tests/sampling/test_random_walks_mg.py b/python/cugraph/cugraph/tests/sampling/test_random_walks_mg.py index 2db3c6f5907..34eeb2902f8 100644 --- a/python/cugraph/cugraph/tests/sampling/test_random_walks_mg.py +++ b/python/cugraph/cugraph/tests/sampling/test_random_walks_mg.py @@ -19,8 +19,10 @@ import cugraph import dask_cudf import cugraph.dask as dcg +import cudf from cugraph.testing import SMALL_DATASETS from cugraph.datasets import karate_asymmetric +from cugraph.structure.symmetrize import symmetrize from pylibcugraph.testing.utils import gen_fixture_params_product @@ -205,4 +207,15 @@ def input_graph(request): def test_dask_mg_random_walks(dask_client, input_graph): path_data, seeds, max_depth = calc_random_walks(input_graph) df_G = input_graph.input_df.compute().reset_index(drop=True) - check_random_walks(input_graph, path_data, seeds, max_depth, df_G) + + # FIXME: leverages the deprecated symmetrize call + source_col, dest_col, value_col = symmetrize( + df_G, "src", "dst", "value", symmetrize=not input_graph.is_directed() + ) + + df = cudf.DataFrame() + df["src"] = source_col + df["dst"] = dest_col + df["value"] = value_col + + check_random_walks(input_graph, path_data, seeds, max_depth, df) diff --git a/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample.py b/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample.py index 304ead6fea9..ad0dbe77f7d 100644 --- a/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample.py +++ b/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample.py @@ -21,6 +21,7 @@ from cugraph import uniform_neighbor_sample from cugraph.testing import UNDIRECTED_DATASETS from cugraph.datasets import email_Eu_core, small_tree +from cugraph.structure.symmetrize import symmetrize from pylibcugraph.testing.utils import gen_fixture_params_product @@ -148,6 +149,15 @@ def test_uniform_neighbor_sample_simple(input_combo): # should be 'None' if the datasets was never renumbered input_df = G.edgelist.edgelist_df + # FIXME: Uses the deprecated implementation of symmetrize. + source_col, dest_col = symmetrize( + input_df, "src", "dst", symmetrize=not G.is_directed() + ) + + input_df = cudf.DataFrame() + input_df["src"] = source_col + input_df["dst"] = dest_col + result_nbr = uniform_neighbor_sample( G, input_combo["start_list"], @@ -235,6 +245,19 @@ def test_uniform_neighbor_sample_tree(directed): G = cugraph.Graph(directed=directed) G.from_cudf_edgelist(df, "src", "dst", "value") + # FIXME: Uses the deprecated implementation of symmetrize. + source_col, dest_col, value_col = symmetrize( + G.edgelist.edgelist_df, "src", "dst", "weights", symmetrize=not G.is_directed() + ) + + # Retrieve the input dataframe. + # input_df != df if 'directed = False' because df will be symmetrized + # internally. + input_df = cudf.DataFrame() + input_df["src"] = source_col + input_df["dst"] = dest_col + input_df["value"] = value_col + # # Make sure the old C++ renumbering was skipped because: # 1) Pylibcugraph already does renumbering @@ -245,11 +268,6 @@ def test_uniform_neighbor_sample_tree(directed): assert G.renumbered is False - # Retrieve the input dataframe. - # input_df != df if 'directed = False' because df will be symmetrized - # internally. - input_df = G.edgelist.edgelist_df - # TODO: Incomplete, include more testing for tree graph as well as # for larger graphs start_list = cudf.Series([0, 0], dtype="int32") diff --git a/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample_mg.py b/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample_mg.py index c65535f98a2..4a85b49a66e 100644 --- a/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample_mg.py +++ b/python/cugraph/cugraph/tests/sampling/test_uniform_neighbor_sample_mg.py @@ -27,6 +27,7 @@ from cugraph.dask import uniform_neighbor_sample from cugraph.dask.common.mg_utils import is_single_gpu from cugraph.structure.symmetrize import _memory_efficient_drop_duplicates +from cugraph.structure.symmetrize import symmetrize_ddf from cugraph.datasets import email_Eu_core, small_tree from pylibcugraph.testing.utils import gen_fixture_params_product @@ -144,6 +145,10 @@ def test_mg_uniform_neighbor_sample_simple(dask_client, input_combo): input_df, vertex_col_name, len(workers) ) + input_df = symmetrize_ddf( + input_df, src_name="src", dst_name="dst", symmetrize=not dg.is_directed() + ) + result_nbr = uniform_neighbor_sample( dg, input_combo["start_list"], @@ -247,6 +252,11 @@ def test_mg_uniform_neighbor_sample_tree(dask_client, directed): # input_df != ddf if 'directed = False' because ddf will be symmetrized # internally. input_df = G.input_df + + input_df = symmetrize_ddf( + input_df, src_name="src", dst_name="dst", symmetrize=not G.is_directed() + ) + join = result_nbr.merge( input_df, left_on=[*result_nbr.columns[:2]], right_on=[*input_df.columns[:2]] ) diff --git a/python/cugraph/cugraph/tests/structure/test_graph.py b/python/cugraph/cugraph/tests/structure/test_graph.py index c0524fcfe77..48a0b257b12 100644 --- a/python/cugraph/cugraph/tests/structure/test_graph.py +++ b/python/cugraph/cugraph/tests/structure/test_graph.py @@ -25,6 +25,7 @@ from cugraph.testing import utils from cudf.testing import assert_series_equal from cudf.testing.testing import assert_frame_equal +from cugraph.structure.symmetrize import symmetrize # MG import dask_cudf @@ -534,6 +535,18 @@ def test_to_directed(graph_file): # cugraph add_edge_list G = cugraph.Graph() G.from_cudf_edgelist(cu_M, source="0", destination="1") + + # FIXME: Uses the deprecated implementation of symmetrize. + source_col, dest_col = symmetrize( + G.edgelist.edgelist_df, "src", "dst", symmetrize=not G.is_directed() + ) + + input_df = cudf.DataFrame() + input_df["src"] = source_col + input_df["dst"] = dest_col + + G.edgelist.edgelist_df = input_df + Gnx = nx.from_pandas_edgelist(M, source="0", target="1", create_using=nx.Graph()) DiG = G.to_directed() diff --git a/python/cugraph/cugraph/tests/structure/test_multigraph.py b/python/cugraph/cugraph/tests/structure/test_multigraph.py index a9ea617fdb8..e245894b479 100644 --- a/python/cugraph/cugraph/tests/structure/test_multigraph.py +++ b/python/cugraph/cugraph/tests/structure/test_multigraph.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -76,7 +76,7 @@ def test_Graph_from_MultiGraph(graph_file): G = cugraph.Graph(GM) Gnx = nx.Graph(GnxM) - assert Gnx.number_of_edges() == G.number_of_edges() + assert Gnx.number_of_edges() == G.number_of_edges(directed_edges=True) GdM = graph_file.get_graph(create_using=cugraph.MultiGraph(directed=True)) GnxdM = nx.from_pandas_edgelist( nxM, diff --git a/python/cugraph/cugraph/tests/utils/test_dataset.py b/python/cugraph/cugraph/tests/utils/test_dataset.py index a52b99dabfe..3873cd1c3e4 100644 --- a/python/cugraph/cugraph/tests/utils/test_dataset.py +++ b/python/cugraph/cugraph/tests/utils/test_dataset.py @@ -26,6 +26,7 @@ from cugraph.dask.common.mg_utils import is_single_gpu from cugraph.datasets import karate from cugraph.structure import Graph +from cugraph.structure.symmetrize import symmetrize from cugraph.testing import ( RAPIDS_DATASET_ROOT_DIR_PATH, ALL_DATASETS, @@ -379,6 +380,29 @@ def test_node_and_edge_count(dataset): download=True, create_using=Graph(directed=dataset_is_directed) ) + df = cudf.DataFrame() + if "weights" in G.edgelist.edgelist_df: + source_col, dest_col, value_col = symmetrize( + G.edgelist.edgelist_df, + "src", + "dst", + "weights", + symmetrize=not G.is_directed(), + ) + + df["src"] = source_col + df["dst"] = dest_col + df["weights"] = value_col + else: + source_col, dest_col = symmetrize( + G.edgelist.edgelist_df, "src", "dst", symmetrize=not G.is_directed() + ) + + df["src"] = source_col + df["dst"] = dest_col + + G.edgelist.edgelist_df = df + assert G.number_of_nodes() == dataset.metadata["number_of_nodes"] assert G.number_of_edges() == dataset.metadata["number_of_edges"] diff --git a/python/cugraph/cugraph/utilities/nx_factory.py b/python/cugraph/cugraph/utilities/nx_factory.py index d07d17978d7..794fb33a7a1 100644 --- a/python/cugraph/cugraph/utilities/nx_factory.py +++ b/python/cugraph/cugraph/utilities/nx_factory.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# Copyright (c) 2020-2024, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -24,6 +24,8 @@ from cudf import from_pandas from cudf.api.types import is_integer_dtype +from cugraph.structure.symmetrize import symmetrize + # nx will be a MissingModule instance if NetworkX is not installed (any # attribute access on a MissingModule instance results in a RuntimeError). nx = import_optional("networkx") @@ -129,6 +131,17 @@ def convert_from_nx( if is_weighted is False: _gdf = convert_unweighted_to_gdf(nxG, vertex_type) + # FIXME: The legacy algorithms do not support the PLC graph + # hence, the symmetrization cannot be performed at the graph + # creation. Use the deprecated 'symmetrize' function for now. + source_col, dest_col = symmetrize( + _gdf, "src", "dst", symmetrize=not G.is_directed() + ) + + _gdf = cudf.DataFrame() + + _gdf["src"] = source_col + _gdf["dst"] = dest_col G.from_cudf_edgelist( _gdf, source="src", @@ -140,6 +153,18 @@ def convert_from_nx( else: if weight is None: _gdf = convert_weighted_unnamed_to_gdf(nxG, vertex_type) + # FIXME: The legacy algorithms do not support the PLC graph + # hence, the symmetrization cannot be performed at the graph + # creation. Use the deprecated 'symmetrize' function for now. + source_col, dest_col, value_col = symmetrize( + _gdf, "src", "target", "weight", symmetrize=not G.is_directed() + ) + + _gdf = cudf.DataFrame() + + _gdf["src"] = source_col + _gdf["target"] = dest_col + _gdf["weight"] = value_col G.from_cudf_edgelist( _gdf, source="source", @@ -148,8 +173,22 @@ def convert_from_nx( renumber=do_renumber, store_transposed=store_transposed, ) + else: _gdf = convert_weighted_named_to_gdf(nxG, weight, vertex_type) + # FIXME: The legacy algorithms do not support the PLC graph + # hence, the symmetrization cannot be performed at the graph + # creation. Use the deprecated 'symmetrize' function for now. + source_col, dest_col, value_col = symmetrize( + _gdf, "src", "dst", "weight", symmetrize=not G.is_directed() + ) + + _gdf = cudf.DataFrame() + + _gdf["src"] = source_col + _gdf["dst"] = dest_col + _gdf["weight"] = value_col + G.from_cudf_edgelist( _gdf, source="src", diff --git a/python/cugraph/pytest.ini b/python/cugraph/pytest.ini index bca148538d9..2f01a0cc51b 100644 --- a/python/cugraph/pytest.ini +++ b/python/cugraph/pytest.ini @@ -71,3 +71,4 @@ filterwarnings = ignore:This function is deprecated. Batched support for multiple vertices:DeprecationWarning # Called via dask. Not obviously addressable in cugraph. ignore:The behavior of array concatenation with empty entries is deprecated:FutureWarning + ignore:This method is deprecated and will no longer be supported. The symmetrization:FutureWarning diff --git a/python/pylibcugraph/pylibcugraph/_cugraph_c/graph.pxd b/python/pylibcugraph/pylibcugraph/_cugraph_c/graph.pxd index 4247bcc1b2a..497607860bd 100644 --- a/python/pylibcugraph/pylibcugraph/_cugraph_c/graph.pxd +++ b/python/pylibcugraph/pylibcugraph/_cugraph_c/graph.pxd @@ -67,6 +67,7 @@ cdef extern from "cugraph_c/graph.h": bool_t renumber, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t check, cugraph_graph_t** graph, cugraph_error_t** error) @@ -117,6 +118,7 @@ cdef extern from "cugraph_c/graph.h": const cugraph_type_erased_device_array_view_t* edge_type_ids, bool_t store_transposed, bool_t renumber, + bool_t symmetrize, bool_t check, cugraph_graph_t** graph, cugraph_error_t** error @@ -173,6 +175,7 @@ cdef extern from "cugraph_c/graph.h": size_t num_arrays, bool_t drop_self_loops, bool_t drop_multi_edges, + bool_t symmetrize, bool_t do_expensive_check, cugraph_graph_t** graph, cugraph_error_t** error) diff --git a/python/pylibcugraph/pylibcugraph/graphs.pyx b/python/pylibcugraph/pylibcugraph/graphs.pyx index def47390ce5..6eda0a83d3e 100644 --- a/python/pylibcugraph/pylibcugraph/graphs.pyx +++ b/python/pylibcugraph/pylibcugraph/graphs.pyx @@ -123,9 +123,17 @@ cdef class SGGraph(_GPUGraph): drop_self_loops : bool, optional (default='False') If true, drop any self loops that exist in the provided edge list. + Not supported for CSR graph. + drop_multi_edges: bool, optional (default='False') If true, drop any multi edges that exist in the provided edge list + Not supported for CSR graph. + + symmetrize: bool, optional (default='False') + If true, symmetrize the edge list + + Examples --------- >>> import pylibcugraph, cupy, numpy @@ -155,7 +163,8 @@ cdef class SGGraph(_GPUGraph): input_array_format="COO", vertices_array=None, drop_self_loops=False, - drop_multi_edges=False): + drop_multi_edges=False, + symmetrize=False): # FIXME: add tests for these if not(isinstance(store_transposed, (int, bool))): @@ -217,6 +226,7 @@ cdef class SGGraph(_GPUGraph): renumber, drop_self_loops, drop_multi_edges, + symmetrize, do_expensive_check, &(self.c_graph_ptr), &error_ptr) @@ -234,6 +244,7 @@ cdef class SGGraph(_GPUGraph): edge_type_view_ptr, store_transposed, renumber, + symmetrize, # drop_self_loops, #FIXME: Not supported yet # drop_multi_edges, #FIXME: Not supported yet do_expensive_check, @@ -325,6 +336,10 @@ cdef class MGGraph(_GPUGraph): drop_multi_edges: bool, optional (default='False') If true, drop any multi edges that exist in the provided edge list + + symmetrize: bool, optional (default='False') + If true, symmetrize the edge list + """ def __cinit__(self, ResourceHandle resource_handle, @@ -339,7 +354,8 @@ cdef class MGGraph(_GPUGraph): vertices_array=None, size_t num_arrays=1, # default value to not break users drop_self_loops=False, - drop_multi_edges=False): + drop_multi_edges=False, + symmetrize=False): if not(isinstance(store_transposed, (int, bool))): raise TypeError("expected int or bool for store_transposed, got " @@ -465,6 +481,7 @@ cdef class MGGraph(_GPUGraph): num_arrays, drop_self_loops, drop_multi_edges, + symmetrize, do_expensive_check, &(self.c_graph_ptr), &error_ptr)