Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Option to change parallel edge behavior in adjacency_matrix functions #899

Merged
merged 16 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions releasenotes/notes/expand-adjacency-matrix-11e56c1f49b8e4e5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
features:
- |
The functions :func:`~rustworkx.graph_adjacency_matrix` and :func:`~rustworkx.digraph_adjacency_matrix` now have the option to adjust parallel edge behavior.
Instead of just the default sum behavior, the value in the output matrix can be the minimum ("min"), maximum ("max"), or average ("avg") of the weights of the parallel edges.
For example:

.. jupyter-execute::

import rustworkx as rx
graph = rx.PyGraph()
a = graph.add_node("A")
b = graph.add_node("B")
c = graph.add_node("C")

graph.add_edges_from([
(a, b, 3.0),
(a, b, 1.0),
(a, c, 2.0),
(b, c, 7.0),
(c, a, 1.0),
(b, c, 2.0),
(a, b, 4.0)
])

print("Adjacency Matrix with Summed Parallel Edges")
print(rx.graph_adjacency_matrix(graph, weight_fn= lambda x: float(x)))
print("Adjacency Matrix with Averaged Parallel Edges")
print(rx.graph_adjacency_matrix(graph, weight_fn= lambda x: float(x), parallel_edge="avg"))



86 changes: 77 additions & 9 deletions src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult<bool> {
/// Return the adjacency matrix for a PyDiGraph object
///
/// In the case where there are multiple edges between nodes the value in the
/// output matrix will be the sum of the edges' weights.
/// output matrix will be assigned based on a given parameter. Currently, the minimum, maximum, average, and default sum are supported.
///
/// :param PyDiGraph graph: The DiGraph used to generate the adjacency matrix
/// from
Expand All @@ -290,29 +290,60 @@ pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult<bool> {
/// value. This is the default value in the output matrix and it is used
/// to indicate the absence of an edge between 2 nodes. By default this is
/// ``0.0``.
/// :param String parallel_edge: Optional argument that determines how the function handles parallel edges.
/// ``"min"`` causes the value in the output matrix to be the minimum of the edges' weights, and similar behavior can be expected for ``"max"`` and ``"avg"``.
/// The function defaults to ``"sum"`` behavior, where the value in the output matrix is the sum of all parallel edge weights.
///
/// :return: The adjacency matrix for the input directed graph as a numpy array
/// :rtype: numpy.ndarray
#[pyfunction]
#[pyo3(
signature=(graph, weight_fn=None, default_weight=1.0, null_value=0.0),
text_signature = "(graph, /, weight_fn=None, default_weight=1.0, null_value=0.0)"
signature=(graph, weight_fn=None, default_weight=1.0, null_value=0.0, parallel_edge="sum"),
text_signature = "(graph, /, weight_fn=None, default_weight=1.0, null_value=0.0, parallel_edge=\"sum\")"
)]
pub fn digraph_adjacency_matrix(
py: Python,
graph: &digraph::PyDiGraph,
weight_fn: Option<PyObject>,
default_weight: f64,
null_value: f64,
parallel_edge: &str,
) -> PyResult<PyObject> {
let n = graph.node_count();
let mut matrix = Array2::<f64>::from_elem((n, n), null_value);
let mut parallel_edge_count = HashMap::new();
for (i, j, weight) in get_edge_iter_with_weights(&graph.graph) {
let edge_weight = weight_callable(py, &weight_fn, &weight, default_weight)?;
if matrix[[i, j]] == null_value || (null_value.is_nan() && matrix[[i, j]].is_nan()) {
matrix[[i, j]] = edge_weight;
} else {
matrix[[i, j]] += edge_weight;
match parallel_edge {
"sum" => {
matrix[[i, j]] += edge_weight;
}
"min" => {
let weight_min = matrix[[i, j]].min(edge_weight);
matrix[[i, j]] = weight_min;
}
"max" => {
let weight_max = matrix[[i, j]].max(edge_weight);
matrix[[i, j]] = weight_max;
}
"avg" => {
if parallel_edge_count.contains_key(&[i, j]) {
matrix[[i, j]] = (matrix[[i, j]] * parallel_edge_count[&[i, j]] as f64
+ edge_weight)
/ ((parallel_edge_count[&[i, j]] + 1) as f64);
*parallel_edge_count.get_mut(&[i, j]).unwrap() += 1;
} else {
parallel_edge_count.insert([i, j], 2);
matrix[[i, j]] = (matrix[[i, j]] + edge_weight) / 2.0;
}
}
_ => {
return Err(PyValueError::new_err("Parallel edges can currently only be dealt with using \"sum\", \"min\", \"max\", or \"avg\"."));
}
}
}
}
Ok(matrix.into_pyarray(py).into())
Expand All @@ -321,7 +352,7 @@ pub fn digraph_adjacency_matrix(
/// Return the adjacency matrix for a PyGraph class
///
/// In the case where there are multiple edges between nodes the value in the
/// output matrix will be the sum of the edges' weights.
/// output matrix will be assigned based on a given parameter. Currently, the minimum, maximum, average, and default sum are supported.
///
/// :param PyGraph graph: The graph used to generate the adjacency matrix from
/// :param weight_fn: A callable object (function, lambda, etc) which
Expand All @@ -344,31 +375,68 @@ pub fn digraph_adjacency_matrix(
/// value. This is the default value in the output matrix and it is used
/// to indicate the absence of an edge between 2 nodes. By default this is
/// ``0.0``.
/// :param String parallel_edge: Optional argument that determines how the function handles parallel edges.
/// ``"min"`` causes the value in the output matrix to be the minimum of the edges' weights, and similar behavior can be expected for ``"max"`` and ``"avg"``.
/// The function defaults to ``"sum"`` behavior, where the value in the output matrix is the sum of all parallel edge weights.
///
/// :return: The adjacency matrix for the input graph as a numpy array
/// :rtype: numpy.ndarray
#[pyfunction]
#[pyo3(
signature=(graph, weight_fn=None, default_weight=1.0, null_value=0.0),
text_signature = "(graph, /, weight_fn=None, default_weight=1.0, null_value=0.0)"
signature=(graph, weight_fn=None, default_weight=1.0, null_value=0.0, parallel_edge="sum"),
text_signature = "(graph, /, weight_fn=None, default_weight=1.0, null_value=0.0, parallel_edge=\"sum\")"
)]
pub fn graph_adjacency_matrix(
py: Python,
graph: &graph::PyGraph,
weight_fn: Option<PyObject>,
default_weight: f64,
null_value: f64,
parallel_edge: &str,
) -> PyResult<PyObject> {
let n = graph.node_count();
let mut matrix = Array2::<f64>::from_elem((n, n), null_value);
let mut parallel_edge_count = HashMap::new();
for (i, j, weight) in get_edge_iter_with_weights(&graph.graph) {
let edge_weight = weight_callable(py, &weight_fn, &weight, default_weight)?;
if matrix[[i, j]] == null_value || (null_value.is_nan() && matrix[[i, j]].is_nan()) {
matrix[[i, j]] = edge_weight;
matrix[[j, i]] = edge_weight;
} else {
matrix[[i, j]] += edge_weight;
matrix[[j, i]] += edge_weight;
match parallel_edge {
"sum" => {
matrix[[i, j]] += edge_weight;
matrix[[j, i]] += edge_weight;
}
"min" => {
let weight_min = matrix[[i, j]].min(edge_weight);
matrix[[i, j]] = weight_min;
matrix[[j, i]] = weight_min;
}
"max" => {
let weight_max = matrix[[i, j]].max(edge_weight);
matrix[[i, j]] = weight_max;
matrix[[j, i]] = weight_max;
}
"avg" => {
if parallel_edge_count.contains_key(&[i, j]) {
matrix[[i, j]] = (matrix[[i, j]] * parallel_edge_count[&[i, j]] as f64
+ edge_weight)
/ ((parallel_edge_count[&[i, j]] + 1) as f64);
matrix[[j, i]] = (matrix[[j, i]] * parallel_edge_count[&[i, j]] as f64
+ edge_weight)
/ ((parallel_edge_count[&[i, j]] + 1) as f64);
*parallel_edge_count.get_mut(&[i, j]).unwrap() += 1;
} else {
parallel_edge_count.insert([i, j], 2);
matrix[[i, j]] = (matrix[[i, j]] + edge_weight) / 2.0;
matrix[[j, i]] = (matrix[[j, i]] + edge_weight) / 2.0;
}
}
_ => {
return Err(PyValueError::new_err("Parallel edges can currently only be dealt with using \"sum\", \"min\", \"max\", or \"avg\"."));
}
}
}
}
Ok(matrix.into_pyarray(py).into())
Expand Down
51 changes: 51 additions & 0 deletions tests/rustworkx_tests/digraph/test_adjacency_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,54 @@ def test_nan_null(self):
edge_list,
[(0, 1, 1 + 0j), (1, 0, 1 + 0j), (1, 2, 1 + 0j), (2, 1, 1 + 0j)],
)

def test_parallel_edge(self):
graph = rustworkx.PyDiGraph()
a = graph.add_node("A")
b = graph.add_node("B")
c = graph.add_node("C")

graph.add_edges_from(
[
(a, b, 3.0),
(a, b, 1.0),
(a, c, 2.0),
(b, c, 7.0),
(c, a, 1.0),
(b, c, 2.0),
(a, b, 4.0),
]
)

min_matrix = rustworkx.digraph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="min"
)
np.testing.assert_array_equal(
[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0], [1.0, 0.0, 0.0]], min_matrix
)

