Skip to content

Commit

Permalink
nx-cugraph: add k_truss and degree centralities
Browse files Browse the repository at this point in the history
New algorithms:
  - `degree_centrality`
  - `in_degree_centrality`
  - `k_truss`
  - `number_of_selfloops`
  - `out_degree_centrality

Also, rename `row_indices, col_indices` to `src_indices, dst_indices`
  • Loading branch information
eriknw committed Oct 19, 2023
1 parent 0fd5883 commit 786c1e2
Show file tree
Hide file tree
Showing 17 changed files with 325 additions and 125 deletions.
5 changes: 5 additions & 0 deletions python/nx-cugraph/_nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@
"functions": {
# BEGIN: functions
"betweenness_centrality",
"degree_centrality",
"edge_betweenness_centrality",
"in_degree_centrality",
"is_isolate",
"isolates",
"k_truss",
"louvain_communities",
"number_of_isolates",
"number_of_selfloops",
"out_degree_centrality",
# END: functions
},
"extra_docstrings": {
Expand Down
6 changes: 3 additions & 3 deletions python/nx-cugraph/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ repos:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 23.10.0
hooks:
- id: black
# - id: black-jupyter
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.292
rev: v0.1.1
hooks:
- id: ruff
args: [--fix-only, --show-fixes]
Expand All @@ -77,7 +77,7 @@ repos:
additional_dependencies: [tomli]
files: ^(nx_cugraph|docs)/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.292
rev: v0.1.1
hooks:
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
1 change: 1 addition & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
# limitations under the License.
from . import centrality, community
from .centrality import *
from .core import *
from .isolate import *
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .betweenness import *
from .degree_alg import *
48 changes: 48 additions & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/centrality/degree_alg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) 2023, 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nx_cugraph.convert import _to_directed_graph, _to_graph
from nx_cugraph.utils import networkx_algorithm, not_implemented_for

__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]


@networkx_algorithm
def degree_centrality(G):
G = _to_graph(G)
if len(G) <= 1:
return dict.fromkeys(G, 1)
deg = G._degrees_array()
centrality = deg * (1 / (len(G) - 1))
return G._nodearray_to_dict(centrality)


@not_implemented_for("undirected")
@networkx_algorithm
def in_degree_centrality(G):
G = _to_directed_graph(G)
if len(G) <= 1:
return dict.fromkeys(G, 1)
deg = G._in_degrees_array()
centrality = deg * (1 / (len(G) - 1))
return G._nodearray_to_dict(centrality)


@not_implemented_for("undirected")
@networkx_algorithm
def out_degree_centrality(G):
G = _to_directed_graph(G)
if len(G) <= 1:
return dict.fromkeys(G, 1)
deg = G._out_degrees_array()
centrality = deg * (1 / (len(G) - 1))
return G._nodearray_to_dict(centrality)
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def louvain_communities(
# NetworkX allows both directed and undirected, but cugraph only allows undirected.
seed = _seed_to_int(seed) # Unused, but ensure it's valid for future compatibility
G = _to_undirected_graph(G, weight)
if G.row_indices.size == 0:
if G.src_indices.size == 0:
# TODO: PLC doesn't handle empty graphs gracefully!
return [{key} for key in G._nodeiter_to_iter(range(len(G)))]
if max_level is None:
Expand Down
91 changes: 91 additions & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (c) 2023, 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import cupy as cp
import networkx as nx
import numpy as np
import pylibcugraph as plc

import nx_cugraph as nxcg
from nx_cugraph.utils import networkx_algorithm, not_implemented_for

__all__ = ["k_truss"]


@not_implemented_for("directed")
@not_implemented_for("multigraph")
@networkx_algorithm
def k_truss(G, k):
if is_nx := isinstance(G, nx.Graph):
G = nxcg.from_networkx(G, preserve_all_attrs=True)
if nxcg.number_of_selfloops(G) > 0:
raise nx.NetworkXError(
"Input graph has self loops which is not permitted; "
"Consider using G.remove_edges_from(nx.selfloop_edges(G))."
)
# TODO: create renumbering helper function(s)
if k < 3:
# Drop nodes with zero degree
degrees = G._degrees_array()
# Renumber step 0: node indices
node_indices = degrees.nonzero()[0]
if degrees.size == node_indices.size:
# No change
return G if is_nx else G.copy()
src_indices = G.src_indices
dst_indices = G.dst_indices
# Renumber step 1: edge values (no changes needed)
edge_values = {key: val.copy() for key, val in G.edge_values.items()}
edge_masks = {key: val.copy() for key, val in G.edge_masks.items()}
else:
# int dtype for edge_indices would be preferred
edge_indices = cp.arange(G.src_indices.size, dtype=np.float64)
src_indices, dst_indices, edge_indices, _ = plc.k_truss_subgraph(
resource_handle=plc.ResourceHandle(),
graph=G._get_plc_graph(edge_array=edge_indices),
k=k,
do_expensive_check=False,
)
# Renumber step 0: node indices
node_indices = cp.unique(cp.concatenate([src_indices, dst_indices]))
# Renumber step 1: edge values
edge_indices = edge_indices.astype(np.int64)
edge_values = {key: val[edge_indices] for key, val in G.edge_values.items()}
edge_masks = {key: val[edge_indices] for key, val in G.edge_masks.items()}
# Renumber step 2: edge indices
mapper = cp.zeros(len(G), src_indices.dtype)
mapper[node_indices] = cp.arange(node_indices.size, dtype=np.int64)
src_indices = mapper[src_indices]
dst_indices = mapper[dst_indices]
# Renumber step 3: node values
node_values = {key: val[node_indices] for key, val in G.node_values.items()}
node_masks = {key: val[node_indices] for key, val in G.node_masks.items()}
# Renumber step 4: key_to_id
if (id_to_key := G.id_to_key) is not None:
key_to_id = {
id_to_key[old_index]: new_index
for new_index, old_index in enumerate(node_indices.tolist())
}
else:
key_to_id = None
new_graph = G.__class__.from_coo(
node_indices.size,
src_indices,
dst_indices,
edge_values,
edge_masks,
node_values,
node_masks,
key_to_id=key_to_id,
)
new_graph.graph.update(G.graph)
return new_graph
8 changes: 4 additions & 4 deletions python/nx-cugraph/nx_cugraph/algorithms/isolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ def is_isolate(G, n):
G = _to_graph(G)
index = n if G.key_to_id is None else G.key_to_id[n]
return not (
(G.row_indices == index).any().tolist()
(G.src_indices == index).any().tolist()
or G.is_directed()
and (G.col_indices == index).any().tolist()
and (G.dst_indices == index).any().tolist()
)


def _mark_isolates(G) -> cp.ndarray[bool]:
"""Return a boolean mask array indicating indices of isolated nodes."""
mark_isolates = cp.ones(len(G), bool)
mark_isolates[G.row_indices] = False
mark_isolates[G.src_indices] = False
if G.is_directed():
mark_isolates[G.col_indices] = False
mark_isolates[G.dst_indices] = False
return mark_isolates


Expand Down
5 changes: 3 additions & 2 deletions python/nx-cugraph/nx_cugraph/classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .graph import Graph
from .digraph import DiGraph
from .multigraph import MultiGraph
from .multidigraph import MultiDiGraph

from .digraph import DiGraph # isort:skip
from .multidigraph import MultiDiGraph # isort:skip
from .function import *
13 changes: 12 additions & 1 deletion python/nx-cugraph/nx_cugraph/classes/digraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from typing import TYPE_CHECKING

import cupy as cp
import networkx as nx

import nx_cugraph as nxcg
Expand Down Expand Up @@ -48,7 +49,7 @@ def number_of_edges(
) -> int:
if u is not None or v is not None:
raise NotImplementedError
return self.row_indices.size
return self.src_indices.size

##########################
# NetworkX graph methods #
Expand All @@ -59,3 +60,13 @@ def reverse(self, copy: bool = True) -> DiGraph:
return self._copy(not copy, self.__class__, reverse=True)

# Many more methods to implement...

###################
# Private methods #
###################

def _in_degrees_array(self):
return cp.bincount(self.dst_indices, minlength=self._N)

def _out_degrees_array(self):
return cp.bincount(self.src_indices, minlength=self._N)
23 changes: 23 additions & 0 deletions python/nx-cugraph/nx_cugraph/classes/function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2023, 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nx_cugraph.convert import _to_graph
from nx_cugraph.utils import networkx_algorithm

__all__ = ["number_of_selfloops"]


@networkx_algorithm
def number_of_selfloops(G):
G = _to_graph(G)
is_selfloop = G.src_indices == G.dst_indices
return is_selfloop.sum().tolist()
Loading

0 comments on commit 786c1e2

Please sign in to comment.