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

Add substitute_node_with_subgraph method to PyDiGraph #312

Merged
merged 23 commits into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
058abc6
Add substitute_node_with_subgraph method to PyDiGraph
mtreinish Apr 26, 2021
7cfdeb2
Merge branch 'main' into substitute-node
mtreinish Apr 26, 2021
2ee3938
Raise an IndexError if the callback returns an invalid index
mtreinish Apr 28, 2021
cfd8d0c
Merge branch 'main' into substitute-node
mtreinish May 9, 2021
d03a4a6
Merge branch 'main' into substitute-node
mtreinish Jun 2, 2021
1d45eec
Remove node on empty map too
mtreinish Jun 3, 2021
427a943
Avoid overallocation on empty graph
mtreinish Jun 3, 2021
e67e958
Run cargo fmt
mtreinish Jun 3, 2021
eafb002
Add release notes
mtreinish Jun 3, 2021
f08a063
Fix docstring
mtreinish Jun 3, 2021
64606f2
Add tests
mtreinish Jun 4, 2021
5056244
Add missing negative test
mtreinish Jun 4, 2021
23a0c42
Fix lint
mtreinish Jun 4, 2021
dd08472
Add tests for custom return type
mtreinish Jun 4, 2021
6dcbc85
Add len test for custom return type
mtreinish Jun 4, 2021
681fcab
Add node map to release note example
mtreinish Jun 11, 2021
cca111b
Add test with bidirectional edge on replaced node
mtreinish Jun 11, 2021
4374d4a
Merge remote-tracking branch 'origin/main' into substitute-node
mtreinish Jun 21, 2021
80d1216
Document unordered return type
mtreinish Jun 21, 2021
4ad41c7
Merge branch 'main' into substitute-node
mtreinish Jun 22, 2021
411f6e9
Merge branch 'main' into substitute-node
mtreinish Jun 24, 2021
ca7bbaa
Merge branch 'main' into substitute-node
mtreinish Jun 24, 2021
e261144
Apply suggestions from code review
mtreinish Jun 24, 2021
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
34 changes: 34 additions & 0 deletions releasenotes/notes/add-subgraph-replace-ec54792df1d641fb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
features:
- |
A new method, :meth:`~retworkx.PyDiGraph.substitute_node_with_subgraph`,
to the :class:`~retworkx.PyDiGraph` class. This method is used to replace a
node in a :class:`~retworkx.PyDiGraph` object with another
:class:`~retwork.PyDiGraph` object. For example, first creating a new
graph:

.. jupyter-execute::

import retworkx
from retworkx.visualization import mpl_draw

original_graph = retworkx.generators.directed_path_graph(5)
mpl_draw(original_graph)

then create another graph to use in place of a node:

.. jupyter-execute::

other_graph = retworkx.generators.directed_star_graph(25)
mpl_draw(other_graph)

finally replace a node in the original graph with the second graph:

.. jupyter-execute::

def edge_map_fn(_source, _target, _weight):
return 0

