From 31068bb509d5076647f4fff8f264f3c0db4a4633 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Tue, 10 Dec 2024 14:51:23 +0900 Subject: [PATCH 01/27] feat(core/graph): implement VF2++ algorithm --- include/nuri/core/graph_vf2pp.h | 447 ++++++++++++++++++++++++++++++++ include/nuri/eigen_config.h | 1 + 2 files changed, 448 insertions(+) create mode 100644 include/nuri/core/graph_vf2pp.h diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h new file mode 100644 index 00000000..31aa20c3 --- /dev/null +++ b/include/nuri/core/graph_vf2pp.h @@ -0,0 +1,447 @@ +// +// Project NuriKit - Copyright 2024 SNU Compbio Lab. +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef NURI_CORE_GRAPH_VF2PP_H_ +#define NURI_CORE_GRAPH_VF2PP_H_ + +/// @cond +#include +#include + +#include +#include +#include +#include +/// @endcond + +#include "nuri/eigen_config.h" +#include "nuri/core/graph.h" +#include "nuri/utils.h" + +namespace nuri { +enum class MappingType : int { + kEdgeSubgraph, // Subgraph isomorphism + kNodeSubgraph, // Induced subgraph isomorphism + kIsomorphism, // Graph isomorphism +}; + +namespace internal { + template + int vf2pp_process_bfs_tree(const Graph &query, ArrayXi &order, + ArrayXb &visited, ArrayXi &curr_conn, + AL &query_cnts, int root, int i) { + order[i] = root; + visited[root] = true; + + int lp = i, rp = i + 1, wp = i + 1; + + for (; i < wp; ++i) { + int curr = order[i]; + + for (auto nei: query.node(curr)) { + if (!visited[nei.dst().id()]) { + order[wp++] = nei.dst().id(); + visited[nei.dst().id()] = true; + } + } + + if (i < rp) + continue; + + for (int j = lp; j < rp; ++j) { + int min_pos = j; + for (int k = j + 1; k < rp; ++k) { + if (curr_conn[order[min_pos]] < curr_conn[order[k]] + || (curr_conn[order[min_pos]] == curr_conn[order[k]] + && (query.degree(order[min_pos]) < query.degree(order[k]) + || (query.degree(order[min_pos]) == query.degree(order[k]) + && query_cnts[order[min_pos]] + > query_cnts[order[k]])))) { + min_pos = k; + } + } + + --query_cnts[order[min_pos]]; + for (auto nei: query.node(order[min_pos])) + ++curr_conn[nei.dst().id()]; + + std::swap(order[j], order[min_pos]); + } + + lp = rp; + rp = wp; + } + + return i; + } + + template + ArrayXi vf2pp_init_order(const Graph &query, const ArrayXi &qlbl, + const ArrayXi &tlbl, ArrayXi &target_lcnt, + ArrayXi &curr_conn, ArrayXb &visited) { + target_lcnt.setZero(); + for (int l: tlbl) + ++target_lcnt[l]; + + auto query_cnts = target_lcnt(qlbl); + + ArrayXi order = ArrayXi::Constant(query.size(), -1); + curr_conn.setZero(); + visited.setZero(); + + int iorder = 0; + for (int i = 0; iorder < query.size() && i < query.size();) { + if (visited[i]) { + ++i; + continue; + } + + int imin = i; + for (int j = i + 1; j < query.size(); ++j) { + if (!visited[j] + && (query_cnts[imin] > query_cnts[j] + || (query_cnts[imin] == query_cnts[j] + && query.degree(imin) < query.degree(j)))) { + imin = j; + } + } + + iorder = vf2pp_process_bfs_tree(query, order, visited, curr_conn, + query_cnts, imin, iorder); + } + + return order; + } + + using Vf2ppLabelMap = std::vector>>; + + template + std::pair + vf2pp_init_r_new_r_inout(const Graph &query, const ArrayXi &qlbl, + const ArrayXi &order, ArrayXi &r_inout, + ArrayXi &r_new, ArrayXi &visit_count) { + // r_inout == _labelTmp1, r_new == _labelTmp2 + ABSL_DCHECK_EQ(r_inout.size(), r_new.size()); + + Vf2ppLabelMap r_inout_labels(query.size()), r_new_labels(query.size()); + + r_inout.setZero(); + r_new.setZero(); + visit_count.setZero(); + for (const int i: order) { + visit_count[i] = -1; + + for (auto nei: query.node(i)) { + const int curr = nei.dst().id(); + if (visit_count[curr] > 0) { + ++r_inout[qlbl[curr]]; + } else if (visit_count[curr] == 0) { + ++r_new[qlbl[curr]]; + } + } + + for (auto nei: query.node(i)) { + const int curr = nei.dst().id(); + + const int curr_lbl = qlbl[curr]; + if (r_inout[curr_lbl] > 0) { + r_inout_labels[i].push_back({ curr_lbl, r_inout[curr_lbl] }); + } else if (r_new[curr_lbl] > 0) { + r_new_labels[i].push_back({ curr_lbl, r_new[curr_lbl] }); + } + + if (visit_count[curr] >= 0) + ++visit_count[curr]; + } + } + + return { r_inout_labels, r_new_labels }; + } + + template + void vf2pp_add_pair(const Graph &target, ArrayXi &q2t, ArrayXi &conn, + const int qi, const int ti) { + ABSL_DCHECK_GE(ti, 0); + ABSL_DCHECK_LT(ti, target.size()); + + conn[ti] = -1; + q2t[qi] = ti; + for (auto nei: target.node(ti)) { + if (conn[nei.dst().id()] != -1) + ++conn[nei.dst().id()]; + } + } + + template + void vf2pp_sub_pair(const Graph &target, ArrayXi &q2t, ArrayXi &conn, + const int qi, const int ti) { + ABSL_DCHECK_GE(ti, 0); + ABSL_DCHECK_LT(ti, target.size()); + + q2t[qi] = -1; + conn[ti] = 0; + + for (auto nei: target.node(ti)) { + int curr_conn = conn[nei.dst().id()]; + if (curr_conn > 0) { + --conn[nei.dst().id()]; + } else if (curr_conn == -1) { + ++conn[ti]; + } + } + } + + template + bool vf2pp_cut_by_labels(typename Graph::ConstNodeRef qn, + typename Graph::ConstNodeRef tn, + ArrayXi &label_tmp1, ArrayXi &label_tmp2, + const ArrayXi &tlbl, const Vf2ppLabelMap &r_inout, + const Vf2ppLabelMap &r_new, const ArrayXi &conn) { + // zero-init + label_tmp1(tlbl)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_inout[qn.id()]) + label_tmp1[lbl] = 0; + if constexpr (kMt != MappingType::kEdgeSubgraph) { + label_tmp2(tlbl)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_new[qn.id()]) + label_tmp2[lbl] = 0; + } + + for (auto tnei: tn) { + const int curr = tnei.dst().id(); + if (conn[curr] > 0) + --label_tmp1[tlbl[curr]]; + else if constexpr (kMt != MappingType::kEdgeSubgraph) { + if (conn[curr] == 0) + --label_tmp2[tlbl[curr]]; + } + } + + for (auto [lbl, cnt]: r_inout[qn.id()]) + label_tmp1[lbl] += cnt; + if constexpr (kMt != MappingType::kEdgeSubgraph) { + for (auto [lbl, cnt]: r_new[qn.id()]) + label_tmp2[lbl] += cnt; + } + + if constexpr (kMt == MappingType::kEdgeSubgraph) { + return absl::c_none_of(r_inout[qn.id()], [&](std::pair p) { + return label_tmp1[p.first] > 0; + }); + } + + if constexpr (kMt == MappingType::kNodeSubgraph) { + return absl::c_none_of(r_inout[qn.id()], + [&](std::pair p) { + return label_tmp1[p.first] > 0; + }) + && absl::c_none_of(r_new[qn.id()], [&](std::pair p) { + return label_tmp2[p.first] > 0; + }); + } + + if constexpr (kMt == MappingType::kIsomorphism) { + return absl::c_none_of(r_inout[qn.id()], + [&](std::pair p) { + return label_tmp1[p.first] != 0; + }) + && absl::c_none_of(r_new[qn.id()], [&](std::pair p) { + return label_tmp2[p.first] != 0; + }); + } + + ABSL_UNREACHABLE(); + } + + template + bool vf2pp_feas(typename Graph::ConstNodeRef qn, + typename Graph::ConstNodeRef tn, const ArrayXi &qlbl, + const ArrayXi &tlbl, ArrayXi &q2t, ArrayXi &conn, + ArrayXi &label_tmp1, ArrayXi &label_tmp2, + const Vf2ppLabelMap &r_inout, const Vf2ppLabelMap &r_new) { + if (qlbl[qn.id()] != tlbl[tn.id()]) + return false; + + for (auto qnei: qn) + if (q2t[qnei.dst().id()] >= 0) + --conn[q2t[qnei.dst().id()]]; + + bool is_iso = true; + for (auto tnei: tn) { + const int curr_conn = conn[tnei.dst().id()]; + if (curr_conn < -1) + ++conn[tnei.dst().id()]; + else if constexpr (kMt != MappingType::kEdgeSubgraph) { + if (curr_conn == -1) + is_iso = false; + } + } + + if (!is_iso) { + for (auto qnei: qn) { + const int ti = q2t[qnei.dst().id()]; + if (ti >= 0) + conn[ti] = -1; + } + return false; + } + + for (auto qnei: qn) { + const int ti = q2t[qnei.dst().id()]; + if (ti < 0 || conn[ti] == -1) + continue; + + const int curr_conn = conn[ti]; + conn[ti] = -1; + if constexpr (kMt == MappingType::kEdgeSubgraph) { + if (curr_conn < -1) + return false; + } else { + return false; + } + } + + return vf2pp_cut_by_labels(qn, tn, label_tmp1, label_tmp2, + tlbl, r_inout, r_new, conn); + } + + template + std::pair + vf2pp_ext_match(const Graph &query, const Graph &target, + const ArrayXi &qlbl, const ArrayXi &tlbl, + const ArrayXi &order, ArrayXi &conn, ArrayXi &label_tmp1, + ArrayXi &label_tmp2, const Vf2ppLabelMap &r_inout, + const Vf2ppLabelMap &r_new) { + ArrayXi q2t = ArrayXi::Constant(query.size(), -1); + + Array2Xi src_nei = Array2Xi::Constant(2, target.size(), -1); + conn.setZero(); + label_tmp1.setZero(); + label_tmp2.setZero(); + for (int depth = 0; depth >= 0;) { + if (depth == order.size()) + return { q2t, true }; + + auto qn = query.node(order[depth]); + + const int ti = q2t[qn.id()]; + int tj = src_nei(0, depth), tk = src_nei(1, depth); + if (tk >= 0) { + ABSL_DCHECK_NE(ti, tj); + vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); + ++tk; + } else { + int qk = 0; + if (ti < 0) { + qk = absl::c_find_if( + qn, [&](auto nei) { return q2t[nei.dst().id()] >= 0; }) + - qn.begin(); + } + + if (qk == qn.degree() || ti >= 0) { + int ti2 = 0; + if (ti >= 0) { + ti2 = ti + 1; + vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); + } + + if constexpr (kMt != MappingType::kEdgeSubgraph) { + ti2 = std::find_if(target.begin() + ti2, target.end(), + [&](auto tn) { + return conn[tn.id()] == 0 + && vf2pp_feas( + qn, tn, qlbl, tlbl, q2t, conn, + label_tmp1, label_tmp2, r_inout, + r_new); + }) + - target.begin(); + } else { + ti2 = std::find_if(target.begin() + ti2, target.end(), + [&](auto tn) { + return conn[tn.id()] >= 0 + && vf2pp_feas( + qn, tn, qlbl, tlbl, q2t, conn, + label_tmp1, label_tmp2, r_inout, + r_new); + }) + - target.begin(); + } + + if (ti2 < target.size()) { + vf2pp_add_pair(target, q2t, conn, qn.id(), ti2); + ++depth; + } else { + --depth; + } + + continue; + } + + tj = q2t[qn[qk].dst().id()]; + tk = 0; + } + + for (; tk < target.degree(tj); ++tk) { + auto tn = target.node(tj)[tk].dst(); + if (conn[tn.id()] > 0 + && vf2pp_feas(qn, tn, qlbl, tlbl, q2t, conn, + label_tmp1, label_tmp2, r_inout, + r_new)) { + vf2pp_add_pair(target, q2t, conn, qn.id(), tn.id()); + break; + } + } + + if (tk < target.degree(tj)) { + src_nei(1, depth++) = tk; + } else { + src_nei.col(depth--).fill(-1); + } + } + + return { q2t, false }; + } +} // namespace internal + +template +std::pair vf2pp(const Graph &query, + const Graph &target, const ArrayXi &qlbl, + const ArrayXi &tlbl, MappingType mt) { + const int nlabel = nuri::max(qlbl.maxCoeff(), tlbl.maxCoeff()) + 1; + + ArrayXi label_tmp1(nlabel), label_tmp2(nlabel); + + ArrayXi query_tmp(query.size()); + ArrayXb visited(query.size()); + + ArrayXi target_tmp(target.size()); + + // _order + ArrayXi order = internal::vf2pp_init_order(query, qlbl, tlbl, label_tmp1, + query_tmp, visited); + // _rInOutLabels1, _rNewLabels1 + auto [r_inout, r_new] = internal::vf2pp_init_r_new_r_inout( + query, qlbl, order, label_tmp1, label_tmp2, query_tmp); + + switch (mt) { + case MappingType::kEdgeSubgraph: + return internal::vf2pp_ext_match( + query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, + r_inout, r_new); + case MappingType::kNodeSubgraph: + return internal::vf2pp_ext_match( + query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, + r_inout, r_new); + case MappingType::kIsomorphism: + return internal::vf2pp_ext_match( + query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, + r_inout, r_new); + } + + ABSL_UNREACHABLE(); +} +} // namespace nuri + +#endif /* NURI_CORE_GRAPH_VF2PP_H_ */ diff --git a/include/nuri/eigen_config.h b/include/nuri/eigen_config.h index 34b33b4c..9eb19434 100644 --- a/include/nuri/eigen_config.h +++ b/include/nuri/eigen_config.h @@ -40,6 +40,7 @@ using Eigen::Array4i; using Eigen::ArrayX; using ArrayXb = Eigen::ArrayX; using ArrayXc = ArrayX; +using Eigen::Array2Xi; using Eigen::Array2Xd; using Eigen::Array33d; using Eigen::Array3Xd; From 9299dfc164b685157f74227d8653ad36a7255903 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Tue, 10 Dec 2024 14:51:48 +0900 Subject: [PATCH 02/27] test(core/graph): test process_bfs_tree() --- test/core/graph_vf2pp_test.cpp | 145 +++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 test/core/graph_vf2pp_test.cpp diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp new file mode 100644 index 00000000..d9940825 --- /dev/null +++ b/test/core/graph_vf2pp_test.cpp @@ -0,0 +1,145 @@ +// +// Project NuriKit - Copyright 2024 SNU Compbio Lab. +// SPDX-License-Identifier: Apache-2.0 +// + +#include "nuri/core/graph_vf2pp.h" + +#include + +#include "nuri/eigen_config.h" +#include "nuri/core/graph.h" + +namespace nuri { +namespace { +using GT = Graph; + +// lemon node id +int lid(const GT &g, int i) { + return g[i].data(); +} + +auto lemon_add_edge(GT &g, int src, int dst, int data) { + return g.add_edge(lid(g, src), lid(g, dst), data); +} + +Graph lemon_petersen() { + Graph g; + for (int i = 9; i >= 0; --i) + g.add_node(i); + + lemon_add_edge(g, 6, 8, 14); + lemon_add_edge(g, 9, 7, 13); + lemon_add_edge(g, 9, 6, 12); + lemon_add_edge(g, 5, 7, 11); + lemon_add_edge(g, 5, 8, 10); + lemon_add_edge(g, 4, 9, 9); + lemon_add_edge(g, 3, 8, 8); + lemon_add_edge(g, 2, 7, 7); + lemon_add_edge(g, 1, 6, 6); + lemon_add_edge(g, 0, 5, 5); + lemon_add_edge(g, 4, 0, 4); + lemon_add_edge(g, 3, 4, 3); + lemon_add_edge(g, 2, 3, 2); + lemon_add_edge(g, 1, 2, 1); + lemon_add_edge(g, 0, 1, 0); + + return g; +} + +TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { + Graph g = lemon_petersen(); + + ArrayXi order = ArrayXi::Constant(g.size(), -1); + ArrayXb visited = ArrayXb::Zero(g.size()); + ArrayXi curr_conn = ArrayXi::Zero(g.size()); + + ArrayXi target_lcnt(1); + target_lcnt[0] = 10; + ArrayXi qlbl = ArrayXi::Zero(g.size()); + auto query_cnts = target_lcnt(qlbl); + + const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, + query_cnts, 0, 0); + EXPECT_EQ(ret, 10); + + ArrayXi expected_order(10); + expected_order << 9, 7, 6, 4, 5, 2, 8, 1, 0, 3; + for (int i = 0; i < order.size(); ++i) + EXPECT_EQ(lid(g, order[i]), expected_order[i]); +} + +TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { + Graph g = lemon_petersen(); + + ArrayXi order = ArrayXi::Constant(g.size(), -1); + ArrayXb visited = ArrayXb::Zero(g.size()); + ArrayXi curr_conn = ArrayXi::Zero(g.size()); + + ArrayXi target_lcnt(2); + target_lcnt[0] = 4; + target_lcnt[1] = 6; + + ArrayXi qlbl(g.size()); + qlbl.head(6).setOnes(); + qlbl.tail(4).setZero(); + + auto query_cnts = target_lcnt(qlbl); + + const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, + query_cnts, 6, 0); + EXPECT_EQ(ret, 10); + + ArrayXi expected_order(10); + expected_order << 3, 2, 4, 8, 6, 5, 9, 0, 7, 1; + for (int i = 0; i < order.size(); ++i) + EXPECT_EQ(lid(g, order[i]), expected_order[i]); +} + +// TEST(VF2PPTest, Playground) { +// Graph g1; +// g1.add_node(1); +// g1.add_node(2); +// g1.add_node(3); +// g1.add_node(4); +// g1.add_node(5); +// g1.add_edge(0, 1, 1); +// g1.add_edge(1, 2, 2); +// g1.add_edge(2, 3, 3); +// g1.add_edge(3, 4, 4); +// g1.add_edge(4, 0, 5); + +// Graph g2; +// g2.add_node(1); +// g2.add_node(2); +// g2.add_node(3); +// g2.add_node(4); +// g2.add_node(5); +// g2.add_node(1); +// g2.add_node(2); +// g2.add_node(3); +// g2.add_node(4); +// g2.add_node(5); +// g2.add_edge(0, 1, 1); +// g2.add_edge(1, 2, 2); +// g2.add_edge(2, 3, 3); +// g2.add_edge(3, 4, 4); +// g2.add_edge(4, 0, 5); +// g2.add_edge(0, 5, 6); +// g2.add_edge(1, 6, 7); +// g2.add_edge(2, 7, 8); +// g2.add_edge(3, 8, 9); +// g2.add_edge(4, 9, 10); +// g2.add_edge(5, 8, 11); +// g2.add_edge(5, 7, 12); +// g2.add_edge(9, 6, 13); +// g2.add_edge(9, 7, 14); +// g2.add_edge(6, 8, 15); + +// ArrayXi qlbl = ArrayXi::Zero(g1.size()), tlbl = ArrayXi::Zero(g2.size()); + +// auto [map, ok] = vf2pp(g1, g2, qlbl, tlbl, MappingType::kEdgeSubgraph); +// EXPECT_TRUE(ok); +// } +} // namespace +} // namespace nuri From 57e4226fb2dd8c81b8f9e78a01564a09832bf359 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Wed, 11 Dec 2024 14:32:03 +0900 Subject: [PATCH 03/27] chore: don't diagnose dependency files --- .clangd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clangd b/.clangd index 6d5e0821..0b4f8621 100644 --- a/.clangd +++ b/.clangd @@ -46,7 +46,7 @@ InlayHints: DeducedTypes: Yes --- If: - PathMatch: "third-party/.*" + PathMatch: "(third-party|build/.*_deps)/.*" Diagnostics: Suppress: "*" UnusedIncludes: None From d7bbc49edef876275812a18c1731d7e970f2cb2f Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Wed, 11 Dec 2024 14:54:54 +0900 Subject: [PATCH 04/27] fix(core/graph/vf2pp): fix bugs and refactor --- include/nuri/core/graph_vf2pp.h | 276 +++++++++++++++----------------- 1 file changed, 128 insertions(+), 148 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 31aa20c3..33ca8676 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -9,6 +9,7 @@ /// @cond #include #include +#include #include #include @@ -22,16 +23,16 @@ namespace nuri { enum class MappingType : int { - kEdgeSubgraph, // Subgraph isomorphism - kNodeSubgraph, // Induced subgraph isomorphism - kIsomorphism, // Graph isomorphism + kSubgraph, // Subgraph isomorphism + kInduced, // Induced subgraph isomorphism + kIsomorphism, // Graph isomorphism }; namespace internal { - template + template int vf2pp_process_bfs_tree(const Graph &query, ArrayXi &order, - ArrayXb &visited, ArrayXi &curr_conn, - AL &query_cnts, int root, int i) { + ArrayXb &visited, AL1 &curr_conn, AL2 &query_cnts, + int root, int i) { order[i] = root; visited[root] = true; @@ -77,20 +78,17 @@ namespace internal { return i; } - template - ArrayXi vf2pp_init_order(const Graph &query, const ArrayXi &qlbl, - const ArrayXi &tlbl, ArrayXi &target_lcnt, - ArrayXi &curr_conn, ArrayXb &visited) { - target_lcnt.setZero(); + template + ArrayXi vf2pp_init_order(const Graph &query, const AL1 &qlbl, + const AL2 &tlbl, ArrayXi &target_lcnt, + AL3 &curr_conn) { + ArrayXi order = ArrayXi::Constant(query.size(), -1); + for (int l: tlbl) ++target_lcnt[l]; - auto query_cnts = target_lcnt(qlbl); - ArrayXi order = ArrayXi::Constant(query.size(), -1); - curr_conn.setZero(); - visited.setZero(); - + ArrayXb visited = ArrayXb::Zero(query.size()); int iorder = 0; for (int i = 0; iorder < query.size() && i < query.size();) { if (visited[i]) { @@ -115,13 +113,14 @@ namespace internal { return order; } - using Vf2ppLabelMap = std::vector>>; + using Vf2ppLabels = std::vector>; + using Vf2ppLabelMap = std::vector; - template + template std::pair - vf2pp_init_r_new_r_inout(const Graph &query, const ArrayXi &qlbl, + vf2pp_init_r_new_r_inout(const Graph &query, const AL1 &qlbl, const ArrayXi &order, ArrayXi &r_inout, - ArrayXi &r_new, ArrayXi &visit_count) { + ArrayXi &r_new, AL2 &visit_count) { // r_inout == _labelTmp1, r_new == _labelTmp2 ABSL_DCHECK_EQ(r_inout.size(), r_new.size()); @@ -144,12 +143,14 @@ namespace internal { for (auto nei: query.node(i)) { const int curr = nei.dst().id(); - const int curr_lbl = qlbl[curr]; + if (r_inout[curr_lbl] > 0) { r_inout_labels[i].push_back({ curr_lbl, r_inout[curr_lbl] }); + r_inout[curr_lbl] = 0; } else if (r_new[curr_lbl] > 0) { r_new_labels[i].push_back({ curr_lbl, r_new[curr_lbl] }); + r_new[curr_lbl] = 0; } if (visit_count[curr] >= 0) @@ -160,22 +161,23 @@ namespace internal { return { r_inout_labels, r_new_labels }; } - template - void vf2pp_add_pair(const Graph &target, ArrayXi &q2t, ArrayXi &conn, + template + void vf2pp_add_pair(const Graph &target, ArrayXi &q2t, AL &conn, const int qi, const int ti) { ABSL_DCHECK_GE(ti, 0); ABSL_DCHECK_LT(ti, target.size()); - conn[ti] = -1; q2t[qi] = ti; + conn[ti] = -1; + for (auto nei: target.node(ti)) { if (conn[nei.dst().id()] != -1) ++conn[nei.dst().id()]; } } - template - void vf2pp_sub_pair(const Graph &target, ArrayXi &q2t, ArrayXi &conn, + template + void vf2pp_sub_pair(const Graph &target, ArrayXi &q2t, AL &conn, const int qi, const int ti) { ABSL_DCHECK_GE(ti, 0); ABSL_DCHECK_LT(ti, target.size()); @@ -193,74 +195,62 @@ namespace internal { } } - template - bool vf2pp_cut_by_labels(typename Graph::ConstNodeRef qn, - typename Graph::ConstNodeRef tn, - ArrayXi &label_tmp1, ArrayXi &label_tmp2, - const ArrayXi &tlbl, const Vf2ppLabelMap &r_inout, - const Vf2ppLabelMap &r_new, const ArrayXi &conn) { - // zero-init - label_tmp1(tlbl)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_inout[qn.id()]) - label_tmp1[lbl] = 0; - if constexpr (kMt != MappingType::kEdgeSubgraph) { - label_tmp2(tlbl)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_new[qn.id()]) - label_tmp2[lbl] = 0; + template + bool vf2pp_r_matches(const std::vector> &r_node, + const ArrayXi &label_tmp) { + return absl::c_none_of(r_node, [&](std::pair p) { + return kMt == MappingType::kIsomorphism ? label_tmp[p.first] != 0 + : label_tmp[p.first] > 0; + }); + } + + template + bool vf2pp_cut_by_labels(typename Graph::ConstNodeRef tn, + const AL1 &tlbl, ArrayXi &r_inout_cnt, + ArrayXi &r_new_cnt, const Vf2ppLabels &r_inout, + const Vf2ppLabels &r_new, const AL2 &conn) { + // zero init + r_inout_cnt(tlbl)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_inout) + r_inout_cnt[lbl] = 0; + if constexpr (kMt != MappingType::kSubgraph) { + r_new_cnt(tlbl)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_new) + r_new_cnt[lbl] = 0; } for (auto tnei: tn) { const int curr = tnei.dst().id(); if (conn[curr] > 0) - --label_tmp1[tlbl[curr]]; - else if constexpr (kMt != MappingType::kEdgeSubgraph) { + --r_inout_cnt[tlbl[curr]]; + else if constexpr (kMt != MappingType::kSubgraph) { if (conn[curr] == 0) - --label_tmp2[tlbl[curr]]; + --r_new_cnt[tlbl[curr]]; } } - for (auto [lbl, cnt]: r_inout[qn.id()]) - label_tmp1[lbl] += cnt; - if constexpr (kMt != MappingType::kEdgeSubgraph) { - for (auto [lbl, cnt]: r_new[qn.id()]) - label_tmp2[lbl] += cnt; + for (auto [lbl, cnt]: r_inout) + r_inout_cnt[lbl] += cnt; + if constexpr (kMt != MappingType::kSubgraph) { + for (auto [lbl, cnt]: r_new) + r_new_cnt[lbl] += cnt; } - if constexpr (kMt == MappingType::kEdgeSubgraph) { - return absl::c_none_of(r_inout[qn.id()], [&](std::pair p) { - return label_tmp1[p.first] > 0; - }); - } - - if constexpr (kMt == MappingType::kNodeSubgraph) { - return absl::c_none_of(r_inout[qn.id()], - [&](std::pair p) { - return label_tmp1[p.first] > 0; - }) - && absl::c_none_of(r_new[qn.id()], [&](std::pair p) { - return label_tmp2[p.first] > 0; - }); - } + const bool r_inout_match = vf2pp_r_matches(r_inout, r_inout_cnt); + if constexpr (kMt == MappingType::kSubgraph) + return r_inout_match; - if constexpr (kMt == MappingType::kIsomorphism) { - return absl::c_none_of(r_inout[qn.id()], - [&](std::pair p) { - return label_tmp1[p.first] != 0; - }) - && absl::c_none_of(r_new[qn.id()], [&](std::pair p) { - return label_tmp2[p.first] != 0; - }); - } - - ABSL_UNREACHABLE(); + return r_inout_match && vf2pp_r_matches(r_new, r_new_cnt); } - template - bool vf2pp_feas(typename Graph::ConstNodeRef qn, - typename Graph::ConstNodeRef tn, const ArrayXi &qlbl, - const ArrayXi &tlbl, ArrayXi &q2t, ArrayXi &conn, - ArrayXi &label_tmp1, ArrayXi &label_tmp2, - const Vf2ppLabelMap &r_inout, const Vf2ppLabelMap &r_new) { + template + bool vf2pp_feas(typename Graph::ConstNodeRef qn, + typename Graph::ConstNodeRef tn, + const BinaryPred &match, const AL1 &qlbl, const AL2 &tlbl, + ArrayXi &q2t, AL3 &conn, ArrayXi &r_inout_cnt, + ArrayXi &r_new_cnt, const Vf2ppLabelMap &r_inout, + const Vf2ppLabelMap &r_new) { if (qlbl[qn.id()] != tlbl[tn.id()]) return false; @@ -273,7 +263,7 @@ namespace internal { const int curr_conn = conn[tnei.dst().id()]; if (curr_conn < -1) ++conn[tnei.dst().id()]; - else if constexpr (kMt != MappingType::kEdgeSubgraph) { + else if constexpr (kMt != MappingType::kSubgraph) { if (curr_conn == -1) is_iso = false; } @@ -295,7 +285,7 @@ namespace internal { const int curr_conn = conn[ti]; conn[ti] = -1; - if constexpr (kMt == MappingType::kEdgeSubgraph) { + if constexpr (kMt == MappingType::kSubgraph) { if (curr_conn < -1) return false; } else { @@ -303,30 +293,30 @@ namespace internal { } } - return vf2pp_cut_by_labels(qn, tn, label_tmp1, label_tmp2, - tlbl, r_inout, r_new, conn); + return match(qn, tn) + && vf2pp_cut_by_labels(tn, tlbl, r_inout_cnt, r_new_cnt, + r_inout[qn.id()], r_new[qn.id()], + conn); } - template + template std::pair - vf2pp_ext_match(const Graph &query, const Graph &target, - const ArrayXi &qlbl, const ArrayXi &tlbl, - const ArrayXi &order, ArrayXi &conn, ArrayXi &label_tmp1, - ArrayXi &label_tmp2, const Vf2ppLabelMap &r_inout, + vf2pp_ext_match(const Graph &query, const Graph &target, + const BinaryPred &match, const AL1 &qlbl, const AL2 &tlbl, + const ArrayXi &order, AL3 &conn, ArrayXi &r_inout_cnt, + ArrayXi &r_new_cnt, const Vf2ppLabelMap &r_inout, const Vf2ppLabelMap &r_new) { ArrayXi q2t = ArrayXi::Constant(query.size(), -1); Array2Xi src_nei = Array2Xi::Constant(2, target.size(), -1); - conn.setZero(); - label_tmp1.setZero(); - label_tmp2.setZero(); for (int depth = 0; depth >= 0;) { if (depth == order.size()) return { q2t, true }; - auto qn = query.node(order[depth]); - + const auto qn = query.node(order[depth]); const int ti = q2t[qn.id()]; + int tj = src_nei(0, depth), tk = src_nei(1, depth); if (tk >= 0) { ABSL_DCHECK_NE(ti, tj); @@ -338,36 +328,24 @@ namespace internal { qk = absl::c_find_if( qn, [&](auto nei) { return q2t[nei.dst().id()] >= 0; }) - qn.begin(); + } else { + vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); } if (qk == qn.degree() || ti >= 0) { - int ti2 = 0; - if (ti >= 0) { - ti2 = ti + 1; - vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); - } - - if constexpr (kMt != MappingType::kEdgeSubgraph) { - ti2 = std::find_if(target.begin() + ti2, target.end(), - [&](auto tn) { - return conn[tn.id()] == 0 - && vf2pp_feas( - qn, tn, qlbl, tlbl, q2t, conn, - label_tmp1, label_tmp2, r_inout, - r_new); - }) - - target.begin(); - } else { - ti2 = std::find_if(target.begin() + ti2, target.end(), - [&](auto tn) { - return conn[tn.id()] >= 0 - && vf2pp_feas( - qn, tn, qlbl, tlbl, q2t, conn, - label_tmp1, label_tmp2, r_inout, - r_new); - }) - - target.begin(); - } + const int ti2 = + std::find_if( + target.begin() + ti + 1, target.end(), + [&](auto tn) { + bool candidate = kMt == MappingType::kSubgraph + ? conn[tn.id()] >= 0 + : conn[tn.id()] == 0; + return candidate + && vf2pp_feas( + qn, tn, match, qlbl, tlbl, q2t, conn, + r_inout_cnt, r_new_cnt, r_inout, r_new); + }) + - target.begin(); if (ti2 < target.size()) { vf2pp_add_pair(target, q2t, conn, qn.id(), ti2); @@ -379,16 +357,16 @@ namespace internal { continue; } - tj = q2t[qn[qk].dst().id()]; + src_nei(0, depth) = tj = q2t[qn[qk].dst().id()]; tk = 0; } for (; tk < target.degree(tj); ++tk) { auto tn = target.node(tj)[tk].dst(); if (conn[tn.id()] > 0 - && vf2pp_feas(qn, tn, qlbl, tlbl, q2t, conn, - label_tmp1, label_tmp2, r_inout, - r_new)) { + && vf2pp_feas( + qn, tn, match, qlbl, tlbl, q2t, conn, r_inout_cnt, r_new_cnt, + r_inout, r_new)) { vf2pp_add_pair(target, q2t, conn, qn.id(), tn.id()); break; } @@ -397,7 +375,7 @@ namespace internal { if (tk < target.degree(tj)) { src_nei(1, depth++) = tk; } else { - src_nei.col(depth--).fill(-1); + src_nei(1, depth--) = -1; } } @@ -405,39 +383,41 @@ namespace internal { } } // namespace internal -template -std::pair vf2pp(const Graph &query, - const Graph &target, const ArrayXi &qlbl, - const ArrayXi &tlbl, MappingType mt) { +template +std::pair vf2pp(const Graph &query, + const Graph &target, + const BinaryPred &match, const AL1 &qlbl, + const AL2 &tlbl, MappingType mt) { const int nlabel = nuri::max(qlbl.maxCoeff(), tlbl.maxCoeff()) + 1; - ArrayXi label_tmp1(nlabel), label_tmp2(nlabel); - - ArrayXi query_tmp(query.size()); - ArrayXb visited(query.size()); - - ArrayXi target_tmp(target.size()); + ArrayXi label_tmp1 = ArrayXi::Zero(nlabel), + label_tmp2 = ArrayXi::Zero(nlabel); + ArrayXi node_tmp = ArrayXi::Zero(nuri::max(query.size(), target.size())); + auto query_tmp = node_tmp.head(query.size()); // _order - ArrayXi order = internal::vf2pp_init_order(query, qlbl, tlbl, label_tmp1, - query_tmp, visited); + ArrayXi order = + internal::vf2pp_init_order(query, qlbl, tlbl, label_tmp1, query_tmp); // _rInOutLabels1, _rNewLabels1 auto [r_inout, r_new] = internal::vf2pp_init_r_new_r_inout( query, qlbl, order, label_tmp1, label_tmp2, query_tmp); + node_tmp.head(nuri::min(query.size(), target.size())).setZero(); + auto target_tmp = node_tmp.head(target.size()); switch (mt) { - case MappingType::kEdgeSubgraph: - return internal::vf2pp_ext_match( - query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, - r_inout, r_new); - case MappingType::kNodeSubgraph: - return internal::vf2pp_ext_match( - query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, - r_inout, r_new); + case MappingType::kSubgraph: + return internal::vf2pp_ext_match( + query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, + label_tmp2, r_inout, r_new); + case MappingType::kInduced: + return internal::vf2pp_ext_match( + query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, + label_tmp2, r_inout, r_new); case MappingType::kIsomorphism: return internal::vf2pp_ext_match( - query, target, qlbl, tlbl, order, target_tmp, label_tmp1, label_tmp2, - r_inout, r_new); + query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, + label_tmp2, r_inout, r_new); } ABSL_UNREACHABLE(); From d92b67cc5ff78145032ad5f1e60b26a5d7dc813b Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Wed, 11 Dec 2024 14:55:13 +0900 Subject: [PATCH 05/27] test(core/graph/vf2pp): copy more tests from LEMON --- test/core/graph_vf2pp_test.cpp | 480 +++++++++++++++++++++++++++++---- 1 file changed, 422 insertions(+), 58 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index d9940825..c092fd93 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -5,6 +5,12 @@ #include "nuri/core/graph_vf2pp.h" +#include +#include +#include + +#include + #include #include "nuri/eigen_config.h" @@ -14,7 +20,6 @@ namespace nuri { namespace { using GT = Graph; -// lemon node id int lid(const GT &g, int i) { return g[i].data(); } @@ -23,8 +28,30 @@ auto lemon_add_edge(GT &g, int src, int dst, int data) { return g.add_edge(lid(g, src), lid(g, dst), data); } -Graph lemon_petersen() { - Graph g; +GT lemon_c_n(int n) { + GT g; + for (int i = n - 1; i >= 0; --i) + g.add_node(i); + + for (int i = n - 1; i >= 0; --i) + lemon_add_edge(g, i, (i + 1) % n, i); + + return g; +} + +GT lemon_p_n(int n) { + GT g; + for (int i = n - 1; i >= 0; --i) + g.add_node(i); + + for (int i = n - 2; i >= 0; --i) + lemon_add_edge(g, i, i + 1, i); + + return g; +} + +GT lemon_petersen() { + GT g; for (int i = 9; i >= 0; --i) g.add_node(i); @@ -47,17 +74,47 @@ Graph lemon_petersen() { return g; } +GT lemon_c5_petersen() { + GT g; + for (int i = 14; i >= 0; --i) + g.add_node(i); + + lemon_add_edge(g, 11, 13, 19); + lemon_add_edge(g, 14, 12, 18); + lemon_add_edge(g, 14, 11, 17); + lemon_add_edge(g, 10, 12, 16); + lemon_add_edge(g, 10, 13, 15); + lemon_add_edge(g, 9, 14, 14); + lemon_add_edge(g, 8, 13, 13); + lemon_add_edge(g, 7, 12, 12); + lemon_add_edge(g, 6, 11, 11); + lemon_add_edge(g, 5, 10, 10); + lemon_add_edge(g, 9, 5, 9); + lemon_add_edge(g, 8, 9, 8); + lemon_add_edge(g, 7, 8, 7); + lemon_add_edge(g, 6, 7, 6); + lemon_add_edge(g, 5, 6, 5); + + lemon_add_edge(g, 4, 0, 4); + lemon_add_edge(g, 3, 4, 3); + lemon_add_edge(g, 2, 3, 2); + lemon_add_edge(g, 1, 2, 1); + lemon_add_edge(g, 0, 1, 0); + + return g; +} + TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { - Graph g = lemon_petersen(); + GT g = lemon_petersen(); ArrayXi order = ArrayXi::Constant(g.size(), -1); ArrayXb visited = ArrayXb::Zero(g.size()); ArrayXi curr_conn = ArrayXi::Zero(g.size()); - ArrayXi target_lcnt(1); - target_lcnt[0] = 10; - ArrayXi qlbl = ArrayXi::Zero(g.size()); - auto query_cnts = target_lcnt(qlbl); + ArrayXi lcnt(1); + lcnt[0] = 10; + ArrayXi lbl = ArrayXi::Zero(g.size()); + auto query_cnts = lcnt(lbl); const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, query_cnts, 0, 0); @@ -70,21 +127,21 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { } TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { - Graph g = lemon_petersen(); + GT g = lemon_petersen(); ArrayXi order = ArrayXi::Constant(g.size(), -1); ArrayXb visited = ArrayXb::Zero(g.size()); ArrayXi curr_conn = ArrayXi::Zero(g.size()); - ArrayXi target_lcnt(2); - target_lcnt[0] = 4; - target_lcnt[1] = 6; + ArrayXi lcnt(2); + lcnt[0] = 4; + lcnt[1] = 6; ArrayXi qlbl(g.size()); qlbl.head(6).setOnes(); qlbl.tail(4).setZero(); - auto query_cnts = target_lcnt(qlbl); + auto query_cnts = lcnt(qlbl); const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, query_cnts, 6, 0); @@ -96,50 +153,357 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { EXPECT_EQ(lid(g, order[i]), expected_order[i]); } -// TEST(VF2PPTest, Playground) { -// Graph g1; -// g1.add_node(1); -// g1.add_node(2); -// g1.add_node(3); -// g1.add_node(4); -// g1.add_node(5); -// g1.add_edge(0, 1, 1); -// g1.add_edge(1, 2, 2); -// g1.add_edge(2, 3, 3); -// g1.add_edge(3, 4, 4); -// g1.add_edge(4, 0, 5); - -// Graph g2; -// g2.add_node(1); -// g2.add_node(2); -// g2.add_node(3); -// g2.add_node(4); -// g2.add_node(5); -// g2.add_node(1); -// g2.add_node(2); -// g2.add_node(3); -// g2.add_node(4); -// g2.add_node(5); -// g2.add_edge(0, 1, 1); -// g2.add_edge(1, 2, 2); -// g2.add_edge(2, 3, 3); -// g2.add_edge(3, 4, 4); -// g2.add_edge(4, 0, 5); -// g2.add_edge(0, 5, 6); -// g2.add_edge(1, 6, 7); -// g2.add_edge(2, 7, 8); -// g2.add_edge(3, 8, 9); -// g2.add_edge(4, 9, 10); -// g2.add_edge(5, 8, 11); -// g2.add_edge(5, 7, 12); -// g2.add_edge(9, 6, 13); -// g2.add_edge(9, 7, 14); -// g2.add_edge(6, 8, 15); - -// ArrayXi qlbl = ArrayXi::Zero(g1.size()), tlbl = ArrayXi::Zero(g2.size()); - -// auto [map, ok] = vf2pp(g1, g2, qlbl, tlbl, MappingType::kEdgeSubgraph); -// EXPECT_TRUE(ok); -// } +TEST(VF2PPComponentTest, InitOrderC5Petersen) { + GT g = lemon_c5_petersen(); + + ArrayXi lbl(g.size()); + lbl << 1, 1, 1, 1, 1, // + 1, 0, 0, 0, 0, // + 4, 3, 2, 1, 0; + + ArrayXi lcnt = ArrayXi::Zero(5), curr_conn = ArrayXi::Zero(g.size()); + ArrayXi order = internal::vf2pp_init_order(g, lbl, lbl, lcnt, curr_conn); + + ArrayXi expected_order(15); + expected_order << 4, 3, 0, 1, 2, 8, 7, 9, 13, 11, 10, 14, 5, 12, 6; + for (int i = 0; i < order.size(); ++i) + EXPECT_EQ(lid(g, order[i]), expected_order[i]); +} + +TEST(VF2PPComponentTest, InitRNewRInoutC5Petersen) { + GT g = lemon_c5_petersen(); + ArrayXi lbl(g.size()); + lbl << 1, 1, 1, 1, 1, // + 1, 0, 0, 0, 0, // + 4, 3, 2, 1, 0; + + ArrayXi ltmp1(5), ltmp2(5); + ArrayXi visit_count(g.size()); + ArrayXi order(15); + order << 10, 11, 14, 13, 12, 6, 7, 5, 1, 3, 4, 0, 9, 2, 8; + + auto [r_inout, r_new] = internal::vf2pp_init_r_new_r_inout( + g, lbl, order, ltmp1, ltmp2, visit_count); + + internal::Vf2ppLabelMap r_new_expected { + { { 1, 1 } }, + {}, + {}, + { { 2, 1 } }, + { { 0, 1 }, { 3, 1 } }, + {}, + {}, + { { 1, 1 }, { 0, 1 } }, + { { 1, 2 }, { 0, 1 } }, + { { 1, 1 }, { 0, 1 } }, + {}, + {}, + {}, + { { 1, 2 } }, + {}, + }; + internal::Vf2ppLabelMap r_inout_expected { + {}, + { { 2, 1 } }, + {}, + {}, + {}, + { { 0, 1 } }, + {}, + {}, + {}, + {}, + { { 1, 1 }, { 0, 1 } }, + { { 1, 1 }, { 0, 1 } }, + {}, + {}, + { { 1, 1 } }, + }; + + constexpr auto vector_eq = absl::c_equal>, + std::vector>>; + + for (int i = 0; i < g.size(); ++i) { + int j = lid(g, i); + EXPECT_PRED2(vector_eq, r_new[i], r_new_expected[j]); + EXPECT_PRED2(vector_eq, r_inout[i], r_inout_expected[j]); + } +} + +TEST(VF2PPSingleLabelSimpleMatchTest, PetersenSubgraph) { + GT target = lemon_petersen(); + ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); + ArrayXi expected_map(10); + + auto run_vf2pp = [&](const GT &query) { + return vf2pp( + query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), + tlbl, MappingType::kSubgraph); + }; + + { + GT c5 = lemon_c_n(5); + auto [map, ok] = run_vf2pp(c5); + EXPECT_TRUE(ok); + + expected_map.head(5) << 7, 5, 8, 6, 9; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(c5, i)]), expected_map[i]); + } + + { + GT c7 = lemon_c_n(7); + auto [_, ok] = run_vf2pp(c7); + EXPECT_FALSE(ok); + } + + { + GT p10 = lemon_p_n(10); + auto [map, ok] = run_vf2pp(p10); + EXPECT_TRUE(ok); + + expected_map << 2, 1, 0, 4, 3, 8, 5, 7, 9, 6; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(p10, i)]), expected_map[i]); + } + + { + GT c10 = lemon_c_n(10); + auto [_, ok] = run_vf2pp(c10); + EXPECT_FALSE(ok); + } + + { + auto [map, ok] = run_vf2pp(target); + EXPECT_TRUE(ok); + + absl::c_iota(expected_map, 0); + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + } +} + +TEST(VF2PPSingleLabelSimpleMatchTest, PetersenInduced) { + GT target = lemon_petersen(); + ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); + ArrayXi expected_map(10); + + auto run_vf2pp = [&](const GT &query) { + return vf2pp( + query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), + tlbl, MappingType::kInduced); + }; + + { + GT c5 = lemon_c_n(5); + auto [map, ok] = run_vf2pp(c5); + EXPECT_TRUE(ok); + + expected_map.head(5) << 7, 5, 8, 6, 9; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(c5, i)]), expected_map[i]); + } + + { + GT c7 = lemon_c_n(7); + auto [_, ok] = run_vf2pp(c7); + EXPECT_FALSE(ok); + } + + { + GT p10 = lemon_p_n(10); + auto [_, ok] = run_vf2pp(p10); + EXPECT_FALSE(ok); + } + + { + GT c10 = lemon_c_n(10); + auto [_, ok] = run_vf2pp(c10); + EXPECT_FALSE(ok); + } + + { + auto [map, ok] = run_vf2pp(target); + EXPECT_TRUE(ok); + + absl::c_iota(expected_map, 0); + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + } +} + +TEST(VF2PPSingleLabelSimpleMatchTest, PetersenIsomorphism) { + GT target = lemon_petersen(); + ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); + ArrayXi expected_map(10); + + auto run_vf2pp = [&](const GT &query) { + return vf2pp( + query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), + tlbl, MappingType::kIsomorphism); + }; + + { + GT c5 = lemon_c_n(5); + auto [_, ok] = run_vf2pp(c5); + EXPECT_FALSE(ok); + } + + { + GT c7 = lemon_c_n(7); + auto [_, ok] = run_vf2pp(c7); + EXPECT_FALSE(ok); + } + + { + GT p10 = lemon_p_n(10); + auto [_, ok] = run_vf2pp(p10); + EXPECT_FALSE(ok); + } + + { + GT c10 = lemon_c_n(10); + auto [_, ok] = run_vf2pp(c10); + EXPECT_FALSE(ok); + } + + { + auto [map, ok] = run_vf2pp(target); + EXPECT_TRUE(ok); + + absl::c_iota(expected_map, 0); + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + } +} + +TEST(VF2PPSingleLabelSimpleMatchTest, C10P10) { + GT target = lemon_c_n(10); + ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); + ArrayXi expected_map(10); + + auto run_vf2pp = [&](const GT &query, MappingType mt) { + return vf2pp( + query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), + tlbl, mt); + }; + + { + GT petersen = lemon_petersen(); + auto [_, ok] = run_vf2pp(petersen, MappingType::kSubgraph); + EXPECT_FALSE(ok); + } + + { + GT p10 = lemon_p_n(10); + auto [map, ok] = run_vf2pp(p10, MappingType::kSubgraph); + EXPECT_TRUE(ok); + + expected_map << 7, 6, 5, 4, 3, 2, 1, 0, 9, 8; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(p10, i)]), expected_map[i]); + + std::tie(std::ignore, ok) = run_vf2pp(p10, MappingType::kInduced); + EXPECT_FALSE(ok); + + std::tie(std::ignore, ok) = run_vf2pp(p10, MappingType::kIsomorphism); + EXPECT_FALSE(ok); + + // NOLINTNEXTLINE(*-suspicious-call-argument) + std::tie(std::ignore, ok) = vf2pp( + target, p10, [](auto, auto) { return true; }, qlbl, tlbl, + MappingType::kIsomorphism); + EXPECT_FALSE(ok); + } + + { + auto [map, ok] = run_vf2pp(target, MappingType::kIsomorphism); + EXPECT_TRUE(ok); + + absl::c_iota(expected_map, 0); + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + } +} + +TEST(VF2PPMultiLabelSimpleMatchTest, C5Petersen) { + GT c5 = lemon_c_n(5), petersen = lemon_petersen(); + + ArrayXi c5_lbl(5); + c5_lbl << 4, 3, 2, 1, 0; + + ArrayXi petersen_lbl1(10); + petersen_lbl1.head(6).fill(1); + petersen_lbl1.tail(4).fill(0); + + ArrayXi petersen_lbl2(10); + petersen_lbl2.head(5) = c5_lbl; + petersen_lbl2.tail(5) = c5_lbl; + + { + auto [_, ok] = vf2pp( + c5, petersen, [](auto, auto) { return true; }, c5_lbl, petersen_lbl1, + MappingType::kSubgraph); + EXPECT_FALSE(ok); + } + + { + auto [map, ok] = vf2pp( + c5, petersen, [](auto, auto) { return true; }, c5_lbl, petersen_lbl2, + MappingType::kSubgraph); + ASSERT_TRUE(ok); + + ArrayXi expected_map(5); + expected_map << 0, 1, 2, 3, 4; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(petersen, map[lid(c5, i)]), expected_map[i]); + } +} + +TEST(VF2PPMultiLabelSimpleMatchTest, PetersenPetersen) { + GT petersen = lemon_petersen(); + + ArrayXi petersen_lbl1(10); + petersen_lbl1.head(6).fill(1); + petersen_lbl1.tail(4).fill(0); + + ArrayXi petersen_lbl2(10); + petersen_lbl2.head(5) << 4, 3, 2, 1, 0; + petersen_lbl2.tail(5) = petersen_lbl2.head(5); + + ArrayXi expected_map(10); + absl::c_iota(expected_map, 0); + + for (MappingType mt: { MappingType::kSubgraph, MappingType::kInduced, + MappingType::kIsomorphism }) { + auto [map, ok] = vf2pp( + petersen, petersen, [](auto, auto) { return true; }, petersen_lbl1, + petersen_lbl1, mt); + ASSERT_TRUE(ok) << static_cast(mt); + + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(petersen, map[lid(petersen, i)]), expected_map[i]) + << static_cast(mt); + + std::tie(map, ok) = vf2pp( + petersen, petersen, [](auto, auto) { return true; }, petersen_lbl2, + petersen_lbl2, mt); + ASSERT_TRUE(ok) << static_cast(mt); + + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(lid(petersen, map[lid(petersen, i)]), expected_map[i]) + << static_cast(mt); + + std::tie(std::ignore, ok) = vf2pp( + petersen, petersen, [](auto, auto) { return true; }, petersen_lbl1, + petersen_lbl2, mt); + EXPECT_FALSE(ok) << static_cast(mt); + + std::tie(std::ignore, ok) = vf2pp( + petersen, petersen, [](auto, auto) { return true; }, petersen_lbl2, + petersen_lbl1, mt); + EXPECT_FALSE(ok) << static_cast(mt); + } +} } // namespace } // namespace nuri From 5abb6fdce9ed98cfad2e8869ce9b961edd47b589 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Wed, 11 Dec 2024 18:49:20 +0900 Subject: [PATCH 06/27] refactor(core/graph/vf2pp): reimplement with OOP interface --- include/nuri/core/graph_vf2pp.h | 456 +++++++++++++++++++------------- 1 file changed, 270 insertions(+), 186 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 33ca8676..fb080cfb 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -78,10 +78,11 @@ namespace internal { return i; } - template - ArrayXi vf2pp_init_order(const Graph &query, const AL1 &qlbl, - const AL2 &tlbl, ArrayXi &target_lcnt, - AL3 &curr_conn) { + template + ArrayXi vf2pp_init_order(const Graph &query, ConstRef qlbl, + ConstRef tlbl, ArrayXi &target_lcnt, + // NOLINTNEXTLINE(*-missing-std-forward) + AL &&curr_conn) { ArrayXi order = ArrayXi::Constant(query.size(), -1); for (int l: tlbl) @@ -116,11 +117,12 @@ namespace internal { using Vf2ppLabels = std::vector>; using Vf2ppLabelMap = std::vector; - template + template std::pair - vf2pp_init_r_new_r_inout(const Graph &query, const AL1 &qlbl, + vf2pp_init_r_new_r_inout(const Graph &query, ConstRef qlbl, const ArrayXi &order, ArrayXi &r_inout, - ArrayXi &r_new, AL2 &visit_count) { + // NOLINTNEXTLINE(*-missing-std-forward) + ArrayXi &r_new, AL &&visit_count) { // r_inout == _labelTmp1, r_new == _labelTmp2 ABSL_DCHECK_EQ(r_inout.size(), r_new.size()); @@ -161,108 +163,236 @@ namespace internal { return { r_inout_labels, r_new_labels }; } - template - void vf2pp_add_pair(const Graph &target, ArrayXi &q2t, AL &conn, - const int qi, const int ti) { + template + bool vf2pp_r_matches(const Vf2ppLabels &r_node, const ArrayXi &label_tmp) { + return absl::c_none_of(r_node, [&](std::pair p) { + return kMt == MappingType::kIsomorphism ? label_tmp[p.first] != 0 + : label_tmp[p.first] > 0; + }); + } +} // namespace internal + +template +class VF2pp { +private: + const Graph &query() const { return *query_; } + const Graph &target() const { return *target_; } + + auto query_tmp() { return node_tmp_.head(query_->size()); } + + auto conn() { return node_tmp_.head(target_->size()); } + + auto curr_query() const { return query_->node(order_[depth_]); } + + int mapped_target() const { return mapping_[curr_query().id()]; } + + int target_src() const { return src_nei_(0, depth_); } + void update_target_src(int idx) { + ABSL_DCHECK_GE(idx, 0); + ABSL_DCHECK_LT(idx, target().size()); + + src_nei_(0, depth_) = idx; + } + + int target_nei_idx() const { return src_nei_(1, depth_); } + void update_target_nei_idx(int idx) { + ABSL_DCHECK_GE(target_src(), 0); + ABSL_DCHECK_LT(target_src(), target().size()); + + ABSL_DCHECK_GE(idx, 0); + ABSL_DCHECK_LT(idx, target().degree(target_src())); + + src_nei_(1, depth_) = idx; + } + void invalidate_target_nei_idx() { src_nei_(1, depth_) = -1; } + + ArrayXi &r_inout_cnt() { return label_tmp1_; } + ArrayXi &r_new_cnt() { return label_tmp2_; } + +public: + template + VF2pp(const Graph &query, const Graph &target, + AL1 &&query_lbl, AL2 &&target_lbl) + : query_(&query), target_(&target), // + qlbl_(std::forward(query_lbl)), + tlbl_(std::forward(target_lbl)), + mapping_(ArrayXi::Constant(query.size(), -1)), + src_nei_(Array2Xi::Constant(2, target.size(), -1)), + node_tmp_(ArrayXi::Zero(nuri::max(query.size(), target.size()))) { + const int nlabel = nuri::max(qlbl_.maxCoeff(), tlbl_.maxCoeff()) + 1; + + label_tmp1_ = ArrayXi::Zero(nlabel); + label_tmp2_ = ArrayXi::Zero(nlabel); + + order_ = internal::vf2pp_init_order(query, qlbl_, tlbl_, label_tmp1_, + query_tmp()); + std::tie(r_inout_, r_new_) = internal::vf2pp_init_r_new_r_inout( + query, qlbl_, order_, r_inout_cnt(), r_new_cnt(), query_tmp()); + + node_tmp_.head(nuri::min(query.size(), target.size())).setZero(); + } + + template + bool next(const BinaryPred &match) { + while (depth_ >= 0) { + if (depth_ == query().size()) { + --depth_; + return true; + } + + const auto qn = curr_query(); + const int ti = mapped_target(); + + int tj = target_src(), tk = target_nei_idx(); + if (tk >= 0) { + ABSL_DCHECK_NE(ti, tj); + sub_pair(qn.id(), ti); + ++tk; + } else { + int qk = 0; + if (ti < 0) { + qk = absl::c_find_if( + qn, [&](auto nei) { return mapping_[nei.dst().id()] >= 0; }) + - qn.begin(); + } else { + sub_pair(qn.id(), ti); + } + + if (qk == qn.degree() || ti >= 0) { + const int ti2 = + std::find_if(target().begin() + ti + 1, target().end(), + [&](auto tn) { + bool candidate = kMt == MappingType::kSubgraph + ? conn()[tn.id()] >= 0 + : conn()[tn.id()] == 0; + return candidate && feas(qn, tn, match); + }) + - target().begin(); + + if (ti2 < target().size()) { + add_pair(qn.id(), ti2); + ++depth_; + } else { + --depth_; + } + + continue; + } + + tj = mapping_[qn[qk].dst().id()]; + tk = 0; + update_target_src(tj); + } + + for (; tk < target().degree(tj); ++tk) { + auto tn = target().node(tj)[tk].dst(); + if (conn()[tn.id()] > 0 && feas(qn, tn, match)) { + add_pair(qn.id(), tn.id()); + break; + } + } + + if (tk < target().degree(tj)) { + update_target_nei_idx(tk); + ++depth_; + } else { + invalidate_target_nei_idx(); + --depth_; + } + } + + return false; + } + + const ArrayXi &mapping() const & { return mapping_; } + + ArrayXi &&mapping() && { return std::move(mapping_); } + +private: + void add_pair(const int qi, const int ti) { ABSL_DCHECK_GE(ti, 0); - ABSL_DCHECK_LT(ti, target.size()); + ABSL_DCHECK_LT(ti, target().size()); - q2t[qi] = ti; - conn[ti] = -1; + mapping_[qi] = ti; + conn()[ti] = -1; - for (auto nei: target.node(ti)) { - if (conn[nei.dst().id()] != -1) - ++conn[nei.dst().id()]; + for (auto nei: target().node(ti)) { + if (conn()[nei.dst().id()] != -1) + ++conn()[nei.dst().id()]; } } - template - void vf2pp_sub_pair(const Graph &target, ArrayXi &q2t, AL &conn, - const int qi, const int ti) { + void sub_pair(const int qi, const int ti) { ABSL_DCHECK_GE(ti, 0); - ABSL_DCHECK_LT(ti, target.size()); + ABSL_DCHECK_LT(ti, target().size()); - q2t[qi] = -1; - conn[ti] = 0; + mapping_[qi] = -1; + conn()[ti] = 0; - for (auto nei: target.node(ti)) { - int curr_conn = conn[nei.dst().id()]; + for (auto nei: target().node(ti)) { + int curr_conn = conn()[nei.dst().id()]; if (curr_conn > 0) { - --conn[nei.dst().id()]; + --conn()[nei.dst().id()]; } else if (curr_conn == -1) { - ++conn[ti]; + ++conn()[ti]; } } } - template - bool vf2pp_r_matches(const std::vector> &r_node, - const ArrayXi &label_tmp) { - return absl::c_none_of(r_node, [&](std::pair p) { - return kMt == MappingType::kIsomorphism ? label_tmp[p.first] != 0 - : label_tmp[p.first] > 0; - }); - } - - template - bool vf2pp_cut_by_labels(typename Graph::ConstNodeRef tn, - const AL1 &tlbl, ArrayXi &r_inout_cnt, - ArrayXi &r_new_cnt, const Vf2ppLabels &r_inout, - const Vf2ppLabels &r_new, const AL2 &conn) { + bool cut_by_labels(const typename Graph::ConstNodeRef qn, + const typename Graph::ConstNodeRef tn) { // zero init - r_inout_cnt(tlbl)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_inout) - r_inout_cnt[lbl] = 0; + r_inout_cnt()(tlbl_)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_inout_[qn.id()]) + r_inout_cnt()[lbl] = 0; if constexpr (kMt != MappingType::kSubgraph) { - r_new_cnt(tlbl)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_new) - r_new_cnt[lbl] = 0; + r_new_cnt()(tlbl_)(as_index(tn)).setZero(); + for (auto [lbl, cnt]: r_new_[qn.id()]) + r_new_cnt()[lbl] = 0; } for (auto tnei: tn) { const int curr = tnei.dst().id(); - if (conn[curr] > 0) - --r_inout_cnt[tlbl[curr]]; + if (conn()[curr] > 0) + --r_inout_cnt()[tlbl_[curr]]; else if constexpr (kMt != MappingType::kSubgraph) { - if (conn[curr] == 0) - --r_new_cnt[tlbl[curr]]; + if (conn()[curr] == 0) + --r_new_cnt()[tlbl_[curr]]; } } - for (auto [lbl, cnt]: r_inout) - r_inout_cnt[lbl] += cnt; + for (auto [lbl, cnt]: r_inout_[qn.id()]) + r_inout_cnt()[lbl] += cnt; if constexpr (kMt != MappingType::kSubgraph) { - for (auto [lbl, cnt]: r_new) - r_new_cnt[lbl] += cnt; + for (auto [lbl, cnt]: r_new_[qn.id()]) + r_new_cnt()[lbl] += cnt; } - const bool r_inout_match = vf2pp_r_matches(r_inout, r_inout_cnt); + const bool r_inout_match = + internal::vf2pp_r_matches(r_inout_[qn.id()], r_inout_cnt()); if constexpr (kMt == MappingType::kSubgraph) return r_inout_match; - return r_inout_match && vf2pp_r_matches(r_new, r_new_cnt); + return r_inout_match + && internal::vf2pp_r_matches(r_new_[qn.id()], r_new_cnt()); } - template - bool vf2pp_feas(typename Graph::ConstNodeRef qn, - typename Graph::ConstNodeRef tn, - const BinaryPred &match, const AL1 &qlbl, const AL2 &tlbl, - ArrayXi &q2t, AL3 &conn, ArrayXi &r_inout_cnt, - ArrayXi &r_new_cnt, const Vf2ppLabelMap &r_inout, - const Vf2ppLabelMap &r_new) { - if (qlbl[qn.id()] != tlbl[tn.id()]) + template + bool feas(const typename Graph::ConstNodeRef qn, + const typename Graph::ConstNodeRef tn, + const BinaryPred &match) { + if (qlbl_[qn.id()] != tlbl_[tn.id()]) return false; for (auto qnei: qn) - if (q2t[qnei.dst().id()] >= 0) - --conn[q2t[qnei.dst().id()]]; + if (mapping_[qnei.dst().id()] >= 0) + --conn()[mapping_[qnei.dst().id()]]; bool is_iso = true; for (auto tnei: tn) { - const int curr_conn = conn[tnei.dst().id()]; + const int curr_conn = conn()[tnei.dst().id()]; if (curr_conn < -1) - ++conn[tnei.dst().id()]; + ++conn()[tnei.dst().id()]; else if constexpr (kMt != MappingType::kSubgraph) { if (curr_conn == -1) is_iso = false; @@ -271,20 +401,20 @@ namespace internal { if (!is_iso) { for (auto qnei: qn) { - const int ti = q2t[qnei.dst().id()]; + const int ti = mapping_[qnei.dst().id()]; if (ti >= 0) - conn[ti] = -1; + conn()[ti] = -1; } return false; } for (auto qnei: qn) { - const int ti = q2t[qnei.dst().id()]; - if (ti < 0 || conn[ti] == -1) + const int ti = mapping_[qnei.dst().id()]; + if (ti < 0 || conn()[ti] == -1) continue; - const int curr_conn = conn[ti]; - conn[ti] = -1; + const int curr_conn = conn()[ti]; + conn()[ti] = -1; if constexpr (kMt == MappingType::kSubgraph) { if (curr_conn < -1) return false; @@ -293,134 +423,88 @@ namespace internal { } } - return match(qn, tn) - && vf2pp_cut_by_labels(tn, tlbl, r_inout_cnt, r_new_cnt, - r_inout[qn.id()], r_new[qn.id()], - conn); + return match(qn, tn) && cut_by_labels(qn, tn); } - template - std::pair - vf2pp_ext_match(const Graph &query, const Graph &target, - const BinaryPred &match, const AL1 &qlbl, const AL2 &tlbl, - const ArrayXi &order, AL3 &conn, ArrayXi &r_inout_cnt, - ArrayXi &r_new_cnt, const Vf2ppLabelMap &r_inout, - const Vf2ppLabelMap &r_new) { - ArrayXi q2t = ArrayXi::Constant(query.size(), -1); - - Array2Xi src_nei = Array2Xi::Constant(2, target.size(), -1); - for (int depth = 0; depth >= 0;) { - if (depth == order.size()) - return { q2t, true }; - - const auto qn = query.node(order[depth]); - const int ti = q2t[qn.id()]; - - int tj = src_nei(0, depth), tk = src_nei(1, depth); - if (tk >= 0) { - ABSL_DCHECK_NE(ti, tj); - vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); - ++tk; - } else { - int qk = 0; - if (ti < 0) { - qk = absl::c_find_if( - qn, [&](auto nei) { return q2t[nei.dst().id()] >= 0; }) - - qn.begin(); - } else { - vf2pp_sub_pair(target, q2t, conn, qn.id(), ti); - } + const Graph *query_; + const Graph *target_; - if (qk == qn.degree() || ti >= 0) { - const int ti2 = - std::find_if( - target.begin() + ti + 1, target.end(), - [&](auto tn) { - bool candidate = kMt == MappingType::kSubgraph - ? conn[tn.id()] >= 0 - : conn[tn.id()] == 0; - return candidate - && vf2pp_feas( - qn, tn, match, qlbl, tlbl, q2t, conn, - r_inout_cnt, r_new_cnt, r_inout, r_new); - }) - - target.begin(); - - if (ti2 < target.size()) { - vf2pp_add_pair(target, q2t, conn, qn.id(), ti2); - ++depth; - } else { - --depth; - } + Eigen::Ref qlbl_, tlbl_; - continue; - } + // query.size() + ArrayXi mapping_, order_; + internal::Vf2ppLabelMap r_inout_, r_new_; - src_nei(0, depth) = tj = q2t[qn[qk].dst().id()]; - tk = 0; - } + // (2, target.size()) + Array2Xi src_nei_; - for (; tk < target.degree(tj); ++tk) { - auto tn = target.node(tj)[tk].dst(); - if (conn[tn.id()] > 0 - && vf2pp_feas( - qn, tn, match, qlbl, tlbl, q2t, conn, r_inout_cnt, r_new_cnt, - r_inout, r_new)) { - vf2pp_add_pair(target, q2t, conn, qn.id(), tn.id()); - break; - } - } + // max(label) + 1 + ArrayXi label_tmp1_, label_tmp2_; - if (tk < target.degree(tj)) { - src_nei(1, depth++) = tk; - } else { - src_nei(1, depth--) = -1; - } - } + // max(query.size(), target.size()) + ArrayXi node_tmp_; - return { q2t, false }; - } -} // namespace internal + int depth_ = 0; +}; + +template +VF2pp make_vf2pp(const Graph &query, + const Graph &target, AL1 &&qlbl, + AL2 &&tlbl) { + return VF2pp(query, target, std::forward(qlbl), + std::forward(tlbl)); +} + +template +std::pair vf2pp(const Graph &query, + const Graph &target, AL1 &&qlbl, + AL2 &&tlbl, const BinaryPred &match) { + VF2pp vf2pp = make_vf2pp( + query, target, std::forward(qlbl), std::forward(tlbl)); + bool found = vf2pp.next(match); + return { std::move(vf2pp).mapping(), found }; +} template -std::pair vf2pp(const Graph &query, - const Graph &target, - const BinaryPred &match, const AL1 &qlbl, - const AL2 &tlbl, MappingType mt) { - const int nlabel = nuri::max(qlbl.maxCoeff(), tlbl.maxCoeff()) + 1; - - ArrayXi label_tmp1 = ArrayXi::Zero(nlabel), - label_tmp2 = ArrayXi::Zero(nlabel); - ArrayXi node_tmp = ArrayXi::Zero(nuri::max(query.size(), target.size())); - - auto query_tmp = node_tmp.head(query.size()); - // _order - ArrayXi order = - internal::vf2pp_init_order(query, qlbl, tlbl, label_tmp1, query_tmp); - // _rInOutLabels1, _rNewLabels1 - auto [r_inout, r_new] = internal::vf2pp_init_r_new_r_inout( - query, qlbl, order, label_tmp1, label_tmp2, query_tmp); - - node_tmp.head(nuri::min(query.size(), target.size())).setZero(); - auto target_tmp = node_tmp.head(target.size()); +std::pair +vf2pp(const Graph &query, const Graph &target, AL1 &&qlbl, + AL2 &&tlbl, const BinaryPred &match, MappingType mt) { switch (mt) { case MappingType::kSubgraph: - return internal::vf2pp_ext_match( - query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, - label_tmp2, r_inout, r_new); + return vf2pp(query, target, std::forward(qlbl), + std::forward(tlbl), match); case MappingType::kInduced: - return internal::vf2pp_ext_match( - query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, - label_tmp2, r_inout, r_new); + return vf2pp(query, target, std::forward(qlbl), + std::forward(tlbl), match); case MappingType::kIsomorphism: - return internal::vf2pp_ext_match( - query, target, match, qlbl, tlbl, order, target_tmp, label_tmp1, - label_tmp2, r_inout, r_new); + return vf2pp( + query, target, std::forward(qlbl), std::forward(tlbl), match); } - ABSL_UNREACHABLE(); + ABSL_LOG(ERROR) << "Invalid mapping type (" << static_cast(mt) << ")"; + return { {}, false }; +} + +template +std::pair vf2pp(const Graph &query, + const Graph &target, + const BinaryPred &match) { + ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); + return vf2pp(query, target, label.head(query.size()), + label.head(target.size()), match); +} + +template +std::pair vf2pp(const Graph &query, + const Graph &target, + const BinaryPred &match, MappingType mt) { + ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); + return vf2pp(query, target, label.head(query.size()), + label.head(target.size()), match, mt); } } // namespace nuri From c08be5ec9177840b93a183f6cc4f3dd65a79b412 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Wed, 11 Dec 2024 18:49:42 +0900 Subject: [PATCH 07/27] test(core/graph/vf2pp): sync with API update --- test/core/graph_vf2pp_test.cpp | 40 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index c092fd93..61012f1e 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -232,13 +232,11 @@ TEST(VF2PPComponentTest, InitRNewRInoutC5Petersen) { TEST(VF2PPSingleLabelSimpleMatchTest, PetersenSubgraph) { GT target = lemon_petersen(); - ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { return vf2pp( - query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), - tlbl, MappingType::kSubgraph); + query, target, [](auto, auto) { return true; }, MappingType::kSubgraph); }; { @@ -285,13 +283,11 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenSubgraph) { TEST(VF2PPSingleLabelSimpleMatchTest, PetersenInduced) { GT target = lemon_petersen(); - ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { return vf2pp( - query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), - tlbl, MappingType::kInduced); + query, target, [](auto, auto) { return true; }, MappingType::kInduced); }; { @@ -334,13 +330,12 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenInduced) { TEST(VF2PPSingleLabelSimpleMatchTest, PetersenIsomorphism) { GT target = lemon_petersen(); - ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { return vf2pp( - query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), - tlbl, MappingType::kIsomorphism); + query, target, [](auto, auto) { return true; }, + MappingType::kIsomorphism); }; { @@ -379,13 +374,10 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenIsomorphism) { TEST(VF2PPSingleLabelSimpleMatchTest, C10P10) { GT target = lemon_c_n(10); - ArrayXi qlbl = ArrayXi::Zero(10), tlbl = ArrayXi::Zero(target.size()); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query, MappingType mt) { - return vf2pp( - query, target, [](auto, auto) { return true; }, qlbl.head(query.size()), - tlbl, mt); + return vf2pp(query, target, [](auto, auto) { return true; }, mt); }; { @@ -411,7 +403,7 @@ TEST(VF2PPSingleLabelSimpleMatchTest, C10P10) { // NOLINTNEXTLINE(*-suspicious-call-argument) std::tie(std::ignore, ok) = vf2pp( - target, p10, [](auto, auto) { return true; }, qlbl, tlbl, + target, p10, [](auto, auto) { return true; }, MappingType::kIsomorphism); EXPECT_FALSE(ok); } @@ -442,14 +434,14 @@ TEST(VF2PPMultiLabelSimpleMatchTest, C5Petersen) { { auto [_, ok] = vf2pp( - c5, petersen, [](auto, auto) { return true; }, c5_lbl, petersen_lbl1, + c5, petersen, c5_lbl, petersen_lbl1, [](auto, auto) { return true; }, MappingType::kSubgraph); EXPECT_FALSE(ok); } { auto [map, ok] = vf2pp( - c5, petersen, [](auto, auto) { return true; }, c5_lbl, petersen_lbl2, + c5, petersen, c5_lbl, petersen_lbl2, [](auto, auto) { return true; }, MappingType::kSubgraph); ASSERT_TRUE(ok); @@ -477,8 +469,8 @@ TEST(VF2PPMultiLabelSimpleMatchTest, PetersenPetersen) { for (MappingType mt: { MappingType::kSubgraph, MappingType::kInduced, MappingType::kIsomorphism }) { auto [map, ok] = vf2pp( - petersen, petersen, [](auto, auto) { return true; }, petersen_lbl1, - petersen_lbl1, mt); + petersen, petersen, petersen_lbl1, petersen_lbl1, + [](auto, auto) { return true; }, mt); ASSERT_TRUE(ok) << static_cast(mt); for (int i = 0; i < map.size(); ++i) @@ -486,8 +478,8 @@ TEST(VF2PPMultiLabelSimpleMatchTest, PetersenPetersen) { << static_cast(mt); std::tie(map, ok) = vf2pp( - petersen, petersen, [](auto, auto) { return true; }, petersen_lbl2, - petersen_lbl2, mt); + petersen, petersen, petersen_lbl2, petersen_lbl2, + [](auto, auto) { return true; }, mt); ASSERT_TRUE(ok) << static_cast(mt); for (int i = 0; i < map.size(); ++i) @@ -495,13 +487,13 @@ TEST(VF2PPMultiLabelSimpleMatchTest, PetersenPetersen) { << static_cast(mt); std::tie(std::ignore, ok) = vf2pp( - petersen, petersen, [](auto, auto) { return true; }, petersen_lbl1, - petersen_lbl2, mt); + petersen, petersen, petersen_lbl1, petersen_lbl2, + [](auto, auto) { return true; }, mt); EXPECT_FALSE(ok) << static_cast(mt); std::tie(std::ignore, ok) = vf2pp( - petersen, petersen, [](auto, auto) { return true; }, petersen_lbl2, - petersen_lbl1, mt); + petersen, petersen, petersen_lbl2, petersen_lbl1, + [](auto, auto) { return true; }, mt); EXPECT_FALSE(ok) << static_cast(mt); } } From d1b791644439c30fe015ff7ec5d858528d71e9f8 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 11:12:49 +0900 Subject: [PATCH 08/27] feat(core/graph): add is_degree_constant_time trait --- include/nuri/core/graph.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/nuri/core/graph.h b/include/nuri/core/graph.h index 6dfd5a04..121e3d67 100644 --- a/include/nuri/core/graph.h +++ b/include/nuri/core/graph.h @@ -1297,11 +1297,14 @@ namespace internal { // NOLINTBEGIN(readability-identifier-naming) template - struct GraphTraits> { }; + struct GraphTraits> { + constexpr static bool is_degree_constant_time = true; + }; template struct GraphTraits> { constexpr static bool is_const = subg_const; + constexpr static bool is_degree_constant_time = false; }; template From 5cbe5291e3c8bae26907bfb1707ecafbd98631bc Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 11:13:09 +0900 Subject: [PATCH 09/27] feat(core/graph/vf2++): support subgraph as input --- include/nuri/core/graph_vf2pp.h | 238 ++++++++++++++++---------------- 1 file changed, 119 insertions(+), 119 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index fb080cfb..bde1ed57 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -29,10 +29,39 @@ enum class MappingType : int { }; namespace internal { - template - int vf2pp_process_bfs_tree(const Graph &query, ArrayXi &order, - ArrayXb &visited, AL1 &curr_conn, AL2 &query_cnts, - int root, int i) { + template ::is_degree_constant_time> + class Vf2ppDegreeHelper; + + template + class Vf2ppDegreeHelper { + public: + explicit Vf2ppDegreeHelper(const GT &graph): graph_(&graph) { } + + int operator[](int i) const { return graph_->degree(i); } + + private: + const GT *graph_; + }; + + template + class Vf2ppDegreeHelper { + public: + explicit Vf2ppDegreeHelper(const GT &graph): degree_(graph.size()) { + for (int i = 0; i < graph.size(); ++i) + degree_[i] = graph.degree(i); + } + + int operator[](int i) const { return degree_[i]; } + + private: + ArrayXi degree_; + }; + + template + int vf2pp_process_bfs_tree(const GT &query, + const Vf2ppDegreeHelper °rees, + ArrayXi &order, ArrayXb &visited, AL1 &curr_conn, + AL2 &query_cnts, int root, int i) { order[i] = root; visited[root] = true; @@ -56,8 +85,8 @@ namespace internal { for (int k = j + 1; k < rp; ++k) { if (curr_conn[order[min_pos]] < curr_conn[order[k]] || (curr_conn[order[min_pos]] == curr_conn[order[k]] - && (query.degree(order[min_pos]) < query.degree(order[k]) - || (query.degree(order[min_pos]) == query.degree(order[k]) + && (degrees[order[min_pos]] < degrees[order[k]] + || (degrees[order[min_pos]] == degrees[order[k]] && query_cnts[order[min_pos]] > query_cnts[order[k]])))) { min_pos = k; @@ -78,8 +107,8 @@ namespace internal { return i; } - template - ArrayXi vf2pp_init_order(const Graph &query, ConstRef qlbl, + template + ArrayXi vf2pp_init_order(const GT &query, ConstRef qlbl, ConstRef tlbl, ArrayXi &target_lcnt, // NOLINTNEXTLINE(*-missing-std-forward) AL &&curr_conn) { @@ -89,6 +118,7 @@ namespace internal { ++target_lcnt[l]; auto query_cnts = target_lcnt(qlbl); + Vf2ppDegreeHelper degrees(query); ArrayXb visited = ArrayXb::Zero(query.size()); int iorder = 0; for (int i = 0; iorder < query.size() && i < query.size();) { @@ -102,12 +132,12 @@ namespace internal { if (!visited[j] && (query_cnts[imin] > query_cnts[j] || (query_cnts[imin] == query_cnts[j] - && query.degree(imin) < query.degree(j)))) { + && degrees[imin] < degrees[j]))) { imin = j; } } - iorder = vf2pp_process_bfs_tree(query, order, visited, curr_conn, + iorder = vf2pp_process_bfs_tree(query, degrees, order, visited, curr_conn, query_cnts, imin, iorder); } @@ -117,9 +147,9 @@ namespace internal { using Vf2ppLabels = std::vector>; using Vf2ppLabelMap = std::vector; - template + template std::pair - vf2pp_init_r_new_r_inout(const Graph &query, ConstRef qlbl, + vf2pp_init_r_new_r_inout(const GT &query, ConstRef qlbl, const ArrayXi &order, ArrayXi &r_inout, // NOLINTNEXTLINE(*-missing-std-forward) ArrayXi &r_new, AL &&visit_count) { @@ -172,53 +202,40 @@ namespace internal { } } // namespace internal -template +template class VF2pp { private: - const Graph &query() const { return *query_; } - const Graph &target() const { return *target_; } + const GT &query() const { return *query_; } + const GU &target() const { return *target_; } auto query_tmp() { return node_tmp_.head(query_->size()); } - auto conn() { return node_tmp_.head(target_->size()); } + ArrayXi &conn() { return node_tmp_; } auto curr_query() const { return query_->node(order_[depth_]); } int mapped_target() const { return mapping_[curr_query().id()]; } - int target_src() const { return src_nei_(0, depth_); } - void update_target_src(int idx) { - ABSL_DCHECK_GE(idx, 0); - ABSL_DCHECK_LT(idx, target().size()); - - src_nei_(0, depth_) = idx; + auto target_ait() const { return target_ait_[depth_]; } + void update_target_ait(typename GU::const_adjacency_iterator ait) { + target_ait_[depth_] = ait; + ait.end() ? --depth_ : ++depth_; } - int target_nei_idx() const { return src_nei_(1, depth_); } - void update_target_nei_idx(int idx) { - ABSL_DCHECK_GE(target_src(), 0); - ABSL_DCHECK_LT(target_src(), target().size()); - - ABSL_DCHECK_GE(idx, 0); - ABSL_DCHECK_LT(idx, target().degree(target_src())); - - src_nei_(1, depth_) = idx; - } - void invalidate_target_nei_idx() { src_nei_(1, depth_) = -1; } - ArrayXi &r_inout_cnt() { return label_tmp1_; } ArrayXi &r_new_cnt() { return label_tmp2_; } public: template - VF2pp(const Graph &query, const Graph &target, - AL1 &&query_lbl, AL2 &&target_lbl) + VF2pp(const GT &query, const GU &target, AL1 &&query_lbl, AL2 &&target_lbl) : query_(&query), target_(&target), // qlbl_(std::forward(query_lbl)), tlbl_(std::forward(target_lbl)), mapping_(ArrayXi::Constant(query.size(), -1)), - src_nei_(Array2Xi::Constant(2, target.size(), -1)), - node_tmp_(ArrayXi::Zero(nuri::max(query.size(), target.size()))) { + target_ait_(target.size(), target.adj_end(0)), + node_tmp_(ArrayXi::Zero(target.size())) { + ABSL_DCHECK(query.size() <= target.size()); + const int nlabel = nuri::max(qlbl_.maxCoeff(), tlbl_.maxCoeff()) + 1; label_tmp1_ = ArrayXi::Zero(nlabel); @@ -229,7 +246,7 @@ class VF2pp { std::tie(r_inout_, r_new_) = internal::vf2pp_init_r_new_r_inout( query, qlbl_, order_, r_inout_cnt(), r_new_cnt(), query_tmp()); - node_tmp_.head(nuri::min(query.size(), target.size())).setZero(); + query_tmp().setZero(); } template @@ -243,34 +260,34 @@ class VF2pp { const auto qn = curr_query(); const int ti = mapped_target(); - int tj = target_src(), tk = target_nei_idx(); - if (tk >= 0) { - ABSL_DCHECK_NE(ti, tj); + auto ta = target_ait(); + if (!ta.end()) { + ABSL_DCHECK_NE(ti, ta->src().id()); sub_pair(qn.id(), ti); - ++tk; + ++ta; } else { - int qk = 0; + auto qa = qn.begin(); if (ti < 0) { - qk = absl::c_find_if( - qn, [&](auto nei) { return mapping_[nei.dst().id()] >= 0; }) - - qn.begin(); + qa = absl::c_find_if(qn, [&](auto nei) { + return mapping_[nei.dst().id()] >= 0; + }); } else { sub_pair(qn.id(), ti); } - if (qk == qn.degree() || ti >= 0) { - const int ti2 = - std::find_if(target().begin() + ti + 1, target().end(), - [&](auto tn) { - bool candidate = kMt == MappingType::kSubgraph - ? conn()[tn.id()] >= 0 - : conn()[tn.id()] == 0; - return candidate && feas(qn, tn, match); - }) - - target().begin(); - - if (ti2 < target().size()) { - add_pair(qn.id(), ti2); + if (qa.end() || ti >= 0) { + const int tj = std::find_if(target().begin() + ti + 1, target().end(), + [&](auto tn) { + bool candidate = + kMt == MappingType::kSubgraph + ? conn()[tn.id()] >= 0 + : conn()[tn.id()] == 0; + return candidate && feas(qn, tn, match); + }) + - target().begin(); + + if (tj < target().size()) { + add_pair(qn.id(), tj); ++depth_; } else { --depth_; @@ -279,26 +296,18 @@ class VF2pp { continue; } - tj = mapping_[qn[qk].dst().id()]; - tk = 0; - update_target_src(tj); + ta = target().adj_begin(mapping_[qa->dst().id()]); } - for (; tk < target().degree(tj); ++tk) { - auto tn = target().node(tj)[tk].dst(); + for (; !ta.end(); ++ta) { + auto tn = ta->dst(); if (conn()[tn.id()] > 0 && feas(qn, tn, match)) { add_pair(qn.id(), tn.id()); break; } } - if (tk < target().degree(tj)) { - update_target_nei_idx(tk); - ++depth_; - } else { - invalidate_target_nei_idx(); - --depth_; - } + update_target_ait(ta); } return false; @@ -339,23 +348,21 @@ class VF2pp { } } - bool cut_by_labels(const typename Graph::ConstNodeRef qn, - const typename Graph::ConstNodeRef tn) { + bool cut_by_labels(const typename GT::ConstNodeRef qn, + const typename GU::ConstNodeRef tn) { // zero init - r_inout_cnt()(tlbl_)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_inout_[qn.id()]) + for (auto [lbl, _]: r_inout_[qn.id()]) r_inout_cnt()[lbl] = 0; if constexpr (kMt != MappingType::kSubgraph) { - r_new_cnt()(tlbl_)(as_index(tn)).setZero(); - for (auto [lbl, cnt]: r_new_[qn.id()]) + for (auto [lbl, _]: r_new_[qn.id()]) r_new_cnt()[lbl] = 0; } - for (auto tnei: tn) { - const int curr = tnei.dst().id(); - if (conn()[curr] > 0) + for (auto nei: tn) { + const int curr = nei.dst().id(); + if (conn()[curr] > 0) { --r_inout_cnt()[tlbl_[curr]]; - else if constexpr (kMt != MappingType::kSubgraph) { + } else if constexpr (kMt != MappingType::kSubgraph) { if (conn()[curr] == 0) --r_new_cnt()[tlbl_[curr]]; } @@ -378,9 +385,8 @@ class VF2pp { } template - bool feas(const typename Graph::ConstNodeRef qn, - const typename Graph::ConstNodeRef tn, - const BinaryPred &match) { + bool feas(const typename GT::ConstNodeRef qn, + const typename GU::ConstNodeRef tn, const BinaryPred &match) { if (qlbl_[qn.id()] != tlbl_[tn.id()]) return false; @@ -394,8 +400,10 @@ class VF2pp { if (curr_conn < -1) ++conn()[tnei.dst().id()]; else if constexpr (kMt != MappingType::kSubgraph) { - if (curr_conn == -1) + if (curr_conn == -1) { is_iso = false; + break; + } } } @@ -415,19 +423,18 @@ class VF2pp { const int curr_conn = conn()[ti]; conn()[ti] = -1; - if constexpr (kMt == MappingType::kSubgraph) { - if (curr_conn < -1) - return false; - } else { + if constexpr (kMt != MappingType::kSubgraph) + return false; + + if (curr_conn < -1) return false; - } } return match(qn, tn) && cut_by_labels(qn, tn); } - const Graph *query_; - const Graph *target_; + const GT *query_; + const GU *target_; Eigen::Ref qlbl_, tlbl_; @@ -435,8 +442,8 @@ class VF2pp { ArrayXi mapping_, order_; internal::Vf2ppLabelMap r_inout_, r_new_; - // (2, target.size()) - Array2Xi src_nei_; + // target.size() + std::vector target_ait_; // max(label) + 1 ArrayXi label_tmp1_, label_tmp2_; @@ -447,31 +454,27 @@ class VF2pp { int depth_ = 0; }; -template -VF2pp make_vf2pp(const Graph &query, - const Graph &target, AL1 &&qlbl, - AL2 &&tlbl) { - return VF2pp(query, target, std::forward(qlbl), - std::forward(tlbl)); +template +VF2pp make_vf2pp(const GT &query, const GU &target, AL1 &&qlbl, + AL2 &&tlbl) { + return VF2pp(query, target, std::forward(qlbl), + std::forward(tlbl)); } -template -std::pair vf2pp(const Graph &query, - const Graph &target, AL1 &&qlbl, +template +std::pair vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, const BinaryPred &match) { - VF2pp vf2pp = make_vf2pp( + VF2pp vf2pp = make_vf2pp( query, target, std::forward(qlbl), std::forward(tlbl)); bool found = vf2pp.next(match); return { std::move(vf2pp).mapping(), found }; } -template -std::pair -vf2pp(const Graph &query, const Graph &target, AL1 &&qlbl, - AL2 &&tlbl, const BinaryPred &match, MappingType mt) { +template +std::pair vf2pp(const GT &query, const GU &target, AL1 &&qlbl, + AL2 &&tlbl, const BinaryPred &match, + MappingType mt) { switch (mt) { case MappingType::kSubgraph: return vf2pp(query, target, std::forward(qlbl), @@ -488,19 +491,16 @@ vf2pp(const Graph &query, const Graph &target, AL1 &&qlbl, return { {}, false }; } -template -std::pair vf2pp(const Graph &query, - const Graph &target, +template +std::pair vf2pp(const GT &query, const GU &target, const BinaryPred &match) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); return vf2pp(query, target, label.head(query.size()), label.head(target.size()), match); } -template -std::pair vf2pp(const Graph &query, - const Graph &target, +template +std::pair vf2pp(const GT &query, const GU &target, const BinaryPred &match, MappingType mt) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); return vf2pp(query, target, label.head(query.size()), From cd7c0cb684aa9999f0d9c980c3df5e142ba64245 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 11:13:32 +0900 Subject: [PATCH 10/27] test(core/graph/vf2++): sync with internal API change --- test/core/graph_vf2pp_test.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 61012f1e..af66b535 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -116,8 +116,9 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { ArrayXi lbl = ArrayXi::Zero(g.size()); auto query_cnts = lcnt(lbl); - const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, - query_cnts, 0, 0); + internal::Vf2ppDegreeHelper degrees(g); + const int ret = internal::vf2pp_process_bfs_tree(g, degrees, order, visited, + curr_conn, query_cnts, 0, 0); EXPECT_EQ(ret, 10); ArrayXi expected_order(10); @@ -140,11 +141,11 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { ArrayXi qlbl(g.size()); qlbl.head(6).setOnes(); qlbl.tail(4).setZero(); - auto query_cnts = lcnt(qlbl); - const int ret = internal::vf2pp_process_bfs_tree(g, order, visited, curr_conn, - query_cnts, 6, 0); + internal::Vf2ppDegreeHelper degrees(g); + const int ret = internal::vf2pp_process_bfs_tree(g, degrees, order, visited, + curr_conn, query_cnts, 6, 0); EXPECT_EQ(ret, 10); ArrayXi expected_order(10); From ea63c54e8784295e59e7eb2a07aad84202142680 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 15:28:44 +0900 Subject: [PATCH 11/27] perf(core/graph/vf2++): assume no invalid mapping type is provided --- include/nuri/core/graph_vf2pp.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index bde1ed57..5652af38 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -487,8 +487,7 @@ std::pair vf2pp(const GT &query, const GU &target, AL1 &&qlbl, query, target, std::forward(qlbl), std::forward(tlbl), match); } - ABSL_LOG(ERROR) << "Invalid mapping type (" << static_cast(mt) << ")"; - return { {}, false }; + ABSL_UNREACHABLE(); } template From bfa8758455fb6654cd96d762f71599e36ae49e06 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 15:29:32 +0900 Subject: [PATCH 12/27] test(core/graph/vf2++): rename tests to match implementation class --- test/core/graph_vf2pp_test.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index af66b535..291b1b30 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -104,7 +104,7 @@ GT lemon_c5_petersen() { return g; } -TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { +TEST(VF2ppComponentTest, ProcessBfsTreePetersenSingleLabel) { GT g = lemon_petersen(); ArrayXi order = ArrayXi::Constant(g.size(), -1); @@ -127,7 +127,7 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenSingleLabel) { EXPECT_EQ(lid(g, order[i]), expected_order[i]); } -TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { +TEST(VF2ppComponentTest, ProcessBfsTreePetersenMultiLabel) { GT g = lemon_petersen(); ArrayXi order = ArrayXi::Constant(g.size(), -1); @@ -154,7 +154,7 @@ TEST(VF2PPComponentTest, ProcessBfsTreePetersenMultiLabel) { EXPECT_EQ(lid(g, order[i]), expected_order[i]); } -TEST(VF2PPComponentTest, InitOrderC5Petersen) { +TEST(VF2ppComponentTest, InitOrderC5Petersen) { GT g = lemon_c5_petersen(); ArrayXi lbl(g.size()); @@ -171,7 +171,7 @@ TEST(VF2PPComponentTest, InitOrderC5Petersen) { EXPECT_EQ(lid(g, order[i]), expected_order[i]); } -TEST(VF2PPComponentTest, InitRNewRInoutC5Petersen) { +TEST(VF2ppComponentTest, InitRNewRInoutC5Petersen) { GT g = lemon_c5_petersen(); ArrayXi lbl(g.size()); lbl << 1, 1, 1, 1, 1, // @@ -231,7 +231,7 @@ TEST(VF2PPComponentTest, InitRNewRInoutC5Petersen) { } } -TEST(VF2PPSingleLabelSimpleMatchTest, PetersenSubgraph) { +TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { GT target = lemon_petersen(); ArrayXi expected_map(10); @@ -282,7 +282,7 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenSubgraph) { } } -TEST(VF2PPSingleLabelSimpleMatchTest, PetersenInduced) { +TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { GT target = lemon_petersen(); ArrayXi expected_map(10); @@ -329,7 +329,7 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenInduced) { } } -TEST(VF2PPSingleLabelSimpleMatchTest, PetersenIsomorphism) { +TEST(VF2ppSingleLabelSimpleMatchTest, PetersenIsomorphism) { GT target = lemon_petersen(); ArrayXi expected_map(10); @@ -373,7 +373,7 @@ TEST(VF2PPSingleLabelSimpleMatchTest, PetersenIsomorphism) { } } -TEST(VF2PPSingleLabelSimpleMatchTest, C10P10) { +TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { GT target = lemon_c_n(10); ArrayXi expected_map(10); @@ -419,7 +419,7 @@ TEST(VF2PPSingleLabelSimpleMatchTest, C10P10) { } } -TEST(VF2PPMultiLabelSimpleMatchTest, C5Petersen) { +TEST(VF2ppMultiLabelSimpleMatchTest, C5Petersen) { GT c5 = lemon_c_n(5), petersen = lemon_petersen(); ArrayXi c5_lbl(5); @@ -453,7 +453,7 @@ TEST(VF2PPMultiLabelSimpleMatchTest, C5Petersen) { } } -TEST(VF2PPMultiLabelSimpleMatchTest, PetersenPetersen) { +TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { GT petersen = lemon_petersen(); ArrayXi petersen_lbl1(10); From f2929e5b735cd0563d1486262fd205f3fdfd4d2c Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 15:29:54 +0900 Subject: [PATCH 13/27] test(core/graph/vf2++): test for subgraph matches --- test/core/graph_vf2pp_test.cpp | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 291b1b30..136f1f3d 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -498,5 +498,133 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { EXPECT_FALSE(ok) << static_cast(mt); } } + +TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { + GT petersen = lemon_petersen(); + ArrayXi idxs(5), expected_map(5); + + // 0 1 2 4 3 + // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 + auto p5 = subgraph_from_edges(petersen, { 1, 3, 9, 10 }); + // 0 2 1 4 3 0 + // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 + auto c5 = subgraph_from_edges(petersen, { 3, 4, 6, 7, 12 }); + + idxs << 0, 1, 2, 4, 3; + { + auto [map, ok] = + vf2pp(p5, c5, [](auto, auto) { return true; }, MappingType::kSubgraph); + EXPECT_TRUE(ok); + + expected_map << 3, 0, 2, 1, 4; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + + std::tie(std::ignore, ok) = + vf2pp(p5, c5, [](auto, auto) { return true; }, MappingType::kInduced); + EXPECT_FALSE(ok); + + std::tie(std::ignore, ok) = + vf2pp(c5, p5, [](auto, auto) { return true; }, MappingType::kSubgraph); + EXPECT_FALSE(ok); + + std::tie(map, ok) = vf2pp( + c5, c5, [](auto, auto) { return true; }, MappingType::kIsomorphism); + EXPECT_TRUE(ok); + + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[i], i) << i; + } + + { + auto [map, ok] = vf2pp( + p5, petersen, [](auto, auto) { return true; }, MappingType::kSubgraph); + EXPECT_TRUE(ok); + + expected_map << 3, 0, 2, 4, 1; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + + std::tie(map, ok) = vf2pp( + c5, petersen, [](auto, auto) { return true; }, MappingType::kSubgraph); + EXPECT_TRUE(ok); + + idxs << 0, 2, 1, 4, 3; + expected_map << 0, 2, 4, 1, 3; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + } +} + +TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { + GT petersen = lemon_petersen(); + ArrayXi qlbl(5), tlbl(10), idxs(5), expected_map(5); + + // 0 1 2 1 1 0 1 2 4 3 + // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 + auto p5 = subgraph_from_edges(petersen, { 1, 3, 9, 10 }); + // 0 2 1 1 1 0 2 1 4 3 0 + // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 + auto c5 = subgraph_from_edges(petersen, { 3, 4, 6, 7, 12 }); + + { + qlbl << 0, 1, 2, 1, 1; + tlbl.head(5) << 0, 2, 1, 1, 1; + auto [map, ok] = vf2pp( + p5, c5, qlbl, tlbl.head(5), [](auto, auto) { return true; }, + MappingType::kSubgraph); + EXPECT_TRUE(ok); + + idxs << 0, 1, 2, 4, 3; + expected_map << 0, 2, 1, 4, 3; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + + qlbl << 0, 0, 1, 1, 1; + std::tie(std::ignore, ok) = vf2pp( + p5, c5, qlbl, qlbl, [](auto, auto) { return true; }, + MappingType::kSubgraph); + EXPECT_FALSE(ok); + } + + tlbl.head(4).setZero(); + tlbl.tail(6).setOnes(); + + { + auto matcher = make_vf2pp( + p5, petersen, tlbl(p5.node_ids()), tlbl); + + bool ok = matcher.next([](auto, auto) { return true; }); + EXPECT_TRUE(ok); + auto map = matcher.mapping(); + // 0 0 1 1 1 0 1 2 4 3 + // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 + idxs << 0, 1, 2, 4, 3; + expected_map.head(5) << 2, 0, 5, 9, 4; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + + bool found_original = false; + while (matcher.next([](auto, auto) { return true; })) { + if (absl::c_equal(p5.node_ids(), matcher.mapping())) { + found_original = true; + break; + } + } + EXPECT_TRUE(found_original); + + std::tie(map, ok) = vf2pp( + c5, petersen, tlbl(c5.node_ids()), tlbl, + [](auto, auto) { return true; }, MappingType::kSubgraph); + EXPECT_TRUE(ok); + + // 0 0 1 1 1 0 2 1 4 3 0 + // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 + idxs.head(5) << 0, 2, 1, 4, 3; + expected_map.head(5) << 1, 4, 2, 7, 6; + for (int i = 0; i < map.size(); ++i) + EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + } +} } // namespace } // namespace nuri From 487196c4f09543a17e9f0c82ac544d87d700a8bb Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 15:38:11 +0900 Subject: [PATCH 14/27] chore: resolve clang-format/tidy diagnostics --- include/nuri/core/graph_vf2pp.h | 1 + include/nuri/eigen_config.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 5652af38..2971b1ff 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -8,6 +8,7 @@ /// @cond #include +#include #include #include diff --git a/include/nuri/eigen_config.h b/include/nuri/eigen_config.h index 9eb19434..f0bfa048 100644 --- a/include/nuri/eigen_config.h +++ b/include/nuri/eigen_config.h @@ -40,8 +40,8 @@ using Eigen::Array4i; using Eigen::ArrayX; using ArrayXb = Eigen::ArrayX; using ArrayXc = ArrayX; -using Eigen::Array2Xi; using Eigen::Array2Xd; +using Eigen::Array2Xi; using Eigen::Array33d; using Eigen::Array3Xd; using Eigen::Array4Xd; From 4a6906548c8f042479f4de736259e97456f424d4 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 16:43:29 +0900 Subject: [PATCH 15/27] feat(core/graph/vf2++): search for mapped edge too --- include/nuri/core/graph_vf2pp.h | 220 ++++++++++++++++++++------------ 1 file changed, 137 insertions(+), 83 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 2971b1ff..e29a32e4 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -213,14 +213,15 @@ class VF2pp { ArrayXi &conn() { return node_tmp_; } - auto curr_query() const { return query_->node(order_[depth_]); } + auto curr_node() const { return query_->node(order_[depth_]); } - int mapped_target() const { return mapping_[curr_query().id()]; } + int mapped_node() const { return node_map_[curr_node().id()]; } - auto target_ait() const { return target_ait_[depth_]; } - void update_target_ait(typename GU::const_adjacency_iterator ait) { - target_ait_[depth_] = ait; - ait.end() ? --depth_ : ++depth_; + auto query_target_ait() const { return query_target_ait_[depth_]; } + void update_ait(typename GT::const_adjacency_iterator qa, + typename GU::const_adjacency_iterator ta) { + query_target_ait_[depth_] = { qa, ta }; + ta.end() ? --depth_ : ++depth_; } ArrayXi &r_inout_cnt() { return label_tmp1_; } @@ -232,10 +233,13 @@ class VF2pp { : query_(&query), target_(&target), // qlbl_(std::forward(query_lbl)), tlbl_(std::forward(target_lbl)), - mapping_(ArrayXi::Constant(query.size(), -1)), - target_ait_(target.size(), target.adj_end(0)), + node_map_(ArrayXi::Constant(query.size(), -1)), + edge_map_(ArrayXi::Constant(query.num_edges(), -1)), + query_target_ait_(query.size(), + { query.adj_end(0), target.adj_end(0) }), node_tmp_(ArrayXi::Zero(target.size())) { - ABSL_DCHECK(query.size() <= target.size()); + ABSL_DCHECK(!query.empty()); + ABSL_DCHECK_LE(query.size(), target.size()); const int nlabel = nuri::max(qlbl_.maxCoeff(), tlbl_.maxCoeff()) + 1; @@ -250,45 +254,50 @@ class VF2pp { query_tmp().setZero(); } - template - bool next(const BinaryPred &match) { + template + bool next(const NodeMatch &node_match, const EdgeMatch &edge_match) { while (depth_ >= 0) { if (depth_ == query().size()) { --depth_; - return true; + + if (map_remaining_edges(edge_match)) + return true; } - const auto qn = curr_query(); - const int ti = mapped_target(); + const auto qn = curr_node(); + const int ti = mapped_node(); - auto ta = target_ait(); + auto [qa, ta] = query_target_ait(); if (!ta.end()) { + ABSL_DCHECK(!qa.end()); ABSL_DCHECK_NE(ti, ta->src().id()); - sub_pair(qn.id(), ti); + + sub_pair(qn.id(), ti, qa->eid()); ++ta; } else { - auto qa = qn.begin(); if (ti < 0) { - qa = absl::c_find_if(qn, [&](auto nei) { - return mapping_[nei.dst().id()] >= 0; + auto qb = absl::c_find_if(qn, [&](auto nei) { + return node_map_[nei.dst().id()] >= 0; }); + if (!qb.end()) + qa = qb->dst().find_adjacent(qn); } else { - sub_pair(qn.id(), ti); + sub_pair(qn.id(), ti, qa.end() ? -1 : qa->eid()); } if (qa.end() || ti >= 0) { - const int tj = std::find_if(target().begin() + ti + 1, target().end(), - [&](auto tn) { - bool candidate = - kMt == MappingType::kSubgraph - ? conn()[tn.id()] >= 0 - : conn()[tn.id()] == 0; - return candidate && feas(qn, tn, match); - }) - - target().begin(); + const int tj = + std::find_if(target().begin() + ti + 1, target().end(), + [&](auto tn) { + bool candidate = kMt == MappingType::kSubgraph + ? conn()[tn.id()] >= 0 + : conn()[tn.id()] == 0; + return candidate && feas(qn, tn, node_match); + }) + - target().begin(); if (tj < target().size()) { - add_pair(qn.id(), tj); + add_pair(qn.id(), tj, -1, -1); ++depth_; } else { --depth_; @@ -297,54 +306,64 @@ class VF2pp { continue; } - ta = target().adj_begin(mapping_[qa->dst().id()]); + ta = target().adj_begin(node_map_[qa->src().id()]); } + ABSL_DCHECK(ta.end() || !qa.end()); + for (; !ta.end(); ++ta) { auto tn = ta->dst(); - if (conn()[tn.id()] > 0 && feas(qn, tn, match)) { - add_pair(qn.id(), tn.id()); + if (conn()[tn.id()] > 0 // + && feas(qn, tn, node_match) + && edge_match(query().edge(qa->eid()), target().edge(ta->eid()))) { + add_pair(qn.id(), tn.id(), qa->eid(), ta->eid()); break; } } - update_target_ait(ta); + update_ait(qa, ta); } return false; } - const ArrayXi &mapping() const & { return mapping_; } + const ArrayXi &node_map() const & { return node_map_; } + ArrayXi &&node_map() && { return std::move(node_map_); } - ArrayXi &&mapping() && { return std::move(mapping_); } + const ArrayXi &edge_map() const & { return edge_map_; } + ArrayXi &&edge_map() && { return std::move(edge_map_); } private: - void add_pair(const int qi, const int ti) { - ABSL_DCHECK_GE(ti, 0); - ABSL_DCHECK_LT(ti, target().size()); + void add_pair(const int qn, const int tn, const int qe, const int te) { + ABSL_DCHECK_GE(tn, 0); + ABSL_DCHECK_LT(tn, target().size()); - mapping_[qi] = ti; - conn()[ti] = -1; + node_map_[qn] = tn; + if (qe >= 0) + edge_map_[qe] = te; - for (auto nei: target().node(ti)) { + conn()[tn] = -1; + for (auto nei: target().node(tn)) { if (conn()[nei.dst().id()] != -1) ++conn()[nei.dst().id()]; } } - void sub_pair(const int qi, const int ti) { - ABSL_DCHECK_GE(ti, 0); - ABSL_DCHECK_LT(ti, target().size()); + void sub_pair(const int qn, const int tn, const int qe) { + ABSL_DCHECK_GE(tn, 0); + ABSL_DCHECK_LT(tn, target().size()); - mapping_[qi] = -1; - conn()[ti] = 0; + node_map_[qn] = -1; + if (qe >= 0) + edge_map_[qe] = -1; - for (auto nei: target().node(ti)) { + conn()[tn] = 0; + for (auto nei: target().node(tn)) { int curr_conn = conn()[nei.dst().id()]; if (curr_conn > 0) { --conn()[nei.dst().id()]; } else if (curr_conn == -1) { - ++conn()[ti]; + ++conn()[tn]; } } } @@ -385,15 +404,15 @@ class VF2pp { && internal::vf2pp_r_matches(r_new_[qn.id()], r_new_cnt()); } - template + template bool feas(const typename GT::ConstNodeRef qn, - const typename GU::ConstNodeRef tn, const BinaryPred &match) { + const typename GU::ConstNodeRef tn, const NodeMatch &node_match) { if (qlbl_[qn.id()] != tlbl_[tn.id()]) return false; for (auto qnei: qn) - if (mapping_[qnei.dst().id()] >= 0) - --conn()[mapping_[qnei.dst().id()]]; + if (node_map_[qnei.dst().id()] >= 0) + --conn()[node_map_[qnei.dst().id()]]; bool is_iso = true; for (auto tnei: tn) { @@ -410,7 +429,7 @@ class VF2pp { if (!is_iso) { for (auto qnei: qn) { - const int ti = mapping_[qnei.dst().id()]; + const int ti = node_map_[qnei.dst().id()]; if (ti >= 0) conn()[ti] = -1; } @@ -418,7 +437,7 @@ class VF2pp { } for (auto qnei: qn) { - const int ti = mapping_[qnei.dst().id()]; + const int ti = node_map_[qnei.dst().id()]; if (ti < 0 || conn()[ti] == -1) continue; @@ -431,7 +450,26 @@ class VF2pp { return false; } - return match(qn, tn) && cut_by_labels(qn, tn); + return node_match(qn, tn) && cut_by_labels(qn, tn); + } + + template + bool map_remaining_edges(const EdgeMatch &edge_match) { + for (auto qe: query().edges()) { + if (edge_map_[qe.id()] >= 0) + continue; + + auto teit = target().find_edge(target().node(node_map_[qe.src().id()]), + target().node(node_map_[qe.dst().id()])); + ABSL_DCHECK(teit != target().edge_end()); + + if (!edge_match(qe, *teit)) + return false; + + edge_map_[qe.id()] = teit->id(); + } + + return true; } const GT *query_; @@ -439,12 +477,14 @@ class VF2pp { Eigen::Ref qlbl_, tlbl_; + ArrayXi node_map_, edge_map_; + // query.size() - ArrayXi mapping_, order_; + ArrayXi order_; internal::Vf2ppLabelMap r_inout_, r_new_; - - // target.size() - std::vector target_ait_; + std::vector> + query_target_ait_; // max(label) + 1 ArrayXi label_tmp1_, label_tmp2_; @@ -462,49 +502,63 @@ VF2pp make_vf2pp(const GT &query, const GU &target, AL1 &&qlbl, std::forward(tlbl)); } +struct VF2ppResult { + ArrayXi node_map; + ArrayXi edge_map; + bool found; +}; + template -std::pair vf2pp(const GT &query, const GU &target, AL1 &&qlbl, - AL2 &&tlbl, const BinaryPred &match) { + class NodeMatch, class EdgeMatch> +VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, + const NodeMatch &node_match, const EdgeMatch &edge_match) { VF2pp vf2pp = make_vf2pp( query, target, std::forward(qlbl), std::forward(tlbl)); - bool found = vf2pp.next(match); - return { std::move(vf2pp).mapping(), found }; + bool found = vf2pp.next(node_match, edge_match); + return { std::move(vf2pp).node_map(), std::move(vf2pp).edge_map(), found }; } -template -std::pair vf2pp(const GT &query, const GU &target, AL1 &&qlbl, - AL2 &&tlbl, const BinaryPred &match, - MappingType mt) { +template +VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, + const NodeMatch &node_match, const EdgeMatch &edge_match, + MappingType mt) { switch (mt) { case MappingType::kSubgraph: - return vf2pp(query, target, std::forward(qlbl), - std::forward(tlbl), match); + return vf2pp( // + query, target, // + std::forward(qlbl), std::forward(tlbl), // + node_match, edge_match); case MappingType::kInduced: - return vf2pp(query, target, std::forward(qlbl), - std::forward(tlbl), match); + return vf2pp( // + query, target, // + std::forward(qlbl), std::forward(tlbl), // + node_match, edge_match); case MappingType::kIsomorphism: - return vf2pp( - query, target, std::forward(qlbl), std::forward(tlbl), match); + return vf2pp( // + query, target, // + std::forward(qlbl), std::forward(tlbl), // + node_match, edge_match); } ABSL_UNREACHABLE(); } -template -std::pair vf2pp(const GT &query, const GU &target, - const BinaryPred &match) { +template +VF2ppResult vf2pp(const GT &query, const GU &target, + const NodeMatch &node_match, const EdgeMatch &edge_match) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); return vf2pp(query, target, label.head(query.size()), - label.head(target.size()), match); + label.head(target.size()), node_match, edge_match); } -template -std::pair vf2pp(const GT &query, const GU &target, - const BinaryPred &match, MappingType mt) { +template +VF2ppResult vf2pp(const GT &query, const GU &target, + const NodeMatch &node_match, const EdgeMatch &edge_match, + MappingType mt) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); return vf2pp(query, target, label.head(query.size()), - label.head(target.size()), match, mt); + label.head(target.size()), node_match, edge_match, mt); } } // namespace nuri From 45558bcf4da17468d6a8ca47598a50fc63a27538 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 16:44:09 +0900 Subject: [PATCH 16/27] test(core/graph/vf2++): sync with API change --- test/core/graph_vf2pp_test.cpp | 258 ++++++++++++++++++--------------- 1 file changed, 138 insertions(+), 120 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 136f1f3d..2f4cf538 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -231,54 +231,56 @@ TEST(VF2ppComponentTest, InitRNewRInoutC5Petersen) { } } +constexpr auto dummy_match = [](auto, auto) { return true; }; + TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { GT target = lemon_petersen(); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { - return vf2pp( - query, target, [](auto, auto) { return true; }, MappingType::kSubgraph); + return vf2pp(query, target, dummy_match, dummy_match, + MappingType::kSubgraph); }; { GT c5 = lemon_c_n(5); - auto [map, ok] = run_vf2pp(c5); + auto [nmap, emap, ok] = run_vf2pp(c5); EXPECT_TRUE(ok); expected_map.head(5) << 7, 5, 8, 6, 9; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(c5, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(c5, i)]), expected_map[i]); } { GT c7 = lemon_c_n(7); - auto [_, ok] = run_vf2pp(c7); + auto [_1, _2, ok] = run_vf2pp(c7); EXPECT_FALSE(ok); } { GT p10 = lemon_p_n(10); - auto [map, ok] = run_vf2pp(p10); + auto [nmap, emap, ok] = run_vf2pp(p10); EXPECT_TRUE(ok); expected_map << 2, 1, 0, 4, 3, 8, 5, 7, 9, 6; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(p10, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(p10, i)]), expected_map[i]); } { GT c10 = lemon_c_n(10); - auto [_, ok] = run_vf2pp(c10); + auto [_1, _2, ok] = run_vf2pp(c10); EXPECT_FALSE(ok); } { - auto [map, ok] = run_vf2pp(target); + auto [nmap, emap, ok] = run_vf2pp(target); EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); } } @@ -287,45 +289,45 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { - return vf2pp( - query, target, [](auto, auto) { return true; }, MappingType::kInduced); + return vf2pp(query, target, dummy_match, dummy_match, + MappingType::kInduced); }; { GT c5 = lemon_c_n(5); - auto [map, ok] = run_vf2pp(c5); + auto [nmap, emap, ok] = run_vf2pp(c5); EXPECT_TRUE(ok); expected_map.head(5) << 7, 5, 8, 6, 9; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(c5, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(c5, i)]), expected_map[i]); } { GT c7 = lemon_c_n(7); - auto [_, ok] = run_vf2pp(c7); + auto [_1, _2, ok] = run_vf2pp(c7); EXPECT_FALSE(ok); } { GT p10 = lemon_p_n(10); - auto [_, ok] = run_vf2pp(p10); + auto [_1, _2, ok] = run_vf2pp(p10); EXPECT_FALSE(ok); } { GT c10 = lemon_c_n(10); - auto [_, ok] = run_vf2pp(c10); + auto [_1, _2, ok] = run_vf2pp(c10); EXPECT_FALSE(ok); } { - auto [map, ok] = run_vf2pp(target); + auto [nmap, emap, ok] = run_vf2pp(target); EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); } } @@ -334,88 +336,89 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenIsomorphism) { ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query) { - return vf2pp( - query, target, [](auto, auto) { return true; }, - MappingType::kIsomorphism); + return vf2pp(query, target, dummy_match, dummy_match, + MappingType::kIsomorphism); }; { GT c5 = lemon_c_n(5); - auto [_, ok] = run_vf2pp(c5); + auto [_1, _2, ok] = run_vf2pp(c5); EXPECT_FALSE(ok); } { GT c7 = lemon_c_n(7); - auto [_, ok] = run_vf2pp(c7); + auto [_1, _2, ok] = run_vf2pp(c7); EXPECT_FALSE(ok); } { GT p10 = lemon_p_n(10); - auto [_, ok] = run_vf2pp(p10); + auto [_1, _2, ok] = run_vf2pp(p10); EXPECT_FALSE(ok); } { GT c10 = lemon_c_n(10); - auto [_, ok] = run_vf2pp(c10); + auto [_1, _2, ok] = run_vf2pp(c10); EXPECT_FALSE(ok); } { - auto [map, ok] = run_vf2pp(target); + auto [nmap, emap, ok] = run_vf2pp(target); EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); } } TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { - GT target = lemon_c_n(10); + GT petersen = lemon_petersen(), p10 = lemon_p_n(10), target = lemon_c_n(10); ArrayXi expected_map(10); auto run_vf2pp = [&](const GT &query, MappingType mt) { - return vf2pp(query, target, [](auto, auto) { return true; }, mt); + return vf2pp(query, target, dummy_match, dummy_match, mt); }; { - GT petersen = lemon_petersen(); - auto [_, ok] = run_vf2pp(petersen, MappingType::kSubgraph); + auto [_1, _2, ok] = run_vf2pp(petersen, MappingType::kSubgraph); EXPECT_FALSE(ok); } { - GT p10 = lemon_p_n(10); - auto [map, ok] = run_vf2pp(p10, MappingType::kSubgraph); + auto [nmap, emap, ok] = run_vf2pp(p10, MappingType::kSubgraph); EXPECT_TRUE(ok); expected_map << 7, 6, 5, 4, 3, 2, 1, 0, 9, 8; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(p10, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(p10, i)]), expected_map[i]); + } - std::tie(std::ignore, ok) = run_vf2pp(p10, MappingType::kInduced); + { + auto [_1, _2, ok] = run_vf2pp(p10, MappingType::kInduced); EXPECT_FALSE(ok); - - std::tie(std::ignore, ok) = run_vf2pp(p10, MappingType::kIsomorphism); + } + { + auto [_1, _2, ok] = run_vf2pp(p10, MappingType::kIsomorphism); EXPECT_FALSE(ok); + } - // NOLINTNEXTLINE(*-suspicious-call-argument) - std::tie(std::ignore, ok) = vf2pp( - target, p10, [](auto, auto) { return true; }, - MappingType::kIsomorphism); + { + auto [_1, _2, ok] = + // NOLINTNEXTLINE(*-suspicious-call-argument) + vf2pp(target, p10, dummy_match, dummy_match, MappingType::kIsomorphism); EXPECT_FALSE(ok); } { - auto [map, ok] = run_vf2pp(target, MappingType::kIsomorphism); + auto [nmap, emap, ok] = run_vf2pp(target, MappingType::kIsomorphism); EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(target, map[lid(target, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); } } @@ -434,22 +437,21 @@ TEST(VF2ppMultiLabelSimpleMatchTest, C5Petersen) { petersen_lbl2.tail(5) = c5_lbl; { - auto [_, ok] = vf2pp( - c5, petersen, c5_lbl, petersen_lbl1, [](auto, auto) { return true; }, - MappingType::kSubgraph); + auto [_1, _2, ok] = vf2pp(c5, petersen, c5_lbl, petersen_lbl1, dummy_match, + dummy_match, MappingType::kSubgraph); EXPECT_FALSE(ok); } { - auto [map, ok] = vf2pp( - c5, petersen, c5_lbl, petersen_lbl2, [](auto, auto) { return true; }, - MappingType::kSubgraph); + auto [nmap, emap, ok] = vf2pp(c5, petersen, c5_lbl, petersen_lbl2, + dummy_match, dummy_match, + MappingType::kSubgraph); ASSERT_TRUE(ok); ArrayXi expected_map(5); expected_map << 0, 1, 2, 3, 4; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(petersen, map[lid(c5, i)]), expected_map[i]); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(petersen, nmap[lid(c5, i)]), expected_map[i]); } } @@ -469,33 +471,39 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { for (MappingType mt: { MappingType::kSubgraph, MappingType::kInduced, MappingType::kIsomorphism }) { - auto [map, ok] = vf2pp( - petersen, petersen, petersen_lbl1, petersen_lbl1, - [](auto, auto) { return true; }, mt); - ASSERT_TRUE(ok) << static_cast(mt); - - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(petersen, map[lid(petersen, i)]), expected_map[i]) - << static_cast(mt); + { + auto [nmap, emap, ok] = vf2pp(petersen, petersen, petersen_lbl1, + petersen_lbl1, dummy_match, dummy_match, + mt); + ASSERT_TRUE(ok) << static_cast(mt); + + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(petersen, nmap[lid(petersen, i)]), expected_map[i]) + << static_cast(mt); + } - std::tie(map, ok) = vf2pp( - petersen, petersen, petersen_lbl2, petersen_lbl2, - [](auto, auto) { return true; }, mt); - ASSERT_TRUE(ok) << static_cast(mt); + { + auto [nmap, emap, ok] = vf2pp(petersen, petersen, petersen_lbl2, + petersen_lbl2, dummy_match, dummy_match, + mt); + ASSERT_TRUE(ok) << static_cast(mt); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(lid(petersen, map[lid(petersen, i)]), expected_map[i]) - << static_cast(mt); + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(lid(petersen, nmap[lid(petersen, i)]), expected_map[i]) + << static_cast(mt); + } - std::tie(std::ignore, ok) = vf2pp( - petersen, petersen, petersen_lbl1, petersen_lbl2, - [](auto, auto) { return true; }, mt); - EXPECT_FALSE(ok) << static_cast(mt); + { + auto [_1, _2, ok] = vf2pp(petersen, petersen, petersen_lbl1, + petersen_lbl2, dummy_match, dummy_match, mt); + EXPECT_FALSE(ok) << static_cast(mt); + } - std::tie(std::ignore, ok) = vf2pp( - petersen, petersen, petersen_lbl2, petersen_lbl1, - [](auto, auto) { return true; }, mt); - EXPECT_FALSE(ok) << static_cast(mt); + { + auto [_1, _2, ok] = vf2pp(petersen, petersen, petersen_lbl2, + petersen_lbl1, dummy_match, dummy_match, mt); + EXPECT_FALSE(ok) << static_cast(mt); + } } } @@ -512,47 +520,55 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { idxs << 0, 1, 2, 4, 3; { - auto [map, ok] = - vf2pp(p5, c5, [](auto, auto) { return true; }, MappingType::kSubgraph); + auto [nmap, emap, ok] = + vf2pp(p5, c5, dummy_match, dummy_match, MappingType::kSubgraph); EXPECT_TRUE(ok); expected_map << 3, 0, 2, 1, 4; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + } - std::tie(std::ignore, ok) = - vf2pp(p5, c5, [](auto, auto) { return true; }, MappingType::kInduced); + { + auto [_1, _2, ok] = + vf2pp(p5, c5, dummy_match, dummy_match, MappingType::kInduced); EXPECT_FALSE(ok); + } - std::tie(std::ignore, ok) = - vf2pp(c5, p5, [](auto, auto) { return true; }, MappingType::kSubgraph); + { + auto [_1, _2, ok] = + vf2pp(c5, p5, dummy_match, dummy_match, MappingType::kSubgraph); EXPECT_FALSE(ok); + } - std::tie(map, ok) = vf2pp( - c5, c5, [](auto, auto) { return true; }, MappingType::kIsomorphism); + { + auto [nmap, emap, ok] = + vf2pp(c5, c5, dummy_match, dummy_match, MappingType::kIsomorphism); EXPECT_TRUE(ok); - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[i], i) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[i], i) << i; } { - auto [map, ok] = vf2pp( - p5, petersen, [](auto, auto) { return true; }, MappingType::kSubgraph); + auto [nmap, emap, ok] = + vf2pp(p5, petersen, dummy_match, dummy_match, MappingType::kSubgraph); EXPECT_TRUE(ok); expected_map << 3, 0, 2, 4, 1; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + } - std::tie(map, ok) = vf2pp( - c5, petersen, [](auto, auto) { return true; }, MappingType::kSubgraph); + { + auto [nmap, emap, ok] = + vf2pp(c5, petersen, dummy_match, dummy_match, MappingType::kSubgraph); EXPECT_TRUE(ok); idxs << 0, 2, 1, 4, 3; expected_map << 0, 2, 4, 1, 3; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; } } @@ -570,20 +586,20 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { { qlbl << 0, 1, 2, 1, 1; tlbl.head(5) << 0, 2, 1, 1, 1; - auto [map, ok] = vf2pp( - p5, c5, qlbl, tlbl.head(5), [](auto, auto) { return true; }, - MappingType::kSubgraph); + auto [nmap, emap, ok] = vf2pp(p5, c5, qlbl, tlbl.head(5), dummy_match, + dummy_match, MappingType::kSubgraph); EXPECT_TRUE(ok); idxs << 0, 1, 2, 4, 3; expected_map << 0, 2, 1, 4, 3; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + } + { qlbl << 0, 0, 1, 1, 1; - std::tie(std::ignore, ok) = vf2pp( - p5, c5, qlbl, qlbl, [](auto, auto) { return true; }, - MappingType::kSubgraph); + auto [_1, _2, ok] = vf2pp(p5, c5, qlbl, qlbl, dummy_match, dummy_match, + MappingType::kSubgraph); EXPECT_FALSE(ok); } @@ -594,9 +610,9 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { auto matcher = make_vf2pp( p5, petersen, tlbl(p5.node_ids()), tlbl); - bool ok = matcher.next([](auto, auto) { return true; }); + bool ok = matcher.next(dummy_match, dummy_match); EXPECT_TRUE(ok); - auto map = matcher.mapping(); + auto map = matcher.node_map(); // 0 0 1 1 1 0 1 2 4 3 // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 idxs << 0, 1, 2, 4, 3; @@ -605,25 +621,27 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; bool found_original = false; - while (matcher.next([](auto, auto) { return true; })) { - if (absl::c_equal(p5.node_ids(), matcher.mapping())) { + while (matcher.next(dummy_match, dummy_match)) { + if (absl::c_equal(p5.node_ids(), matcher.node_map())) { found_original = true; break; } } EXPECT_TRUE(found_original); + } - std::tie(map, ok) = vf2pp( - c5, petersen, tlbl(c5.node_ids()), tlbl, - [](auto, auto) { return true; }, MappingType::kSubgraph); + { + auto [nmap, emap, ok] = vf2pp(c5, petersen, tlbl(c5.node_ids()), tlbl, + dummy_match, dummy_match, + MappingType::kSubgraph); EXPECT_TRUE(ok); // 0 0 1 1 1 0 2 1 4 3 0 // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 idxs.head(5) << 0, 2, 1, 4, 3; expected_map.head(5) << 1, 4, 2, 7, 6; - for (int i = 0; i < map.size(); ++i) - EXPECT_EQ(map[idxs[i]], expected_map[i]) << i; + for (int i = 0; i < nmap.size(); ++i) + EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; } } } // namespace From 611d9f20a373cda203a81aba5b7e8afccbb816a0 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 17:11:17 +0900 Subject: [PATCH 17/27] fix(core/graph/vf2++): remap stale edge --- include/nuri/core/graph_vf2pp.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index e29a32e4..47a82354 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -453,11 +453,21 @@ class VF2pp { return node_match(qn, tn) && cut_by_labels(qn, tn); } + bool is_stale(const typename GT::ConstEdgeRef qe, + const typename GU::ConstEdgeRef te) { + const int curr_src = node_map_[qe.src().id()], + curr_dst = node_map_[qe.dst().id()]; + return (curr_src != te.src().id() || curr_dst != te.dst().id()) + && (curr_src != te.dst().id() || curr_dst != te.src().id()); + } + template bool map_remaining_edges(const EdgeMatch &edge_match) { for (auto qe: query().edges()) { - if (edge_map_[qe.id()] >= 0) + if (edge_map_[qe.id()] >= 0 + && !is_stale(qe, target().edge(edge_map_[qe.id()]))) { continue; + } auto teit = target().find_edge(target().node(node_map_[qe.src().id()]), target().node(node_map_[qe.dst().id()])); From 934261ce5c49bd58b5a434e83da47b296db1f68c Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 18:12:57 +0900 Subject: [PATCH 18/27] feat(core/graph/vf2++): add debug mode-only consistency check --- include/nuri/core/graph_vf2pp.h | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 47a82354..b83ede6a 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -260,8 +260,12 @@ class VF2pp { if (depth_ == query().size()) { --depth_; - if (map_remaining_edges(edge_match)) + if (map_remaining_edges(edge_match)) { +#ifdef NURI_DEBUG + first_ = false; +#endif return true; + } } const auto qn = curr_node(); @@ -457,8 +461,16 @@ class VF2pp { const typename GU::ConstEdgeRef te) { const int curr_src = node_map_[qe.src().id()], curr_dst = node_map_[qe.dst().id()]; - return (curr_src != te.src().id() || curr_dst != te.dst().id()) - && (curr_src != te.dst().id() || curr_dst != te.src().id()); + + const bool stale = + (curr_src != te.src().id() || curr_dst != te.dst().id()) + && (curr_src != te.dst().id() || curr_dst != te.src().id()); + +#ifdef NURI_DEBUG + ABSL_DCHECK(!first_ || !stale) << qe.id() << " " << te.id(); +#endif + + return stale; } template @@ -503,6 +515,9 @@ class VF2pp { ArrayXi node_tmp_; int depth_ = 0; +#ifdef NURI_DEBUG + bool first_ = true; +#endif }; template From dfef8fc8b88f683f68d0c971ad9f56386a8e8df1 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 18:13:22 +0900 Subject: [PATCH 19/27] test(core/graph/vf2++): test for edge mapping --- test/core/graph_vf2pp_test.cpp | 95 +++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 2f4cf538..34787a90 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -235,7 +235,7 @@ constexpr auto dummy_match = [](auto, auto) { return true; }; TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { GT target = lemon_petersen(); - ArrayXi expected_map(10); + ArrayXi expected_map(15); auto run_vf2pp = [&](const GT &query) { return vf2pp(query, target, dummy_match, dummy_match, @@ -250,6 +250,11 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { expected_map.head(5) << 7, 5, 8, 6, 9; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(c5, i)]), expected_map[i]); + + // lemon: 4-0, 3-4, 2-3, 1-2, 0-1 + expected_map.head(5) << 1, 2, 0, 4, 3; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } { @@ -263,9 +268,14 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { auto [nmap, emap, ok] = run_vf2pp(p10); EXPECT_TRUE(ok); - expected_map << 2, 1, 0, 4, 3, 8, 5, 7, 9, 6; + expected_map.head(10) << 2, 1, 0, 4, 3, 8, 5, 7, 9, 6; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(p10, i)]), expected_map[i]); + + // lemon: 8-9, 7-8, ..., 0-1 + expected_map.head(9) << 2, 1, 3, 4, 6, 11, 10, 14, 13; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } { @@ -279,14 +289,18 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); + for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } } TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { GT target = lemon_petersen(); - ArrayXi expected_map(10); + ArrayXi expected_map(15); auto run_vf2pp = [&](const GT &query) { return vf2pp(query, target, dummy_match, dummy_match, @@ -301,6 +315,11 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { expected_map.head(5) << 7, 5, 8, 6, 9; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(c5, i)]), expected_map[i]); + + // lemon: 4-0, 3-4, 2-3, 1-2, 0-1 + expected_map.head(5) << 1, 2, 0, 4, 3; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } { @@ -326,14 +345,18 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); + for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } } TEST(VF2ppSingleLabelSimpleMatchTest, PetersenIsomorphism) { GT target = lemon_petersen(); - ArrayXi expected_map(10); + ArrayXi expected_map(15); auto run_vf2pp = [&](const GT &query) { return vf2pp(query, target, dummy_match, dummy_match, @@ -369,8 +392,12 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenIsomorphism) { EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); + for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } } @@ -394,6 +421,9 @@ TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { expected_map << 7, 6, 5, 4, 3, 2, 1, 0, 9, 8; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(p10, i)]), expected_map[i]); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], nmap[i]); } { @@ -417,8 +447,12 @@ TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); + for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(target, nmap[lid(target, i)]), expected_map[i]); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } } @@ -449,9 +483,14 @@ TEST(VF2ppMultiLabelSimpleMatchTest, C5Petersen) { ASSERT_TRUE(ok); ArrayXi expected_map(5); + expected_map << 0, 1, 2, 3, 4; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(petersen, nmap[lid(c5, i)]), expected_map[i]); + + expected_map << 10, 11, 12, 13, 14; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]); } } @@ -466,7 +505,7 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { petersen_lbl2.head(5) << 4, 3, 2, 1, 0; petersen_lbl2.tail(5) = petersen_lbl2.head(5); - ArrayXi expected_map(10); + ArrayXi expected_map(15); absl::c_iota(expected_map, 0); for (MappingType mt: { MappingType::kSubgraph, MappingType::kInduced, @@ -480,6 +519,9 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(petersen, nmap[lid(petersen, i)]), expected_map[i]) << static_cast(mt); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << static_cast(mt); } { @@ -491,6 +533,9 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(lid(petersen, nmap[lid(petersen, i)]), expected_map[i]) << static_cast(mt); + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << static_cast(mt); } { @@ -511,9 +556,11 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { GT petersen = lemon_petersen(); ArrayXi idxs(5), expected_map(5); + // 1 3 9 10 // 0 1 2 4 3 // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 auto p5 = subgraph_from_edges(petersen, { 1, 3, 9, 10 }); + // 4 3 7 12 6 // 0 2 1 4 3 0 // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 auto c5 = subgraph_from_edges(petersen, { 3, 4, 6, 7, 12 }); @@ -527,6 +574,10 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { expected_map << 3, 0, 2, 1, 4; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + + expected_map.head(4) << 2, 1, 0, 3; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << i; } { @@ -548,6 +599,9 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[i], i) << i; + + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], i) << i; } { @@ -558,6 +612,10 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { expected_map << 3, 0, 2, 4, 1; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + + expected_map.head(4) << 2, 1, 3, 4; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << i; } { @@ -569,6 +627,11 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { expected_map << 0, 2, 4, 1, 3; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + + idxs << 1, 0, 3, 4, 2; + expected_map << 1, 3, 4, 0, 2; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[idxs[i]], expected_map[i]) << i; } } @@ -576,9 +639,11 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { GT petersen = lemon_petersen(); ArrayXi qlbl(5), tlbl(10), idxs(5), expected_map(5); + // 1 3 9 10 // 0 1 2 1 1 0 1 2 4 3 // {0, 2, 4, 5, 9}, connection: 0 - 2 - 4 - 9 - 5 auto p5 = subgraph_from_edges(petersen, { 1, 3, 9, 10 }); + // 4 3 7 12 6 // 0 2 1 1 1 0 2 1 4 3 0 // {1, 2, 4, 6, 7}, connection: 1 - 4 - 2 - 7 - 6 - 1 auto c5 = subgraph_from_edges(petersen, { 3, 4, 6, 7, 12 }); @@ -594,6 +659,10 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { expected_map << 0, 2, 1, 4, 3; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + + expected_map.head(4) << 1, 0, 3, 4; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << i; } { @@ -622,9 +691,17 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { bool found_original = false; while (matcher.next(dummy_match, dummy_match)) { - if (absl::c_equal(p5.node_ids(), matcher.node_map())) { + if (absl::c_equal(p5.node_ids(), matcher.node_map())) found_original = true; - break; + + for (int i = 0; i < p5.num_edges(); ++i) { + int src = matcher.node_map()[p5.edge(i).src().id()], + dst = matcher.node_map()[p5.edge(i).dst().id()]; + + auto edge = petersen.edge(matcher.edge_map()[i]); + + EXPECT_TRUE((edge.src().id() == src && edge.dst().id() == dst) + || (edge.src().id() == dst && edge.dst().id() == src)); } } EXPECT_TRUE(found_original); @@ -642,6 +719,10 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { expected_map.head(5) << 1, 4, 2, 7, 6; for (int i = 0; i < nmap.size(); ++i) EXPECT_EQ(nmap[idxs[i]], expected_map[i]) << i; + + expected_map.head(5) << 3, 4, 6, 7, 12; + for (int i = 0; i < emap.size(); ++i) + EXPECT_EQ(emap[i], expected_map[i]) << i; } } } // namespace From eaa67e15c0f0ac440ec0a385c488b4f2bfa5b5d1 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 21:05:47 +0900 Subject: [PATCH 20/27] perf(core/graph): optimize find_adjacent/edge family of Subgraph --- include/nuri/core/graph.h | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/include/nuri/core/graph.h b/include/nuri/core/graph.h index 121e3d67..bb9122bf 100644 --- a/include/nuri/core/graph.h +++ b/include/nuri/core/graph.h @@ -1582,11 +1582,18 @@ namespace internal { } private: + friend SGT; + template friend class SubAdjIterator; friend class boost::iterator_core_access; + constexpr SubAdjIterator(parent_type &subgraph, int src, int dst, int eid, + int parent_idx) noexcept + : subgraph_(&subgraph), src_(src), dst_(dst), eid_(eid), + parent_idx_(parent_idx) { } + auto parent_iter() const { return subgraph_->parent_node(src_).begin() + parent_idx_; } @@ -2995,31 +3002,37 @@ class Subgraph { } adjacency_iterator find_adjacent(int src, int dst) { - return find_adj_helper(*this, src, dst); + return find_adj_helper(*this, src, dst); } const_adjacency_iterator find_adjacent(int src, int dst) const { - return find_adj_helper(*this, src, dst); + return find_adj_helper(*this, src, dst); } - template - static auto find_adj_helper(SGT &graph, int src, int dst) { - auto ret = graph.adj_begin(src); + template + static AIT find_adj_helper(SGT &graph, int src, int dst) { + auto pait = graph.parent_node(src).find_adjacent(graph.parent_node(dst)); + if (pait.end()) + return graph.adj_end(src); - for (; ret != graph.adj_end(src); ++ret) - if (ret->dst().id() == dst) - break; + auto eit = graph.find_edge(pait->eid()); + if (eit == graph.edge_end()) + return graph.adj_end(src); - return ret; + return { graph, src, dst, eit->id(), + pait - graph.parent_node(src).begin() }; } template static auto find_edge_helper(SGT &graph, int src, int dst) { - auto it = find_adj_helper(graph, src, dst); - if (it.end()) + if (graph.parent_node(src).degree() > graph.parent_node(dst).degree()) + std::swap(src, dst); + + auto ait = graph.find_adjacent(src, dst); + if (ait.end()) return graph.edge_end(); - return graph.edge_begin() + it->eid(); + return graph.edge_begin() + ait->eid(); } parent_type *parent_; From ed418ce649e23564489f859f8c037c8cabc26d29 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 21:12:45 +0900 Subject: [PATCH 21/27] docs(core/graph): correct time complexity --- include/nuri/core/graph.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/nuri/core/graph.h b/include/nuri/core/graph.h index bb9122bf..42538c30 100644 --- a/include/nuri/core/graph.h +++ b/include/nuri/core/graph.h @@ -2751,7 +2751,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(V'/E' log E')\f$. + * @note Time complexity: \f$O(E/V + log E')\f$. */ edge_iterator find_edge(ConstNodeRef src, ConstNodeRef dst) { return find_edge(src.id(), dst.id()); @@ -2764,7 +2764,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(V'/E' log E')\f$. + * @note Time complexity: \f$O(E/V + log E')\f$. */ const_edge_iterator find_edge(ConstNodeRef src, ConstNodeRef dst) const { return find_edge(src.id(), dst.id()); @@ -2778,7 +2778,7 @@ class Subgraph { * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. If any of the * nodes are not in the subgraph, returns edge_end(). - * @note Time complexity: \f$O(log V' + V'/E' log E')\f$. + * @note Time complexity: \f$O(log V' + E/V + log E')\f$. */ edge_iterator find_edge(typename graph_type::ConstNodeRef src, typename graph_type::ConstNodeRef dst) { @@ -2797,7 +2797,7 @@ class Subgraph { * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. If any of the * nodes are not in the subgraph, returns edge_end(). - * @note Time complexity: \f$O(log V' + V'/E' log E')\f$. + * @note Time complexity: \f$O(log V' + E/V + log E')\f$. */ const_edge_iterator find_edge(typename graph_type::ConstNodeRef src, typename graph_type::ConstNodeRef dst) const { @@ -2904,7 +2904,7 @@ class Subgraph { * @param idx The index of the node * @return The number of neighbors of the node that are in the subgraph * @note The behavior is undefined if the node is not in the subgraph. - * @note Time complexity: \f$O(V/E)\f$. + * @note Time complexity: \f$O(E/V (log V' + log E'))\f$. */ int degree(int idx) const { return std::distance(adj_cbegin(idx), adj_cend(idx)); @@ -2917,7 +2917,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the adjacent node if found, adj_end(src) otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(V'/E' log E')\f$. + * @note Time complexity: \f$O(E/V + log E')\f$. */ adjacency_iterator find_adjacent(ConstNodeRef src, ConstNodeRef dst) { return find_adjacent(src.id(), dst.id()); @@ -2931,7 +2931,7 @@ class Subgraph { * @return A const-iterator to the adjacent node if found, adj_end(src) * otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(V'/E' log E')\f$. + * @note Time complexity: \f$O(E/V + log E')\f$. */ const_adjacency_iterator find_adjacent(ConstNodeRef src, ConstNodeRef dst) const { From e1bbf16cb2c1dcbe96160f185bdb665d4ee4c3b8 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Thu, 12 Dec 2024 21:21:49 +0900 Subject: [PATCH 22/27] test(core/graph): remove out-of-bounds access in test --- test/core/molecule_substructure_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/molecule_substructure_test.cpp b/test/core/molecule_substructure_test.cpp index eaac1f3a..f78e1270 100644 --- a/test/core/molecule_substructure_test.cpp +++ b/test/core/molecule_substructure_test.cpp @@ -319,14 +319,14 @@ TEST_F(SubstructureTest, FindNeighbors) { EXPECT_FALSE(sub_.find_neighbor(sub_.atom(1), sub_.atom(2)).end()); EXPECT_FALSE(sub_.find_neighbor(sub_.atom(2), sub_.atom(0)).end()); - EXPECT_TRUE(sub_.find_neighbor(sub_.atom(0), sub_.atom(4)).end()); + EXPECT_TRUE(sub_.find_neighbor(sub_.atom(0), sub_.atom(3)).end()); const Substructure &csub = sub_; EXPECT_FALSE(csub.find_neighbor(sub_.atom(0), sub_.atom(1)).end()); EXPECT_FALSE(csub.find_neighbor(sub_.atom(1), sub_.atom(2)).end()); EXPECT_FALSE(csub.find_neighbor(sub_.atom(2), sub_.atom(0)).end()); - EXPECT_TRUE(csub.find_neighbor(sub_.atom(0), sub_.atom(4)).end()); + EXPECT_TRUE(csub.find_neighbor(sub_.atom(0), sub_.atom(3)).end()); } TEST_F(SubstructureTest, IterateNeighbors) { From 1c3eb3eed70266c2d92d4bc4d4f3fde77952f6f4 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 13 Dec 2024 09:20:38 +0900 Subject: [PATCH 23/27] docs(core/graph): use correct log symbol --- include/nuri/core/graph.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/include/nuri/core/graph.h b/include/nuri/core/graph.h index 42538c30..a073ec1c 100644 --- a/include/nuri/core/graph.h +++ b/include/nuri/core/graph.h @@ -2398,7 +2398,7 @@ class Subgraph { * * @param begin Iterator pointing to the first node to erase * @param end Iterator pointing to the node after the last node to erase - * @note Time complexity: \f$O(V'log(E') + E')\f$ in worst case. + * @note Time complexity: \f$O(V' \log E' + E')\f$ in worst case. */ void erase_nodes(const_iterator begin, const_iterator end) { std::vector erased_edges; @@ -2417,7 +2417,7 @@ class Subgraph { * * @param id The id of the node to erase * @note This is a no-op if the node is not in the subgraph. - * @note Time complexity: \f$O(V'log(E') + E')\f$ in worst case. + * @note Time complexity: \f$O(V' \log E' + E')\f$ in worst case. */ void erase_node_of(int id) { int idx = nodes_.find(id); @@ -2431,7 +2431,7 @@ class Subgraph { * * @param node The parent node to erase * @note This is a no-op if the node is not in the subgraph. - * @note Time complexity: \f$O(V'log(E') + E')\f$ in worst case. + * @note Time complexity: \f$O(V' \log E' + E')\f$ in worst case. */ void erase_node_of(typename graph_type::ConstNodeRef node) { erase_node_of(node.id()); @@ -2443,7 +2443,7 @@ class Subgraph { * * @tparam UnaryPred Type of the unary predicate * @param pred Unary predicate that returns true for nodes to erase - * @note Time complexity: \f$O(V'log(E') + E')\f$ in worst case. + * @note Time complexity: \f$O(V' \log E' + E')\f$ in worst case. */ template void erase_nodes_if(UnaryPred pred) { @@ -2751,7 +2751,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(E/V + log E')\f$. + * @note Time complexity: \f$O(E/V + \log E')\f$. */ edge_iterator find_edge(ConstNodeRef src, ConstNodeRef dst) { return find_edge(src.id(), dst.id()); @@ -2764,7 +2764,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(E/V + log E')\f$. + * @note Time complexity: \f$O(E/V + \log E')\f$. */ const_edge_iterator find_edge(ConstNodeRef src, ConstNodeRef dst) const { return find_edge(src.id(), dst.id()); @@ -2778,7 +2778,7 @@ class Subgraph { * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. If any of the * nodes are not in the subgraph, returns edge_end(). - * @note Time complexity: \f$O(log V' + E/V + log E')\f$. + * @note Time complexity: \f$O(\log V' + E/V + \log E')\f$. */ edge_iterator find_edge(typename graph_type::ConstNodeRef src, typename graph_type::ConstNodeRef dst) { @@ -2797,7 +2797,7 @@ class Subgraph { * @return An iterator to the edge if found, edge_end() otherwise. * @note This will only find edges that are in the subgraph. If any of the * nodes are not in the subgraph, returns edge_end(). - * @note Time complexity: \f$O(log V' + E/V + log E')\f$. + * @note Time complexity: \f$O(\log V' + E/V + \log E')\f$. */ const_edge_iterator find_edge(typename graph_type::ConstNodeRef src, typename graph_type::ConstNodeRef dst) const { @@ -2904,7 +2904,7 @@ class Subgraph { * @param idx The index of the node * @return The number of neighbors of the node that are in the subgraph * @note The behavior is undefined if the node is not in the subgraph. - * @note Time complexity: \f$O(E/V (log V' + log E'))\f$. + * @note Time complexity: \f$O(E/V (\log V' + \log E'))\f$. */ int degree(int idx) const { return std::distance(adj_cbegin(idx), adj_cend(idx)); @@ -2917,7 +2917,7 @@ class Subgraph { * @param dst The destination atom * @return An iterator to the adjacent node if found, adj_end(src) otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(E/V + log E')\f$. + * @note Time complexity: \f$O(E/V + \log E')\f$. */ adjacency_iterator find_adjacent(ConstNodeRef src, ConstNodeRef dst) { return find_adjacent(src.id(), dst.id()); @@ -2931,7 +2931,7 @@ class Subgraph { * @return A const-iterator to the adjacent node if found, adj_end(src) * otherwise. * @note This will only find edges that are in the subgraph. - * @note Time complexity: \f$O(E/V + log E')\f$. + * @note Time complexity: \f$O(E/V + \log E')\f$. */ const_adjacency_iterator find_adjacent(ConstNodeRef src, ConstNodeRef dst) const { From ecd25f04413ac753f04306a0ff18a7c52388681f Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 13 Dec 2024 12:57:05 +0900 Subject: [PATCH 24/27] refactor(core/graph/vf2++): rename MappingType -> IsoMapType --- include/nuri/core/graph_vf2pp.h | 52 +++++++++++++++---------------- test/core/graph_vf2pp_test.cpp | 54 ++++++++++++++++----------------- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index b83ede6a..a600a526 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -23,10 +23,10 @@ #include "nuri/utils.h" namespace nuri { -enum class MappingType : int { - kSubgraph, // Subgraph isomorphism - kInduced, // Induced subgraph isomorphism - kIsomorphism, // Graph isomorphism +enum class IsoMapType : int { + kSubgraph, // Subgraph isomorphism + kInduced, // Induced subgraph isomorphism + kGraph, // Graph isomorphism }; namespace internal { @@ -194,16 +194,16 @@ namespace internal { return { r_inout_labels, r_new_labels }; } - template + template bool vf2pp_r_matches(const Vf2ppLabels &r_node, const ArrayXi &label_tmp) { return absl::c_none_of(r_node, [&](std::pair p) { - return kMt == MappingType::kIsomorphism ? label_tmp[p.first] != 0 - : label_tmp[p.first] > 0; + return kMt == IsoMapType::kGraph ? label_tmp[p.first] != 0 + : label_tmp[p.first] > 0; }); } } // namespace internal -template +template class VF2pp { private: const GT &query() const { return *query_; } @@ -293,7 +293,7 @@ class VF2pp { const int tj = std::find_if(target().begin() + ti + 1, target().end(), [&](auto tn) { - bool candidate = kMt == MappingType::kSubgraph + bool candidate = kMt == IsoMapType::kSubgraph ? conn()[tn.id()] >= 0 : conn()[tn.id()] == 0; return candidate && feas(qn, tn, node_match); @@ -377,7 +377,7 @@ class VF2pp { // zero init for (auto [lbl, _]: r_inout_[qn.id()]) r_inout_cnt()[lbl] = 0; - if constexpr (kMt != MappingType::kSubgraph) { + if constexpr (kMt != IsoMapType::kSubgraph) { for (auto [lbl, _]: r_new_[qn.id()]) r_new_cnt()[lbl] = 0; } @@ -386,7 +386,7 @@ class VF2pp { const int curr = nei.dst().id(); if (conn()[curr] > 0) { --r_inout_cnt()[tlbl_[curr]]; - } else if constexpr (kMt != MappingType::kSubgraph) { + } else if constexpr (kMt != IsoMapType::kSubgraph) { if (conn()[curr] == 0) --r_new_cnt()[tlbl_[curr]]; } @@ -394,14 +394,14 @@ class VF2pp { for (auto [lbl, cnt]: r_inout_[qn.id()]) r_inout_cnt()[lbl] += cnt; - if constexpr (kMt != MappingType::kSubgraph) { + if constexpr (kMt != IsoMapType::kSubgraph) { for (auto [lbl, cnt]: r_new_[qn.id()]) r_new_cnt()[lbl] += cnt; } const bool r_inout_match = internal::vf2pp_r_matches(r_inout_[qn.id()], r_inout_cnt()); - if constexpr (kMt == MappingType::kSubgraph) + if constexpr (kMt == IsoMapType::kSubgraph) return r_inout_match; return r_inout_match @@ -423,7 +423,7 @@ class VF2pp { const int curr_conn = conn()[tnei.dst().id()]; if (curr_conn < -1) ++conn()[tnei.dst().id()]; - else if constexpr (kMt != MappingType::kSubgraph) { + else if constexpr (kMt != IsoMapType::kSubgraph) { if (curr_conn == -1) { is_iso = false; break; @@ -447,7 +447,7 @@ class VF2pp { const int curr_conn = conn()[ti]; conn()[ti] = -1; - if constexpr (kMt != MappingType::kSubgraph) + if constexpr (kMt != IsoMapType::kSubgraph) return false; if (curr_conn < -1) @@ -520,7 +520,7 @@ class VF2pp { #endif }; -template +template VF2pp make_vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl) { return VF2pp(query, target, std::forward(qlbl), @@ -533,7 +533,7 @@ struct VF2ppResult { bool found; }; -template VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, const NodeMatch &node_match, const EdgeMatch &edge_match) { @@ -547,20 +547,20 @@ template VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, const NodeMatch &node_match, const EdgeMatch &edge_match, - MappingType mt) { + IsoMapType mt) { switch (mt) { - case MappingType::kSubgraph: - return vf2pp( // + case IsoMapType::kSubgraph: + return vf2pp( // query, target, // std::forward(qlbl), std::forward(tlbl), // node_match, edge_match); - case MappingType::kInduced: - return vf2pp( // + case IsoMapType::kInduced: + return vf2pp( // query, target, // std::forward(qlbl), std::forward(tlbl), // node_match, edge_match); - case MappingType::kIsomorphism: - return vf2pp( // + case IsoMapType::kGraph: + return vf2pp( // query, target, // std::forward(qlbl), std::forward(tlbl), // node_match, edge_match); @@ -569,7 +569,7 @@ VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, ABSL_UNREACHABLE(); } -template +template VF2ppResult vf2pp(const GT &query, const GU &target, const NodeMatch &node_match, const EdgeMatch &edge_match) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); @@ -580,7 +580,7 @@ VF2ppResult vf2pp(const GT &query, const GU &target, template VF2ppResult vf2pp(const GT &query, const GU &target, const NodeMatch &node_match, const EdgeMatch &edge_match, - MappingType mt) { + IsoMapType mt) { ArrayXi label = ArrayXi::Zero(nuri::max(query.size(), target.size())); return vf2pp(query, target, label.head(query.size()), label.head(target.size()), node_match, edge_match, mt); diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 34787a90..594c876b 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -239,7 +239,7 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenSubgraph) { auto run_vf2pp = [&](const GT &query) { return vf2pp(query, target, dummy_match, dummy_match, - MappingType::kSubgraph); + IsoMapType::kSubgraph); }; { @@ -303,8 +303,7 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { ArrayXi expected_map(15); auto run_vf2pp = [&](const GT &query) { - return vf2pp(query, target, dummy_match, dummy_match, - MappingType::kInduced); + return vf2pp(query, target, dummy_match, dummy_match, IsoMapType::kInduced); }; { @@ -354,13 +353,12 @@ TEST(VF2ppSingleLabelSimpleMatchTest, PetersenInduced) { } } -TEST(VF2ppSingleLabelSimpleMatchTest, PetersenIsomorphism) { +TEST(VF2ppSingleLabelSimpleMatchTest, PetersenGraph) { GT target = lemon_petersen(); ArrayXi expected_map(15); auto run_vf2pp = [&](const GT &query) { - return vf2pp(query, target, dummy_match, dummy_match, - MappingType::kIsomorphism); + return vf2pp(query, target, dummy_match, dummy_match, IsoMapType::kGraph); }; { @@ -405,17 +403,17 @@ TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { GT petersen = lemon_petersen(), p10 = lemon_p_n(10), target = lemon_c_n(10); ArrayXi expected_map(10); - auto run_vf2pp = [&](const GT &query, MappingType mt) { + auto run_vf2pp = [&](const GT &query, IsoMapType mt) { return vf2pp(query, target, dummy_match, dummy_match, mt); }; { - auto [_1, _2, ok] = run_vf2pp(petersen, MappingType::kSubgraph); + auto [_1, _2, ok] = run_vf2pp(petersen, IsoMapType::kSubgraph); EXPECT_FALSE(ok); } { - auto [nmap, emap, ok] = run_vf2pp(p10, MappingType::kSubgraph); + auto [nmap, emap, ok] = run_vf2pp(p10, IsoMapType::kSubgraph); EXPECT_TRUE(ok); expected_map << 7, 6, 5, 4, 3, 2, 1, 0, 9, 8; @@ -427,23 +425,23 @@ TEST(VF2ppSingleLabelSimpleMatchTest, C10P10) { } { - auto [_1, _2, ok] = run_vf2pp(p10, MappingType::kInduced); + auto [_1, _2, ok] = run_vf2pp(p10, IsoMapType::kInduced); EXPECT_FALSE(ok); } { - auto [_1, _2, ok] = run_vf2pp(p10, MappingType::kIsomorphism); + auto [_1, _2, ok] = run_vf2pp(p10, IsoMapType::kGraph); EXPECT_FALSE(ok); } { auto [_1, _2, ok] = // NOLINTNEXTLINE(*-suspicious-call-argument) - vf2pp(target, p10, dummy_match, dummy_match, MappingType::kIsomorphism); + vf2pp(target, p10, dummy_match, dummy_match, IsoMapType::kGraph); EXPECT_FALSE(ok); } { - auto [nmap, emap, ok] = run_vf2pp(target, MappingType::kIsomorphism); + auto [nmap, emap, ok] = run_vf2pp(target, IsoMapType::kGraph); EXPECT_TRUE(ok); absl::c_iota(expected_map, 0); @@ -472,14 +470,14 @@ TEST(VF2ppMultiLabelSimpleMatchTest, C5Petersen) { { auto [_1, _2, ok] = vf2pp(c5, petersen, c5_lbl, petersen_lbl1, dummy_match, - dummy_match, MappingType::kSubgraph); + dummy_match, IsoMapType::kSubgraph); EXPECT_FALSE(ok); } { auto [nmap, emap, ok] = vf2pp(c5, petersen, c5_lbl, petersen_lbl2, dummy_match, dummy_match, - MappingType::kSubgraph); + IsoMapType::kSubgraph); ASSERT_TRUE(ok); ArrayXi expected_map(5); @@ -508,8 +506,8 @@ TEST(VF2ppMultiLabelSimpleMatchTest, PetersenPetersen) { ArrayXi expected_map(15); absl::c_iota(expected_map, 0); - for (MappingType mt: { MappingType::kSubgraph, MappingType::kInduced, - MappingType::kIsomorphism }) { + for (IsoMapType mt: + { IsoMapType::kSubgraph, IsoMapType::kInduced, IsoMapType::kGraph }) { { auto [nmap, emap, ok] = vf2pp(petersen, petersen, petersen_lbl1, petersen_lbl1, dummy_match, dummy_match, @@ -568,7 +566,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { idxs << 0, 1, 2, 4, 3; { auto [nmap, emap, ok] = - vf2pp(p5, c5, dummy_match, dummy_match, MappingType::kSubgraph); + vf2pp(p5, c5, dummy_match, dummy_match, IsoMapType::kSubgraph); EXPECT_TRUE(ok); expected_map << 3, 0, 2, 1, 4; @@ -582,19 +580,19 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { { auto [_1, _2, ok] = - vf2pp(p5, c5, dummy_match, dummy_match, MappingType::kInduced); + vf2pp(p5, c5, dummy_match, dummy_match, IsoMapType::kInduced); EXPECT_FALSE(ok); } { auto [_1, _2, ok] = - vf2pp(c5, p5, dummy_match, dummy_match, MappingType::kSubgraph); + vf2pp(c5, p5, dummy_match, dummy_match, IsoMapType::kSubgraph); EXPECT_FALSE(ok); } { auto [nmap, emap, ok] = - vf2pp(c5, c5, dummy_match, dummy_match, MappingType::kIsomorphism); + vf2pp(c5, c5, dummy_match, dummy_match, IsoMapType::kGraph); EXPECT_TRUE(ok); for (int i = 0; i < nmap.size(); ++i) @@ -606,7 +604,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { { auto [nmap, emap, ok] = - vf2pp(p5, petersen, dummy_match, dummy_match, MappingType::kSubgraph); + vf2pp(p5, petersen, dummy_match, dummy_match, IsoMapType::kSubgraph); EXPECT_TRUE(ok); expected_map << 3, 0, 2, 4, 1; @@ -620,7 +618,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, SingleLabel) { { auto [nmap, emap, ok] = - vf2pp(c5, petersen, dummy_match, dummy_match, MappingType::kSubgraph); + vf2pp(c5, petersen, dummy_match, dummy_match, IsoMapType::kSubgraph); EXPECT_TRUE(ok); idxs << 0, 2, 1, 4, 3; @@ -652,7 +650,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { qlbl << 0, 1, 2, 1, 1; tlbl.head(5) << 0, 2, 1, 1, 1; auto [nmap, emap, ok] = vf2pp(p5, c5, qlbl, tlbl.head(5), dummy_match, - dummy_match, MappingType::kSubgraph); + dummy_match, IsoMapType::kSubgraph); EXPECT_TRUE(ok); idxs << 0, 1, 2, 4, 3; @@ -668,7 +666,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { { qlbl << 0, 0, 1, 1, 1; auto [_1, _2, ok] = vf2pp(p5, c5, qlbl, qlbl, dummy_match, dummy_match, - MappingType::kSubgraph); + IsoMapType::kSubgraph); EXPECT_FALSE(ok); } @@ -676,8 +674,8 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { tlbl.tail(6).setOnes(); { - auto matcher = make_vf2pp( - p5, petersen, tlbl(p5.node_ids()), tlbl); + auto matcher = make_vf2pp(p5, petersen, + tlbl(p5.node_ids()), tlbl); bool ok = matcher.next(dummy_match, dummy_match); EXPECT_TRUE(ok); @@ -710,7 +708,7 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { { auto [nmap, emap, ok] = vf2pp(c5, petersen, tlbl(c5.node_ids()), tlbl, dummy_match, dummy_match, - MappingType::kSubgraph); + IsoMapType::kSubgraph); EXPECT_TRUE(ok); // 0 0 1 1 1 0 2 1 4 3 0 From 6eeec6dcf8a5dab70048d12ac490c99218bcdc34 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 13 Dec 2024 12:54:45 +0900 Subject: [PATCH 25/27] docs(core/graph/vf2++): add docstrings and license text --- NOTICE.md | 37 +++++ include/nuri/core/graph_vf2pp.h | 259 +++++++++++++++++++++++++++++++- 2 files changed, 293 insertions(+), 3 deletions(-) diff --git a/NOTICE.md b/NOTICE.md index 1833f1fd..2aa99812 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -533,3 +533,40 @@ Our implementation is based on the original TM-align software (version 20220412) notice appear in all copies or substantial portions of the Software. It is provided "as is" without express or implied warranty. ``` + +### VF2++ + +- Project URL: +- Full license text: + + ```txt + Copyright (C) 2003-2012 Egervary Jeno Kombinatorikus Optimalizalasi + Kutatocsoport (Egervary Combinatorial Optimization Research Group, + EGRES). + + =========================================================================== + Boost Software License, Version 1.0 + =========================================================================== + + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + ``` diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index a600a526..90cca585 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -23,10 +23,16 @@ #include "nuri/utils.h" namespace nuri { +/** + * @brief The type of isomorphic map to find. + */ enum class IsoMapType : int { - kSubgraph, // Subgraph isomorphism - kInduced, // Induced subgraph isomorphism - kGraph, // Graph isomorphism + /// Subgraph isomorphism. + kSubgraph, + /// Induced subgraph isomorphism. + kInduced, + /// Graph isomorphism. + kGraph, }; namespace internal { @@ -203,6 +209,62 @@ namespace internal { } } // namespace internal +/** + * @brief An implementation of VF2++ algorithm for (sub)graph isomorphism. + * + * @tparam kMt The type of subgraph mapping. + * @tparam GT The type of the query graph ("needle"). + * @tparam GU The type of the target graph ("haystack"). + * + * This class implements the VF2++ algorithm for subgraph isomorphism. The + * implementation is based on the reference implementation in LEMON project. + * Here, we provide two extra functionalities: + * + * 1. Support for extra node and edge matching functions. + * 2. In addition to the node mapping, the algorithm also returns the edge + * mapping. + * + * See the following paper for details of the algorithm. + * + * Reference: + * - A Jüttner and P Madarasi. *Discrete Appl. Math.* **2018**, *247*, + * 69-81. + * DOI:[10.1016/j.dam.2018.02.018](https://doi.org/10.1016/j.dam.2018.02.018) + * + * Here follows the full license text for the LEMON project: + * + * \code{.unparsed} + * Copyright (C) 2003-2012 Egervary Jeno Kombinatorikus Optimalizalasi + * Kutatocsoport (Egervary Combinatorial Optimization Research Group, + * EGRES). + * + * =========================================================================== + * Boost Software License, Version 1.0 + * =========================================================================== + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * \endcode + */ template class VF2pp { private: @@ -228,6 +290,27 @@ class VF2pp { ArrayXi &r_new_cnt() { return label_tmp2_; } public: + /** + * @brief Prepare VF2++ algorithm. + * + * @tparam AL1 The type of query node label array. Must be representable as + * an Eigen 1D array of integers. + * @tparam AL2 The type of target node label array. Must be representable as + * an Eigen 1D array of integers. + * @param query The query graph. + * @param target The target graph. + * @param query_lbl The query node labels. Target nodes with the different + * labels will not be considered for matching. + * @param target_lbl The target node labels. Target nodes must have the same + * label with the corresponding query nodes to be considered for + * matching. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa make_vf2pp() + */ template VF2pp(const GT &query, const GU &target, AL1 &&query_lbl, AL2 &&target_lbl) : query_(&query), target_(&target), // @@ -254,6 +337,31 @@ class VF2pp { query_tmp().setZero(); } + /** + * @brief Find next subgraph mapping. + * + * @tparam NodeMatch A binary predicate that takes two nodes and returns + * whether the nodes are matched. + * @tparam EdgeMatch A binary predicate that takes two edges and returns + * whether the edges are matched. + * @param node_match The node matching function. Must return true if the + * (query, target) node pair is matched. Is guaranteed to be called + * with topologically feasible and label-matching nodes. + * @param edge_match The edge matching function. Must return true if the + * (query, target) edge pair is matched. Each ends of the edge are + * guaranteed to be already-matched nodes. Note that depending on the + * query and target graphs, the edge may be matched in reverse order, + * i.e., target edge might have source and target nodes swapped + * compared to the query edge. + * @return Whether the mapping is found. Once this function returns false, + * all subsequent calls will also return false. + * + * @note This function will find overlapping matches by default. If + * non-overlapping matches are required, supply node_match and + * edge_match functions that return false for already matched nodes + * and/or edges. + * @sa vf2pp() + */ template bool next(const NodeMatch &node_match, const EdgeMatch &edge_match) { while (depth_ >= 0) { @@ -331,10 +439,44 @@ class VF2pp { return false; } + /** + * @brief Get current node mapping. + * @return The node mapping, where the index is the query node ID and the + * value is the target node ID. + * @pre Last call to next() returned true, otherwise the return value is + * unspecified. + */ const ArrayXi &node_map() const & { return node_map_; } + + /** + * @brief Move out current node mapping. + * @return The node mapping, where the index is the query node ID and the + * value is the target node ID. + * @pre Last call to next() returned true, otherwise the return value is + * unspecified. + * @note This function invalidates the current object. Only edge_map() can + * be called after this function. + */ ArrayXi &&node_map() && { return std::move(node_map_); } + /** + * @brief Get current edge mapping. + * @return The edge mapping, where the index is the query edge ID and the + * value is the target edge ID. + * @pre Last call to next() returned true, otherwise the return value is + * unspecified. + */ const ArrayXi &edge_map() const & { return edge_map_; } + + /** + * @brief Move out current edge mapping. + * @return The edge mapping, where the index is the query edge ID and the + * value is the target edge ID. + * @pre Last call to next() returned true, otherwise the return value is + * unspecified. + * @note This function invalidates the current object. Only node_map() can + * be called after this function. + */ ArrayXi &&edge_map() && { return std::move(edge_map_); } private: @@ -520,6 +662,21 @@ class VF2pp { #endif }; +/** + * @brief Prepare VF2++ algorithm. + * + * @param query The query graph ("needle"). + * @param target The target graph ("haystack"). + * @param qlbl The query node labels. + * @param tlbl The target node labels. + * @return The constructed VF2++ algorithm object. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa VF2pp::VF2pp() + */ template VF2pp make_vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl) { @@ -533,6 +690,30 @@ struct VF2ppResult { bool found; }; +/** + * @brief Find a query-to-target subgraph mapping. + * + * @param query The query graph ("needle"). + * @param target The target graph ("haystack"). + * @param qlbl The query node labels. + * @param tlbl The target node labels. + * @param node_match The node matching function. Must return true if the (query, + * target) node pair is matched. Is guaranteed to be called with + * topologically feasible and label-matching nodes. + * @param edge_match The edge matching function. Must return true if the (query, + * target) edge pair is matched. Each ends of the edge are guaranteed to + * be already-matched nodes. Note that depending on the query and target + * graphs, the edge may be matched in reverse order, i.e., target edge + * might have source and target nodes swapped compared to the query edge. + * @return The node and edge mapping, and whether the mapping is found. Mappings + * are valid only if the last element is true. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa VF2pp::next() + */ template VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, @@ -543,6 +724,31 @@ VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, return { std::move(vf2pp).node_map(), std::move(vf2pp).edge_map(), found }; } +/** + * @brief Find a query-to-target subgraph mapping. + * + * @param query The query graph ("needle"). + * @param target The target graph ("haystack"). + * @param qlbl The query node labels. + * @param tlbl The target node labels. + * @param node_match The node matching function. Must return true if the (query, + * target) node pair is matched. Is guaranteed to be called with + * topologically feasible and label-matching nodes. + * @param edge_match The edge matching function. Must return true if the (query, + * target) edge pair is matched. Each ends of the edge are guaranteed to + * be already-matched nodes. Note that depending on the query and target + * graphs, the edge may be matched in reverse order, i.e., target edge + * might have source and target nodes swapped compared to the query edge. + * @param mt The type of subgraph mapping. + * @return The node and edge mapping, and whether the mapping is found. Mappings + * are valid only if the last element is true. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa VF2pp::next() + */ template VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, @@ -569,6 +775,29 @@ VF2ppResult vf2pp(const GT &query, const GU &target, AL1 &&qlbl, AL2 &&tlbl, ABSL_UNREACHABLE(); } +/** + * @brief Find a query-to-target subgraph mapping assuming all node labels are + * equal. + * + * @param query The query graph ("needle"). + * @param target The target graph ("haystack"). + * @param node_match The node matching function. Must return true if the (query, + * target) node pair is matched. Is guaranteed to be called with + * topologically feasible and label-matching nodes. + * @param edge_match The edge matching function. Must return true if the (query, + * target) edge pair is matched. Each ends of the edge are guaranteed to + * be already-matched nodes. Note that depending on the query and target + * graphs, the edge may be matched in reverse order, i.e., target edge + * might have source and target nodes swapped compared to the query edge. + * @return The node and edge mapping, and whether the mapping is found. Mappings + * are valid only if the last element is true. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa VF2pp::next() + */ template VF2ppResult vf2pp(const GT &query, const GU &target, const NodeMatch &node_match, const EdgeMatch &edge_match) { @@ -577,6 +806,30 @@ VF2ppResult vf2pp(const GT &query, const GU &target, label.head(target.size()), node_match, edge_match); } +/** + * @brief Find a query-to-target subgraph mapping assuming all node labels are + * equal. + * + * @param query The query graph ("needle"). + * @param target The target graph ("haystack"). + * @param node_match The node matching function. Must return true if the (query, + * target) node pair is matched. Is guaranteed to be called with + * topologically feasible and label-matching nodes. + * @param edge_match The edge matching function. Must return true if the (query, + * target) edge pair is matched. Each ends of the edge are guaranteed to + * be already-matched nodes. Note that depending on the query and target + * graphs, the edge may be matched in reverse order, i.e., target edge + * might have source and target nodes swapped compared to the query edge. + * @param mt The type of subgraph mapping. + * @return The node and edge mapping, and whether the mapping is found. Mappings + * are valid only if the last element is true. + * + * @note Both graphs must not be empty, the query graph must be smaller + * or equal to the target graph, and each node labels must not contain + * non-negative labels and have same size as the corresponding graph. + * The behavior is undefined if any of the conditions are not met. + * @sa VF2pp::next() + */ template VF2ppResult vf2pp(const GT &query, const GU &target, const NodeMatch &node_match, const EdgeMatch &edge_match, From 4d15096fc66e69bb5a662d81b9f91fba151daac4 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 13 Dec 2024 13:59:00 +0900 Subject: [PATCH 26/27] feat(core/graph/vf2++): support search for non-overlapping match --- include/nuri/core/graph_vf2pp.h | 83 ++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/include/nuri/core/graph_vf2pp.h b/include/nuri/core/graph_vf2pp.h index 90cca585..c3477b4f 100644 --- a/include/nuri/core/graph_vf2pp.h +++ b/include/nuri/core/graph_vf2pp.h @@ -275,15 +275,16 @@ class VF2pp { ArrayXi &conn() { return node_tmp_; } - auto curr_node() const { return query_->node(order_[depth_]); } + auto curr_node(int i) const { return query_->node(order_[i]); } + auto curr_node() const { return curr_node(depth_); } - int mapped_node() const { return node_map_[curr_node().id()]; } + int mapped_node(int i) const { return node_map_[curr_node(i).id()]; } + int mapped_node() const { return mapped_node(depth_); } auto query_target_ait() const { return query_target_ait_[depth_]; } void update_ait(typename GT::const_adjacency_iterator qa, typename GU::const_adjacency_iterator ta) { query_target_ait_[depth_] = { qa, ta }; - ta.end() ? --depth_ : ++depth_; } ArrayXi &r_inout_cnt() { return label_tmp1_; } @@ -366,14 +367,21 @@ class VF2pp { bool next(const NodeMatch &node_match, const EdgeMatch &edge_match) { while (depth_ >= 0) { if (depth_ == query().size()) { - --depth_; - if (map_remaining_edges(edge_match)) { #ifdef NURI_DEBUG first_ = false; #endif + ++depth_; return true; } + + --depth_; + } else if (depth_ > query().size()) { +#ifdef NURI_DEBUG + ABSL_DCHECK(!first_); +#endif + // previous call to next() returned true + clear_stale_maps(node_match, edge_match); } const auto qn = curr_node(); @@ -434,6 +442,7 @@ class VF2pp { } update_ait(qa, ta); + ta.end() ? --depth_ : ++depth_; } return false; @@ -599,8 +608,9 @@ class VF2pp { return node_match(qn, tn) && cut_by_labels(qn, tn); } - bool is_stale(const typename GT::ConstEdgeRef qe, - const typename GU::ConstEdgeRef te) { + constexpr bool is_stale(const typename GT::ConstEdgeRef qe, + const typename GU::ConstEdgeRef te) const { +#ifdef NURI_DEBUG const int curr_src = node_map_[qe.src().id()], curr_dst = node_map_[qe.dst().id()]; @@ -608,18 +618,23 @@ class VF2pp { (curr_src != te.src().id() || curr_dst != te.dst().id()) && (curr_src != te.dst().id() || curr_dst != te.src().id()); -#ifdef NURI_DEBUG ABSL_DCHECK(!first_ || !stale) << qe.id() << " " << te.id(); -#endif return stale; +#else + static_cast(*this); + static_cast(qe); + static_cast(te); + + return false; +#endif } template bool map_remaining_edges(const EdgeMatch &edge_match) { for (auto qe: query().edges()) { - if (edge_map_[qe.id()] >= 0 - && !is_stale(qe, target().edge(edge_map_[qe.id()]))) { + if (edge_map_[qe.id()] >= 0) { + ABSL_DCHECK(!is_stale(qe, target().edge(edge_map_[qe.id()]))); continue; } @@ -636,6 +651,52 @@ class VF2pp { return true; } + template + void clear_stale_maps(const NodeMatch &node_match, + const EdgeMatch &edge_match) { + // clear first, some edges are mapped after BFS search + edge_map_.setConstant(-1); + + // Starting from the deepest stack frame, find first stale node/edge match. + // Must be started from the deepest frame because the following frames + // depend on the mapping of the current frame. This will stop at first + // iteration if non-overlapping match was requested. + // + // Last entry will be popped off after this function returns, so must be + // excluded in this function (< query().size() - 1) + int i = 0; + for (; i < query().size() - 1; ++i) { + int ti = mapped_node(i); + ABSL_DCHECK_GE(ti, 0); + // check for stale nodes, if non-overlapping node match was requested + if (!node_match(curr_node(i), target().node(ti))) + break; + + auto [qa, ta] = query_target_ait_[i]; + if (qa.end()) + continue; + + // check for stale edges, if non-overlapping edge match was requested + if (!edge_match(query().edge(qa->eid()), target().edge(ta->eid()))) + break; + + ABSL_DCHECK(!ta.end()); + edge_map_[qa->eid()] = ta->eid(); + } + + // Simulate stack pop when any of node/edge match fails + // Same here, last edge will be popped off so must be excluded (depth_ > i) + for (depth_ = query().size() - 1; depth_ > i; --depth_) { + auto qn = curr_node(); + int ti = mapped_node(); + auto [qa, _] = query_target_ait(); + + ABSL_DCHECK_GE(ti, 0); + sub_pair(qn.id(), ti, qa.end() ? -1 : qa->eid()); + update_ait(query().adj_end(0), target().adj_end(0)); + } + } + const GT *query_; const GU *target_; From 7811f4df7e9dbd53bf2f108adab12d91e0563c48 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 13 Dec 2024 14:22:25 +0900 Subject: [PATCH 27/27] test(core/graph/vf2++): test for non-overlapping matchers --- test/core/graph_vf2pp_test.cpp | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/core/graph_vf2pp_test.cpp b/test/core/graph_vf2pp_test.cpp index 594c876b..b79d29b3 100644 --- a/test/core/graph_vf2pp_test.cpp +++ b/test/core/graph_vf2pp_test.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -723,5 +724,61 @@ TEST(VF2ppPetersenSubgraphSimpleMatchTest, MultiLabel) { EXPECT_EQ(emap[i], expected_map[i]) << i; } } + +TEST(VF2ppPetersenSubgraphSingleLabelTest, NodeNoOverlap) { + GT petersen = lemon_petersen(); + auto p2 = subgraph_from_edges(petersen, { 1 }); + + ArrayXi qlbl = ArrayXi::Zero(2), tlbl = ArrayXi::Zero(10); + ArrayXi node_cnt = ArrayXi::Zero(10); + + auto vf2 = make_vf2pp(p2, petersen, qlbl, tlbl); + while (vf2.next([&](auto, auto tn) { return node_cnt[tn.id()] == 0; }, + dummy_match)) { + node_cnt(vf2.node_map()) += 1; + } + + for (auto cnt: node_cnt) + EXPECT_EQ(cnt, 1); +} + +TEST(VF2ppPetersenSubgraphSingleLabelTest, EdgeNoOverlap) { + GT petersen = lemon_petersen(); + auto p4 = subgraph_from_edges(petersen, { 1, 7, 12 }); + + ArrayXi qlbl = ArrayXi::Zero(4), tlbl = ArrayXi::Zero(10); + ArrayXi edge_cnt = ArrayXi::Zero(15); + + auto vf2 = make_vf2pp(p4, petersen, qlbl, tlbl); + while (vf2.next(dummy_match, + [&](auto, auto te) { return edge_cnt[te.id()] < 2; })) { + edge_cnt(vf2.edge_map()) += 1; + } + + for (auto edge: petersen.edges()) + EXPECT_EQ(edge_cnt[edge.id()], 2) << edge.id(); +} + +TEST(VF2ppPetersenSubgraphSingleLabelTest, NodeEdgeNoOverlap) { + GT petersen = lemon_petersen(); + auto p4 = subgraph_from_edges(petersen, { 1, 7, 12 }); + + ArrayXi qlbl = ArrayXi::Zero(4), tlbl = ArrayXi::Zero(10); + ArrayXi node_cnt = ArrayXi::Zero(10), edge_cnt = ArrayXi::Zero(15); + + auto vf2 = make_vf2pp(p4, petersen, qlbl, tlbl); + while (vf2.next( // + [&](auto, auto tn) { return node_cnt[tn.id()] < tn.degree() * 2; }, + [&](auto, auto te) { return edge_cnt[te.id()] < 2; })) { + node_cnt(vf2.node_map()) += 1; + edge_cnt(vf2.edge_map()) += 1; + } + + for (auto node: petersen) + EXPECT_LE(node_cnt[node.id()], node.degree() * 2) << node.id(); + + for (auto edge: petersen.edges()) + EXPECT_EQ(edge_cnt[edge.id()], 2) << edge.id(); +} } // namespace } // namespace nuri