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 is_subgraph_isomorphic function #317

Merged
merged 13 commits into from
May 28, 2021
3 changes: 3 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Specific Graph Type Methods
retworkx.is_directed_acyclic_graph
retworkx.digraph_is_isomorphic
retworkx.graph_is_isomorphic
retworkx.digraph_is_subgraph_isomorphic
retworkx.graph_is_subgraph_isomorphic
retworkx.topological_sort
retworkx.descendants
retworkx.ancestors
Expand Down Expand Up @@ -136,6 +138,7 @@ type functions in the algorithms API but can be run with a
retworkx.k_shortest_path_lengths
retworkx.dfs_edges
retworkx.is_isomorphic
retworkx.is_subgraph_isomorphic
retworkx.is_isomorphic_node_match
retworkx.transitivity
retworkx.core_number
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
A new function, :func:`~retworkx.is_subgraph_isomorphic` was added to
determine if two graphs of type :class:`~retworkx.PyGraph` or
:class:`~retworkx.PyDiGraph` are induced subgraph isomorphic.
59 changes: 59 additions & 0 deletions retworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,65 @@ def _graph_is_isomorphic_node_match(first, second, matcher, id_order=True):
return graph_is_isomorphic(first, second, matcher, id_order=id_order)


@functools.singledispatch
def is_subgraph_isomorphic(
first, second, node_matcher=None, edge_matcher=None, id_order=False
):
"""Determine if 2 graphs are subgraph isomorphic

This checks if 2 graphs are subgraph isomorphic both structurally and also
comparing the node and edge data using the provided matcher functions.
The matcher functions take in 2 data objects and will compare them. A
simple example that checks if they're just equal would be::

graph_a = retworkx.PyGraph()
graph_b = retworkx.PyGraph()
retworkx.is_subgraph_isomorphic(graph_a, graph_b,
lambda x, y: x == y)


:param first: The first graph to compare. Can either be a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`.
:param second: The second graph to compare. Can either be a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`.
It should be the same type as the first graph.
:param callable node_matcher: A python callable object that takes 2
positional one for each node data object. If the return of this
function evaluates to True then the nodes passed to it are viewed
as matching.
:param callable 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 viewed
as matching.
:param bool id_order: If set to ``True`` this function will match the nodes
in order specified by their ids. Otherwise it will default to a heuristic
matching order based on [VF2]_ paper.

:returns: ``True`` if there is a subgraph of `first` isomorphic to `second`
, ``False`` if there is not.
:rtype: bool
"""
raise TypeError("Invalid Input Type %s for graph" % type(first))


@is_subgraph_isomorphic.register(PyDiGraph)
def _digraph_is_subgraph_isomorphic(
first, second, node_matcher=None, edge_matcher=None, id_order=False
):
return digraph_is_subgraph_isomorphic(
first, second, node_matcher, edge_matcher, id_order
)


@is_subgraph_isomorphic.register(PyGraph)
def _graph_is_subgraph_isomorphic(
first, second, node_matcher=None, edge_matcher=None, id_order=False
):
return graph_is_subgraph_isomorphic(
first, second, node_matcher, edge_matcher, id_order
)


