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

[WIP] Add vf2 mapping functions #368

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Isomorphism
retworkx.is_isomorphic
retworkx.is_subgraph_isomorphic
retworkx.is_isomorphic_node_match
retworkx.vf2_mapping

.. _matching:

Expand Down Expand Up @@ -203,6 +204,7 @@ the functions from the explicitly typed based on the data type.

retworkx.digraph_is_isomorphic
retworkx.digraph_is_subgraph_isomorphic
retworkx.digraph_vf2_mapping
retworkx.digraph_distance_matrix
retworkx.digraph_floyd_warshall_numpy
retworkx.digraph_adjacency_matrix
Expand Down Expand Up @@ -241,6 +243,7 @@ typed API based on the data type.

retworkx.graph_is_isomorphic
retworkx.graph_is_subgraph_isomorphic
retworkx.graph_vf2_mapping
retworkx.graph_distance_matrix
retworkx.graph_floyd_warshall_numpy
retworkx.graph_adjacency_matrix
Expand Down
16 changes: 16 additions & 0 deletions releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
features:
- |
Added a new function, :func:`retworkx.vf2_mapping`, which will use the
vf2 isomorphism algorithm (which is also used for
:func:`retworkx.is_isomorphic` and :func:`retworkx.is_subgraph_isomorphic`)
to return an isomorphic mapping between two graphs. For example:

.. jupyter-execute::

import retworkx

graph = retworkx.generators.directed_grid_graph(10, 10)
other_graph = retworkx.generators.directed_grid_graph(4, 4)
mapping = retworkx.vf2_mapping(graph, other_graph, subgraph=True)
print(mapping)
79 changes: 79 additions & 0 deletions retworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1259,3 +1259,82 @@ def _graph_spiral_layout(
resolution=resolution,
equidistant=equidistant,
)


@functools.singledispatch
def vf2_mapping(
first,
second,
node_matcher=None,
edge_matcher=None,
id_order=True,
subgraph=False,
):
"""
Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects

This funcion will run the vf2 algorithm used from
:func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic`
but instead of returning a boolean it will return the mapping of node ids
found from ``first`` to ``second``. If the graphs are not isomorphic than
``None`` will be returned.

:param PyDiGraph first: The first graph to find the mapping for
:param PyDiGraph second: The second graph to find the mapping for
:param node_matcher: An optional python callable object that takes 2
positional arguments, one for each node data object in either graph.
If the return of this function evaluates to True then the nodes
passed to it are vieded as matching.
:param edge_matcher: A python callable object that takes 2 positional
one for each edge data object. If the return of this
function evaluates to True then the edges passed to it are vieded
as matching.
:param bool id_order: If set to ``False`` this function will use a
heuristic matching order based on [VF2]_ paper. Otherwise it will
default to matching the nodes in order specified by their ids.
:param bool subgraph: If set to ``True`` the function will return the
subgraph isomorphic found between the graphs.

:returns: A dicitonary of node indices from ``first`` to node indices in
``second`` representing the mapping found.
:rtype: dict
"""
raise TypeError("Invalid Input Type %s for graph" % type(first))


@vf2_mapping.register(PyDiGraph)
def _digraph_vf2_mapping(
first,
second,
node_matcher=None,
edge_matcher=None,
id_order=True,
subgraph=False,
):
return digraph_vf2_mapping(
first,
second,
node_matcher=node_matcher,
edge_matcher=edge_matcher,
id_order=id_order,
subgraph=subgraph,
)


@vf2_mapping.register(PyGraph)
def _graph_vf2_mapping(
first,
second,
node_matcher=None,
edge_matcher=None,
id_order=True,
subgraph=False,
):
return graph_vf2_mapping(
first,
second,
node_matcher=node_matcher,
edge_matcher=edge_matcher,
id_order=id_order,
subgraph=subgraph,
)
98 changes: 91 additions & 7 deletions src/isomorphism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// License for the specific language governing permissions and limitations
// under the License.

#![allow(clippy::too_many_arguments)]
// This module is a forked version of petgraph's isomorphism module @ 0.5.0.
// It has then been modified to function with PyDiGraph inputs instead of Graph.

Expand Down Expand Up @@ -47,6 +48,7 @@ where
&self,
py: Python,
graph: &StablePyGraph<Ty>,
mut mapping: Option<&mut HashMap<usize, usize>>,
) -> StablePyGraph<Ty> {
let order = self.sort(graph);

Expand All @@ -66,7 +68,11 @@ where
let c_index = graph.from_index(c);
new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py));
}