node_mapping = original_graph.substitute_node_with_subgraph(2, other_graph, edge_map_fn)
print(node_mapping)
mpl_draw(original_graph)
149 changes: 148 additions & 1 deletion src/digraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use petgraph::visit::{

use super::dot_utils::build_dot;
use super::iterators::{
EdgeIndexMap, EdgeIndices, EdgeList, NodeIndices, WeightedEdgeList,
EdgeIndexMap, EdgeIndices, EdgeList, NodeIndices, NodeMap, WeightedEdgeList,
};
use super::{
is_directed_acyclic_graph, DAGHasCycle, DAGWouldCycle, NoEdgeBetweenNodes,
Expand Down Expand Up @@ -2203,6 +2203,153 @@ impl PyDiGraph {
Ok(out_dict.into())
}

/// Substitute a node with a PyDigraph object
///
/// :param int node: The node to replace with the PyDiGraph object
/// :param PyDiGraph other: The other graph to replace ``node`` with
/// :param callable edge_map_fn: A callable object that will take 3 position
/// parameters, ``(source, target, weight)`` to represent an edge either to
/// or from ``node`` in this graph. The expected return value from this
/// callable is the node index of the node in ``other`` that an edge should
/// be to/from. If None is returned, that edge will be skipped and not
/// be copied.
/// :param callable node_filter: An optional callable object that when used
/// will receive a node's payload object from ``other`` and return
/// ``True`` if that node is to be included in the graph or not.
/// :param callable edge_weight_map: An optional callable object that when
/// used will receive an edge's weight/data payload from ``other`` and
/// will return an object to use as the weight for a newly created edge
/// after the edge is mapped from ``other``. If not specified the weight
/// from the edge in ``other`` will be copied by reference and used.
///
/// :returns: A mapping of node indices in ``other`` to the equivalent node
/// in this graph.
/// :rtype: NodeMap
#[text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None)"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think edge_map_fn should be optional.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required right now because we without it we don't know what to do with edges to or from node. If we switched it to optional what do you think the default behavior should it was not set? The only option I could think of is to just drop all the edges into or out of node if this wasn't set, but if we did this it wouldn't really feel like it was substituting the node with a graph.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a typo?

Suggested change
#[text_signature = "(self, node, other, edge_map_fn, /, node_filter=None, edge_weight_map=None)"]
#[text_signature = "(self, node, other, edge_map_fn, node_filter=None, edge_weight_map=None)"]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually intentional, the / indicates the end of positional only arguments (it's part of the cPython interface and is actually a valid python syntax for >=3.8). See the docs for the rust python interface here: https://pyo3.rs/v0.9.2/function.html#making-the-function-signature-available-to-python which explains this.

fn substitute_node_with_subgraph(
&mut self,
py: Python,
node: usize,
other: &PyDiGraph,
edge_map_fn: PyObject,
node_filter: Option<PyObject>,
edge_weight_map: Option<PyObject>,
) -> PyResult<NodeMap> {
let weight_map_fn = |obj: &PyObject,
weight_fn: &Option<PyObject>|
-> PyResult<PyObject> {
match weight_fn {
Some(weight_fn) => weight_fn.call1(py, (obj,)),
None => Ok(obj.clone_ref(py)),
}
};
let map_fn = |source: usize,
target: usize,
weight: &PyObject|
-> PyResult<Option<usize>> {
let res = edge_map_fn.call1(py, (source, target, weight))?;
res.extract(py)
};
let filter_fn =
|obj: &PyObject, filter_fn: &Option<PyObject>| -> PyResult<bool> {
match filter_fn {
Some(filter) => {
let res = filter.call1(py, (obj,))?;
res.extract(py)
}
None => Ok(true),
}
};
let node_index: NodeIndex = NodeIndex::new(node);
if self.graph.node_weight(node_index).is_none() {
return Err(PyIndexError::new_err(format!(
"Specified node {} is not in this graph",
node
)));
}
// Copy nodes from other to self
let mut out_map: HashMap<usize, usize> =
HashMap::with_capacity(other.node_count());
for node in other.graph.node_indices() {
let node_weight = other[node].clone_ref(py);
if !filter_fn(&node_weight, &node_filter)? {
continue;
}
let new_index = self.graph.add_node(node_weight);
out_map.insert(node.index(), new_index.index());
}
// If no nodes are copied bail here since there is nothing left
// to do.
if out_map.is_empty() {
self.graph.remove_node(node_index);
// Return a new empty map to clear allocation from out_map
return Ok(NodeMap {
node_map: HashMap::new(),
});
}
// Copy edges from other to self
for edge in other.graph.edge_references().filter(|edge| {
out_map.contains_key(&edge.target().index())
&& out_map.contains_key(&edge.source().index())
}) {
self._add_edge(
NodeIndex::new(out_map[&edge.source().index()]),
NodeIndex::new(out_map[&edge.target().index()]),
weight_map_fn(edge.weight(), &edge_weight_map)?,
)?;
}
// Add edges to/from node to nodes in other
let in_edges: Vec<(NodeIndex, NodeIndex, PyObject)> = self
.graph
.edges_directed(node_index, petgraph::Direction::Incoming)
.map(|edge| {
(edge.source(), edge.target(), edge.weight().clone_ref(py))
})
.collect();
let out_edges: Vec<(NodeIndex, NodeIndex, PyObject)> = self
.graph
.edges_directed(node_index, petgraph::Direction::Outgoing)
.map(|edge| {
(edge.source(), edge.target(), edge.weight().clone_ref(py))
})
.collect();
for (source, target, weight) in in_edges {
let old_index = map_fn(source.index(), target.index(), &weight)?;
let target_out = match old_index {
Some(old_index) => match out_map.get(&old_index) {
Some(new_index) => NodeIndex::new(*new_index),
None => {
return Err(PyIndexError::new_err(format!(
"No mapped index {} found",
old_index
)))
}
},
None => continue,
};
self._add_edge(source, target_out, weight)?;
}
for (source, target, weight) in out_edges {
let old_index = map_fn(source.index(), target.index(), &weight)?;
let source_out = match old_index {
Some(old_index) => match out_map.get(&old_index) {
Some(new_index) => NodeIndex::new(*new_index),
None => {
return Err(PyIndexError::new_err(format!(
"No mapped index {} found",
old_index
)))
}
},
None => continue,
};
self._add_edge(source_out, target, weight)?;
}
// Remove node
self.graph.remove_node(node_index);
Ok(NodeMap { node_map: out_map })
}

/// Return a new PyDiGraph object for a subgraph of this graph
///
/// :param list nodes: A list of node indices to generate the subgraph
Expand Down
Loading