@functools.singledispatch
def transitivity(graph):
"""Compute the transitivity of a graph.
Expand Down
108 changes: 59 additions & 49 deletions src/isomorphism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// It has then been modified to function with PyDiGraph inputs instead of Graph.

use fixedbitset::FixedBitSet;
use std::cmp::Ordering;
use std::iter::FromIterator;
use std::marker;

Expand Down Expand Up @@ -346,7 +347,27 @@ where
new_graph
}

/// [Graph] Return `true` if the graphs `g0` and `g1` are isomorphic.
trait SemanticMatcher<T> {
fn enabled(&self) -> bool;
fn eq(&mut self, _: &T, _: &T) -> PyResult<bool>;
}

impl<T, F> SemanticMatcher<T> for Option<F>
where
F: FnMut(&T, &T) -> PyResult<bool>,
{
#[inline]
fn enabled(&self) -> bool {
self.is_some()
}
#[inline]
fn eq(&mut self, a: &T, b: &T) -> PyResult<bool> {
let res = (self.as_mut().unwrap())(a, b)?;
Ok(res)
}
}

/// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic.
///
/// Using the VF2 algorithm, examining both syntactic and semantic
/// graph isomorphism (graph structure and matching node and edge weights).
Expand All @@ -359,6 +380,7 @@ pub fn is_isomorphic<Ty, F, G>(
mut node_match: Option<F>,
mut edge_match: Option<G>,
id_order: bool,
ordering: Ordering,
) -> PyResult<bool>
where
Ty: EdgeType,
Expand All @@ -380,8 +402,10 @@ where
g1
};

if g0_out.node_count() != g1_out.node_count()
|| g0_out.edge_count() != g1_out.edge_count()
if (g0_out.node_count().cmp(&g1_out.node_count()).then(ordering)
!= ordering)
|| (g0_out.edge_count().cmp(&g1_out.edge_count()).then(ordering)
!= ordering)
{
return Ok(false);
}
Expand All @@ -401,57 +425,26 @@ where
};

let mut st = [Vf2State::new(g0), Vf2State::new(g1)];
let res = try_match(&mut st, g0, g1, &mut node_match, &mut edge_match)?;
let res =
try_match(&mut st, g0, g1, &mut node_match, &mut edge_match, ordering)?;
Ok(res.unwrap_or(false))
}

trait SemanticMatcher<T> {
fn enabled(&self) -> bool;
fn eq(&mut self, _: &T, _: &T) -> PyResult<bool>;
}

struct NoSemanticMatch;

impl<T> SemanticMatcher<T> for NoSemanticMatch {
#[inline]
fn enabled(&self) -> bool {
false
}
#[inline]
fn eq(&mut self, _: &T, _: &T) -> PyResult<bool> {
Ok(true)
}
}

impl<T, F> SemanticMatcher<T> for Option<F>
where
F: FnMut(&T, &T) -> PyResult<bool>,
{
#[inline]
fn enabled(&self) -> bool {
self.is_some()
}
#[inline]
fn eq(&mut self, a: &T, b: &T) -> PyResult<bool> {
let res = (self.as_mut().unwrap())(a, b)?;
Ok(res)
}
}

/// Return Some(bool) if isomorphism is decided, else None.
fn try_match<Ty, F, G>(
mut st: &mut [Vf2State<Ty>; 2],
g0: &StablePyGraph<Ty>,
g1: &StablePyGraph<Ty>,
node_match: &mut F,
edge_match: &mut G,
ordering: Ordering,
) -> PyResult<Option<bool>>
where
Ty: EdgeType,
F: SemanticMatcher<PyObject>,
G: SemanticMatcher<PyObject>,
{
if st[0].is_complete() {
if st[1].is_complete() {
return Ok(Some(true));
}

Expand Down Expand Up @@ -593,7 +586,7 @@ where
}
}
}
if succ_count[0] != succ_count[1] {
if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering {
return Ok(false);
}
// R_pred
Expand All @@ -617,7 +610,7 @@ where
}
}
}
if pred_count[0] != pred_count[1] {
if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering {
return Ok(false);
}
}
Expand All @@ -634,21 +627,36 @@ where
}};
}
// R_out
if rule!(out, 0, Outgoing) != rule!(out, 1, Outgoing) {
if rule!(out, 0, Outgoing)
.cmp(&rule!(out, 1, Outgoing))
.then(ordering)
!= ordering
{
return Ok(false);
}
if g[0].is_directed()
&& rule!(out, 0, Incoming) != rule!(out, 1, Incoming)
&& rule!(out, 0, Incoming)
.cmp(&rule!(out, 1, Incoming))
.then(ordering)
!= ordering
{
return Ok(false);
}
// R_in
if g[0].is_directed() {
if rule!(ins, 0, Outgoing) != rule!(ins, 1, Outgoing) {
if rule!(ins, 0, Outgoing)
.cmp(&rule!(ins, 1, Outgoing))
.then(ordering)
!= ordering
{
return Ok(false);
}

if rule!(ins, 0, Incoming) != rule!(ins, 1, Incoming) {
if rule!(ins, 0, Incoming)
.cmp(&rule!(ins, 1, Incoming))
.then(ordering)
!= ordering
{
return Ok(false);
}
}
Expand All @@ -664,7 +672,7 @@ where
}
}
}
if new_count[0] != new_count[1] {
if new_count[0].cmp(&new_count[1]).then(ordering) != ordering {
return Ok(false);
}
if g[0].is_directed() {
Expand All @@ -677,7 +685,7 @@ where
}
}
}
if new_count[0] != new_count[1] {
if new_count[0].cmp(&new_count[1]).then(ordering) != ordering {
return Ok(false);
}
}
Expand Down Expand Up @@ -779,12 +787,14 @@ where
let feasible = is_feasible(&mut st, nodes)?;
if feasible {
push_state(&mut st, nodes);
if st[0].is_complete() {
if st[1].is_complete() {
return Ok(Some(true));
}
// Check cardinalities of Tin, Tout sets
if st[0].out_size == st[1].out_size
&& st[0].ins_size == st[1].ins_size
if st[0].out_size.cmp(&st[1].out_size).then(ordering)
== ordering
&& st[0].ins_size.cmp(&st[1].ins_size).then(ordering)
== ordering
{
let f0 = Frame::Unwind {
nodes,
Expand Down
Loading