max_matrix = rustworkx.digraph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="max"
)
np.testing.assert_array_equal(
[[0.0, 4.0, 2.0], [0.0, 0.0, 7.0], [1.0, 0.0, 0.0]], max_matrix
)

avg_matrix = rustworkx.digraph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="avg"
)
np.testing.assert_array_equal(
[[0.0, 8 / 3.0, 2.0], [0.0, 0.0, 4.5], [1.0, 0.0, 0.0]], avg_matrix
)

sum_matrix = rustworkx.digraph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="sum"
)
np.testing.assert_array_equal(
[[0.0, 8.0, 2.0], [0.0, 0.0, 9.0], [1.0, 0.0, 0.0]], sum_matrix
)

with self.assertRaises(ValueError):
rustworkx.digraph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="error"
)
51 changes: 51 additions & 0 deletions tests/rustworkx_tests/graph/test_adjencency_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,54 @@ def test_nan_null(self):
edge_list,
[(0, 1, 1 + 0j), (1, 2, 1 + 0j)],
)

def test_parallel_edge(self):
graph = rustworkx.PyGraph()
a = graph.add_node("A")
b = graph.add_node("B")
c = graph.add_node("C")

graph.add_edges_from(
[
(a, b, 3.0),
(a, b, 1.0),
(a, c, 2.0),
(b, c, 7.0),
(c, a, 1.0),
(b, c, 2.0),
(a, b, 4.0),
]
)

min_matrix = rustworkx.graph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="min"
)
np.testing.assert_array_equal(
[[0.0, 1.0, 1.0], [1.0, 0.0, 2.0], [1.0, 2.0, 0.0]], min_matrix
)

max_matrix = rustworkx.graph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="max"
)
np.testing.assert_array_equal(
[[0.0, 4.0, 2.0], [4.0, 0.0, 7.0], [2.0, 7.0, 0.0]], max_matrix
)

avg_matrix = rustworkx.graph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="avg"
)
np.testing.assert_array_equal(
[[0.0, 8 / 3.0, 1.5], [8 / 3.0, 0.0, 4.5], [1.5, 4.5, 0.0]], avg_matrix
)

sum_matrix = rustworkx.graph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="sum"
)
np.testing.assert_array_equal(
[[0.0, 8.0, 3.0], [8.0, 0.0, 9.0], [3.0, 9.0, 0.0]], sum_matrix
)

with self.assertRaises(ValueError):
rustworkx.graph_adjacency_matrix(
graph, weight_fn=lambda x: float(x), parallel_edge="error"
)