diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index f6e5e3aa570..681d3171c9c 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -95,6 +95,7 @@ "is_connected", "is_forest", "is_isolate", + "is_negatively_weighted", "is_tree", "is_weakly_connected", "isolates", diff --git a/python/nx-cugraph/nx_cugraph/classes/function.py b/python/nx-cugraph/nx_cugraph/classes/function.py index 7212a4d2da9..3dac6c2fd08 100644 --- a/python/nx-cugraph/nx_cugraph/classes/function.py +++ b/python/nx-cugraph/nx_cugraph/classes/function.py @@ -11,11 +11,31 @@ # See the License for the specific language governing permissions and # limitations under the License. import cupy as cp +import networkx as nx from nx_cugraph.convert import _to_graph from nx_cugraph.utils import networkx_algorithm -__all__ = ["number_of_selfloops"] +__all__ = [ + "is_negatively_weighted", + "number_of_selfloops", +] + + +@networkx_algorithm(version_added="24.04") +def is_negatively_weighted(G, edge=None, weight="weight"): + G = _to_graph(G, weight) + if edge is not None: + data = G.get_edge_data(*edge) + if data is None: + raise nx.NetworkXError(f"Edge {edge!r} does not exist.") + return weight in data and data[weight] < 0 + if weight not in G.edge_values: + return False + edge_vals = G.edge_values[weight] + if weight in G.edge_masks: + edge_vals = edge_vals[G.edge_masks[weight]] + return bool((edge_vals < 0).any()) @networkx_algorithm(version_added="23.12") diff --git a/python/nx-cugraph/nx_cugraph/tests/test_classes_function.py b/python/nx-cugraph/nx_cugraph/tests/test_classes_function.py new file mode 100644 index 00000000000..d6152f650fb --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/tests/test_classes_function.py @@ -0,0 +1,35 @@ +# Copyright (c) 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 +# +# 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. +"""Test functions from nx_cugraph/classes/function.py""" +import networkx as nx + +import nx_cugraph as nxcg + + +def test_is_negatively_weighted(): + Gnx = nx.MultiGraph() + Gnx.add_edge(0, 1, 2, weight=-3) + Gnx.add_edge(2, 3, foo=3) + Gcg = nxcg.from_networkx(Gnx, preserve_edge_attrs=True) + assert nx.is_negatively_weighted(Gnx) + assert nxcg.is_negatively_weighted(Gnx) + assert nxcg.is_negatively_weighted(Gcg) + assert not nx.is_negatively_weighted(Gnx, weight="foo") + assert not nxcg.is_negatively_weighted(Gcg, weight="foo") + assert not nx.is_negatively_weighted(Gnx, weight="bar") + assert not nxcg.is_negatively_weighted(Gcg, weight="bar") + assert nx.is_negatively_weighted(Gnx, (0, 1, 2)) + assert nxcg.is_negatively_weighted(Gcg, (0, 1, 2)) + assert nx.is_negatively_weighted(Gnx, (0, 1)) == nxcg.is_negatively_weighted( + Gcg, (0, 1) + ) diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index 2ff53c1a3f6..c662e710fee 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -175,6 +175,7 @@ ignore = [ # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) # "TRY004", # Prefer `TypeError` exception for invalid type (Note: good advice, but not worth the nuisance) "B904", # Bare `raise` inside exception clause (like TRY200; sometimes okay) + "S310", # Audit URL open for permitted schemes (Note: we don't download URLs in normal usage) # Intentionally ignored "A003", # Class attribute ... is shadowing a python builtin @@ -232,6 +233,7 @@ ignore = [ "nx_cugraph/**/tests/*py" = ["S101", "S311", "T201", "D103", "D100"] "_nx_cugraph/__init__.py" = ["E501"] "nx_cugraph/algorithms/**/*py" = ["D205", "D401"] # Allow flexible docstrings for algorithms +"scripts/update_readme.py" = ["INP001"] # Not part of a package [tool.ruff.lint.flake8-annotations] mypy-init-return = true diff --git a/python/nx-cugraph/scripts/update_readme.py b/python/nx-cugraph/scripts/update_readme.py index fcaa1769d8b..5ee49e63ac8 100755 --- a/python/nx-cugraph/scripts/update_readme.py +++ b/python/nx-cugraph/scripts/update_readme.py @@ -70,6 +70,9 @@ def replace_body(text, match, new_body): "https://networkx.org/documentation/stable/reference/" "algorithms/component.html#weak-connectivity" ), + "networkx.classes": ( + "https://networkx.org/documentation/stable/reference/classes/index.html" + ), } @@ -193,9 +196,9 @@ def get_payload_internal(keys): def find_or_download_objs_file(objs_file_dir): - """ - Returns the path to /objects.inv, downloading it from - _objs_file_url if it does not already exist. + """Return the path to /objects.inv and download it if necessary. + + Download objects.inv from _objs_file_url if it does not already exist. """ objs_file_path = objs_file_dir / "objects.inv" if not objs_file_path.exists():