if mapping.is_some() {
for (old_value, new_index) in id_map.iter().enumerate() {
mapping.as_mut().unwrap().insert(*new_index, old_value);
}
}
new_graph
}
}
Expand Down Expand Up @@ -319,7 +325,10 @@ where
}
}

fn reindex_graph<Ty>(py: Python, graph: &StablePyGraph<Ty>) -> StablePyGraph<Ty>
fn reindex_graph<Ty>(
py: Python,
graph: &StablePyGraph<Ty>,
) -> (StablePyGraph<Ty>, HashMap<usize, usize>)
where
Ty: EdgeType,
{
Expand All @@ -344,7 +353,10 @@ where
new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py));
}

new_graph
(
new_graph,
id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(),
)
}

trait SemanticMatcher<T> {
Expand Down Expand Up @@ -381,6 +393,7 @@ pub fn is_isomorphic<Ty, F, G>(
mut edge_match: Option<G>,
id_order: bool,
ordering: Ordering,
mut mapping: Option<&mut HashMap<usize, usize>>,
) -> PyResult<bool>
where
Ty: EdgeType,
Expand All @@ -389,16 +402,24 @@ where
{
let mut inner_temp_g0: StablePyGraph<Ty>;
let mut inner_temp_g1: StablePyGraph<Ty>;
let mut node_map_g0: Option<HashMap<usize, usize>>;
let mut node_map_g1: Option<HashMap<usize, usize>>;
let g0_out = if g0.nodes_removed() {
inner_temp_g0 = reindex_graph(py, g0);
let res = reindex_graph(py, g0);
inner_temp_g0 = res.0;
node_map_g0 = Some(res.1);
&inner_temp_g0
} else {
node_map_g0 = None;
g0
};
let g1_out = if g1.nodes_removed() {
inner_temp_g1 = reindex_graph(py, g1);
let res = reindex_graph(py, g1);
inner_temp_g1 = res.0;
node_map_g1 = Some(res.1);
&inner_temp_g1
} else {
node_map_g1 = None;
g1
};

Expand All @@ -411,14 +432,46 @@ where
}

let g0 = if !id_order {
inner_temp_g0 = Vf2ppSorter.reorder(py, g0_out);
inner_temp_g0 = if mapping.is_some() {
let mut vf2pp_map: HashMap<usize, usize> =
HashMap::with_capacity(g0_out.node_count());
let temp = Vf2ppSorter.reorder(py, g0_out, Some(&mut vf2pp_map));
match node_map_g0 {
Some(ref mut g0_map) => {
for (_, old_index) in vf2pp_map.iter_mut() {
*old_index = g0_map[old_index];
}
*g0_map = vf2pp_map;
}
None => node_map_g0 = Some(vf2pp_map),
};
temp
} else {
Vf2ppSorter.reorder(py, g0_out, None)
};
&inner_temp_g0
} else {
g0_out
};

let g1 = if !id_order {
inner_temp_g1 = Vf2ppSorter.reorder(py, g1_out);
inner_temp_g1 = if mapping.is_some() {
let mut vf2pp_map: HashMap<usize, usize> =
HashMap::with_capacity(g1_out.node_count());
let temp = Vf2ppSorter.reorder(py, g1_out, Some(&mut vf2pp_map));
match node_map_g1 {
Some(ref mut g1_map) => {
for (_, old_index) in vf2pp_map.iter_mut() {
*old_index = g1_map[old_index];
}
*g1_map = vf2pp_map;
}
None => node_map_g1 = Some(vf2pp_map),
};
temp
} else {
Vf2ppSorter.reorder(py, g1_out, None)
};
&inner_temp_g1
} else {
g1_out
Expand All @@ -427,6 +480,37 @@ where
let mut st = [Vf2State::new(g0), Vf2State::new(g1)];
let res =
try_match(&mut st, g0, g1, &mut node_match, &mut edge_match, ordering)?;

if mapping.is_some() {
for (index, val) in st[1].mapping.iter().enumerate() {
match node_map_g1 {
Some(ref g1_map) => {
let node_index = g1_map[&index];
match node_map_g0 {
Some(ref g0_map) => mapping
.as_mut()
.unwrap()
.insert(g0_map[&val.index()], node_index),
None => mapping
.as_mut()
.unwrap()
.insert(val.index(), node_index),
};
}
None => {
match node_map_g0 {
Some(ref g0_map) => mapping
.as_mut()
.unwrap()
.insert(g0_map[&val.index()], index),
None => {
mapping.as_mut().unwrap().insert(val.index(), index)
}
};
}
};
}
}
Ok(res.unwrap_or(false))
}

Expand Down
Loading