From e746ee8b9e1622435cddcbdef33c97fb861d6967 Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Fri, 24 Jul 2026 10:23:27 -0400 Subject: [PATCH 1/4] Add tested fMCS compatibility policies --- src/mcs/fmcs_cuda/fmcs_policy.cuh | 153 ++++++++++++++++++++++++++++++ src/mcs/labeled_graph.h | 34 +++++++ tests/CMakeLists.txt | 4 + tests/test_fmcs_policy.cpp | 108 +++++++++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 src/mcs/fmcs_cuda/fmcs_policy.cuh create mode 100644 src/mcs/labeled_graph.h create mode 100644 tests/test_fmcs_policy.cpp diff --git a/src/mcs/fmcs_cuda/fmcs_policy.cuh b/src/mcs/fmcs_cuda/fmcs_policy.cuh new file mode 100644 index 00000000..50644244 --- /dev/null +++ b/src/mcs/fmcs_cuda/fmcs_policy.cuh @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FMCS_CUDA_FMCS_POLICY_CUH +#define FMCS_CUDA_FMCS_POLICY_CUH + +#include +#include +#include +#include + +#include "fmcs_cuda/fmcs_match_tables.cuh" +#include "labeled_graph.h" +#include "mcs_common/mcs_types.cuh" + +namespace mcs { +namespace fmcs { + +inline void fillAllCompatible(MatchTableHost& out, int nRows, int nCols) { + out.resize(nRows, nCols); + for (int i = 0; i < nRows; ++i) { + for (int j = 0; j < nCols; ++j) { + out.setBit(i, j); + } + // Trailing bits past nCols must stay zero so row popcounts do not + // overcount. + if (out.wordsPerRow > 0) { + const int tailBits = out.wordsPerRow * 32 - nCols; + if (tailBits > 0) { + const uint32_t mask = (tailBits == 32) ? 0u : (0xFFFFFFFFu >> tailBits); + out.data[static_cast(i) * out.wordsPerRow + out.wordsPerRow - 1] &= mask; + } + } + } +} + +/// Deterministic bond enumeration for a CSR @ref Graph: each undirected +/// edge (u, v) with u < v gets a bondIdx in [0, numEdges) by first- +/// appearance order in colIndices. @ref Graph carries no explicit bond +/// identifiers, so this function defines the ordering the fMCS code uses +/// everywhere it needs to index bonds. +inline std::vector> enumerateBonds(const Graph& g) { + std::vector> out; + out.reserve(static_cast(g.numEdges)); + for (int u = 0; u < g.numVertices; ++u) { + const std::size_t begin = g.rowOffsets[u]; + const std::size_t end = g.rowOffsets[u + 1]; + for (std::size_t k = begin; k < end; ++k) { + const int v = static_cast(g.colIndices[k]); + if (u < v) + out.emplace_back(u, v); + } + } + return out; +} + +/// Unlabeled policy. Every atom and every bond is compatible; real +/// topology and connectivity enforcement happens in the device-side match +/// walk, not in these tables. +struct NullFMCSPolicy { + using graph_type = Graph; + + static void buildAtomMatchTable(const Graph& query, + const Graph& target, + MatchTableHost& out, + bool /*matchVertexLabels*/) { + fillAllCompatible(out, query.numVertices, target.numVertices); + } + + static void buildBondMatchTable(const Graph& query, + const Graph& target, + MatchTableHost& out, + bool /*matchEdgeLabels*/) { + fillAllCompatible(out, query.numEdges, target.numEdges); + } +}; + +/// Exact uint16_t vertex- and edge-label matching on labeled graphs; +/// 0 in @c edgeLabels means "no edge". Bond indices follow +/// @ref enumerateBonds of the underlying topology. +struct LabeledFMCSPolicy { + using graph_type = LabeledGraph; + + static void buildAtomMatchTable(const LabeledGraph& query, + const LabeledGraph& target, + MatchTableHost& out, + bool matchVertexLabels) { + const int nQ = query.graph.numVertices; + const int nT = target.graph.numVertices; + if (!matchVertexLabels) { + fillAllCompatible(out, nQ, nT); + return; + } + + out.resize(nQ, nT); + for (int i = 0; i < nQ; ++i) { + const uint16_t lq = (i < static_cast(query.vertexLabels.size())) ? query.vertexLabels[i] : 0; + for (int j = 0; j < nT; ++j) { + const uint16_t lt = (j < static_cast(target.vertexLabels.size())) ? target.vertexLabels[j] : 0; + if (lq == lt) + out.setBit(i, j); + } + } + } + + static void buildBondMatchTable(const LabeledGraph& query, + const LabeledGraph& target, + MatchTableHost& out, + bool matchEdgeLabels) { + const auto qBonds = enumerateBonds(query.graph); + const auto tBonds = enumerateBonds(target.graph); + out.resize(static_cast(qBonds.size()), static_cast(tBonds.size())); + if (!matchEdgeLabels) { + fillAllCompatible(out, static_cast(qBonds.size()), static_cast(tBonds.size())); + return; + } + + const int nQ = query.graph.numVertices; + const int nT = target.graph.numVertices; + for (std::size_t i = 0; i < qBonds.size(); ++i) { + const int u = qBonds[i].first; + const int v = qBonds[i].second; + const uint16_t lq = query.edgeLabels[static_cast(u) * nQ + v]; + if (lq == 0) + continue; + for (std::size_t j = 0; j < tBonds.size(); ++j) { + const int x = tBonds[j].first; + const int y = tBonds[j].second; + const uint16_t lt = target.edgeLabels[static_cast(x) * nT + y]; + if (lt != 0 && lq == lt) { + out.setBit(static_cast(i), static_cast(j)); + } + } + } + } +}; + +} // namespace fmcs +} // namespace mcs + +#endif // FMCS_CUDA_FMCS_POLICY_CUH diff --git a/src/mcs/labeled_graph.h b/src/mcs/labeled_graph.h new file mode 100644 index 00000000..b4d1b646 --- /dev/null +++ b/src/mcs/labeled_graph.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NVMOLKIT_MCS_LABELED_GRAPH_H +#define NVMOLKIT_MCS_LABELED_GRAPH_H + +#include +#include + +#include "mcs_common/mcs_types.cuh" + +namespace mcs { + +struct LabeledGraph { + Graph graph; + std::vector vertexLabels; + std::vector edgeLabels; +}; + +} // namespace mcs + +#endif // NVMOLKIT_MCS_LABELED_GRAPH_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f4480ba1..eb6f8d02 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -81,6 +81,9 @@ target_compile_options(test_fmcs_grow add_executable(test_fmcs_foundations test_fmcs_foundations.cu) target_link_libraries(test_fmcs_foundations PRIVATE mcs_fmcs_foundations) +add_executable(test_fmcs_policy test_fmcs_policy.cpp) +target_link_libraries(test_fmcs_policy PRIVATE mcs_fmcs_foundations) + add_executable(test_openmp_helpers test_openmp_helpers.cpp) target_link_libraries(test_openmp_helpers PRIVATE openmp_helpers OpenMP::OpenMP_CXX) @@ -428,6 +431,7 @@ set(TEST_LIST test_fmcs_primitives test_fmcs_foundations test_fmcs_grow + test_fmcs_policy test_work_splitting test_fire_minimizer test_fire_minimizer_permol) diff --git a/tests/test_fmcs_policy.cpp b/tests/test_fmcs_policy.cpp new file mode 100644 index 00000000..7c07b57c --- /dev/null +++ b/tests/test_fmcs_policy.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "fmcs_cuda/fmcs_policy.cuh" + +namespace { + +using mcs::LabeledGraph; +using mcs::fmcs::LabeledFMCSPolicy; +using mcs::fmcs::MatchTableHost; +using mcs::fmcs::NullFMCSPolicy; + +LabeledGraph labeledPath(const std::vector& vertices, + std::uint16_t firstBond, + std::uint16_t secondBond) { + LabeledGraph result; + result.graph = mcs::buildGraphFromEdges(3, {{0, 1}, {1, 2}}); + result.vertexLabels = vertices; + result.edgeLabels.assign(9, 0); + result.edgeLabels[1] = result.edgeLabels[3] = firstBond; + result.edgeLabels[5] = result.edgeLabels[7] = secondBond; + return result; +} + +TEST(FMCSPolicy, EnumeratesUndirectedBondsDeterministically) { + const auto graph = mcs::buildGraphFromEdges(4, {{2, 3}, {0, 2}, {1, 2}}); + EXPECT_EQ(mcs::fmcs::enumerateBonds(graph), + (std::vector>{{0, 2}, {1, 2}, {2, 3}})); +} + +TEST(FMCSPolicy, NullPolicyMakesEveryPairCompatible) { + const auto query = mcs::buildGraphFromEdges(2, {{0, 1}}); + const auto target = mcs::buildGraphFromEdges(3, {{0, 1}, {1, 2}}); + + MatchTableHost atoms; + MatchTableHost bonds; + NullFMCSPolicy::buildAtomMatchTable(query, target, atoms, true); + NullFMCSPolicy::buildBondMatchTable(query, target, bonds, true); + + for (int q = 0; q < atoms.nRows; ++q) + for (int t = 0; t < atoms.nCols; ++t) + EXPECT_TRUE(atoms.testBit(q, t)); + for (int q = 0; q < bonds.nRows; ++q) + for (int t = 0; t < bonds.nCols; ++t) + EXPECT_TRUE(bonds.testBit(q, t)); +} + +TEST(FMCSPolicy, VertexLabelsRestrictCompatibility) { + const auto query = labeledPath({6, 6, 8}, 1, 2); + const auto target = labeledPath({8, 6, 7}, 1, 2); + + MatchTableHost table; + LabeledFMCSPolicy::buildAtomMatchTable(query, target, table, true); + + EXPECT_FALSE(table.testBit(0, 0)); + EXPECT_TRUE(table.testBit(0, 1)); + EXPECT_TRUE(table.testBit(2, 0)); + EXPECT_FALSE(table.testBit(2, 2)); +} + +TEST(FMCSPolicy, MissingVertexLabelsBehaveAsZero) { + auto query = labeledPath({}, 1, 2); + auto target = labeledPath({0, 7, 0}, 1, 2); + MatchTableHost table; + + LabeledFMCSPolicy::buildAtomMatchTable(query, target, table, true); + EXPECT_TRUE(table.testBit(0, 0)); + EXPECT_FALSE(table.testBit(0, 1)); + EXPECT_TRUE(table.testBit(2, 2)); +} + +TEST(FMCSPolicy, BondLabelsRestrictCompatibility) { + const auto query = labeledPath({6, 6, 8}, 1, 2); + const auto target = labeledPath({6, 6, 8}, 2, 1); + + MatchTableHost table; + LabeledFMCSPolicy::buildBondMatchTable(query, target, table, true); + + EXPECT_FALSE(table.testBit(0, 0)); + EXPECT_TRUE(table.testBit(0, 1)); + EXPECT_TRUE(table.testBit(1, 0)); + EXPECT_FALSE(table.testBit(1, 1)); +} + +TEST(FMCSPolicy, LabelMatchingCanBeDisabled) { + const auto query = labeledPath({1, 2, 3}, 4, 5); + const auto target = labeledPath({6, 7, 8}, 9, 10); + + MatchTableHost atoms; + MatchTableHost bonds; + LabeledFMCSPolicy::buildAtomMatchTable(query, target, atoms, false); + LabeledFMCSPolicy::buildBondMatchTable(query, target, bonds, false); + + for (int q = 0; q < atoms.nRows; ++q) + for (int t = 0; t < atoms.nCols; ++t) + EXPECT_TRUE(atoms.testBit(q, t)); + for (int q = 0; q < bonds.nRows; ++q) + for (int t = 0; t < bonds.nCols; ++t) + EXPECT_TRUE(bonds.testBit(q, t)); +} + +} // namespace From efccb8281376334898dfc3b068c7f6b6cbd3951f Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Fri, 24 Jul 2026 13:46:30 -0400 Subject: [PATCH 2/4] Use project-rooted includes in fMCS policies --- src/mcs/fmcs_cuda/fmcs_policy.cuh | 6 +++--- src/mcs/labeled_graph.h | 2 +- tests/test_fmcs_policy.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mcs/fmcs_cuda/fmcs_policy.cuh b/src/mcs/fmcs_cuda/fmcs_policy.cuh index 50644244..ca6ae3ed 100644 --- a/src/mcs/fmcs_cuda/fmcs_policy.cuh +++ b/src/mcs/fmcs_cuda/fmcs_policy.cuh @@ -21,9 +21,9 @@ #include #include -#include "fmcs_cuda/fmcs_match_tables.cuh" -#include "labeled_graph.h" -#include "mcs_common/mcs_types.cuh" +#include "src/mcs/fmcs_cuda/fmcs_match_tables.cuh" +#include "src/mcs/labeled_graph.h" +#include "src/mcs/mcs_common/mcs_types.cuh" namespace mcs { namespace fmcs { diff --git a/src/mcs/labeled_graph.h b/src/mcs/labeled_graph.h index b4d1b646..73b5601c 100644 --- a/src/mcs/labeled_graph.h +++ b/src/mcs/labeled_graph.h @@ -19,7 +19,7 @@ #include #include -#include "mcs_common/mcs_types.cuh" +#include "src/mcs/mcs_common/mcs_types.cuh" namespace mcs { diff --git a/tests/test_fmcs_policy.cpp b/tests/test_fmcs_policy.cpp index 7c07b57c..9fe8a382 100644 --- a/tests/test_fmcs_policy.cpp +++ b/tests/test_fmcs_policy.cpp @@ -7,7 +7,7 @@ #include #include -#include "fmcs_cuda/fmcs_policy.cuh" +#include "src/mcs/fmcs_cuda/fmcs_policy.cuh" namespace { From 3c08e34826eaa5d463c73ccc7f172e9d7ec9034e Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Tue, 28 Jul 2026 13:30:19 -0400 Subject: [PATCH 3/4] Formatting --- tests/test_fmcs_policy.cpp | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/test_fmcs_policy.cpp b/tests/test_fmcs_policy.cpp index 9fe8a382..b32f59b3 100644 --- a/tests/test_fmcs_policy.cpp +++ b/tests/test_fmcs_policy.cpp @@ -17,10 +17,14 @@ using mcs::fmcs::MatchTableHost; using mcs::fmcs::NullFMCSPolicy; LabeledGraph labeledPath(const std::vector& vertices, - std::uint16_t firstBond, - std::uint16_t secondBond) { + std::uint16_t firstBond, + std::uint16_t secondBond) { LabeledGraph result; - result.graph = mcs::buildGraphFromEdges(3, {{0, 1}, {1, 2}}); + result.graph = mcs::buildGraphFromEdges(3, + { + {0, 1}, + {1, 2} + }); result.vertexLabels = vertices; result.edgeLabels.assign(9, 0); result.edgeLabels[1] = result.edgeLabels[3] = firstBond; @@ -29,14 +33,30 @@ LabeledGraph labeledPath(const std::vector& vertices, } TEST(FMCSPolicy, EnumeratesUndirectedBondsDeterministically) { - const auto graph = mcs::buildGraphFromEdges(4, {{2, 3}, {0, 2}, {1, 2}}); + const auto graph = mcs::buildGraphFromEdges(4, + { + {2, 3}, + {0, 2}, + {1, 2} + }); EXPECT_EQ(mcs::fmcs::enumerateBonds(graph), - (std::vector>{{0, 2}, {1, 2}, {2, 3}})); + (std::vector>{ + {0, 2}, + {1, 2}, + {2, 3} + })); } TEST(FMCSPolicy, NullPolicyMakesEveryPairCompatible) { - const auto query = mcs::buildGraphFromEdges(2, {{0, 1}}); - const auto target = mcs::buildGraphFromEdges(3, {{0, 1}, {1, 2}}); + const auto query = mcs::buildGraphFromEdges(2, + { + {0, 1} + }); + const auto target = mcs::buildGraphFromEdges(3, + { + {0, 1}, + {1, 2} + }); MatchTableHost atoms; MatchTableHost bonds; @@ -65,8 +85,8 @@ TEST(FMCSPolicy, VertexLabelsRestrictCompatibility) { } TEST(FMCSPolicy, MissingVertexLabelsBehaveAsZero) { - auto query = labeledPath({}, 1, 2); - auto target = labeledPath({0, 7, 0}, 1, 2); + auto query = labeledPath({}, 1, 2); + auto target = labeledPath({0, 7, 0}, 1, 2); MatchTableHost table; LabeledFMCSPolicy::buildAtomMatchTable(query, target, table, true); From 56a5995f22c302592258d507963354f5cb85335d Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Tue, 28 Jul 2026 13:46:07 -0400 Subject: [PATCH 4/4] Guard missing fMCS edge labels --- src/mcs/fmcs_cuda/fmcs_policy.cuh | 11 +++++++---- tests/test_fmcs_policy.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/mcs/fmcs_cuda/fmcs_policy.cuh b/src/mcs/fmcs_cuda/fmcs_policy.cuh index ca6ae3ed..4563780d 100644 --- a/src/mcs/fmcs_cuda/fmcs_policy.cuh +++ b/src/mcs/fmcs_cuda/fmcs_policy.cuh @@ -93,6 +93,11 @@ struct NullFMCSPolicy { struct LabeledFMCSPolicy { using graph_type = LabeledGraph; + static uint16_t edgeLabel(const LabeledGraph& labeledGraph, int u, int v) { + const std::size_t index = static_cast(u) * labeledGraph.graph.numVertices + v; + return index < labeledGraph.edgeLabels.size() ? labeledGraph.edgeLabels[index] : 0; + } + static void buildAtomMatchTable(const LabeledGraph& query, const LabeledGraph& target, MatchTableHost& out, @@ -127,18 +132,16 @@ struct LabeledFMCSPolicy { return; } - const int nQ = query.graph.numVertices; - const int nT = target.graph.numVertices; for (std::size_t i = 0; i < qBonds.size(); ++i) { const int u = qBonds[i].first; const int v = qBonds[i].second; - const uint16_t lq = query.edgeLabels[static_cast(u) * nQ + v]; + const uint16_t lq = edgeLabel(query, u, v); if (lq == 0) continue; for (std::size_t j = 0; j < tBonds.size(); ++j) { const int x = tBonds[j].first; const int y = tBonds[j].second; - const uint16_t lt = target.edgeLabels[static_cast(x) * nT + y]; + const uint16_t lt = edgeLabel(target, x, y); if (lt != 0 && lq == lt) { out.setBit(static_cast(i), static_cast(j)); } diff --git a/tests/test_fmcs_policy.cpp b/tests/test_fmcs_policy.cpp index b32f59b3..5f5ca91f 100644 --- a/tests/test_fmcs_policy.cpp +++ b/tests/test_fmcs_policy.cpp @@ -108,6 +108,33 @@ TEST(FMCSPolicy, BondLabelsRestrictCompatibility) { EXPECT_FALSE(table.testBit(1, 1)); } +TEST(FMCSPolicy, MissingQueryEdgeLabelsBehaveAsZero) { + auto query = labeledPath({6, 6, 8}, 1, 2); + const auto target = labeledPath({6, 6, 8}, 1, 2); + query.edgeLabels.clear(); + + MatchTableHost table; + LabeledFMCSPolicy::buildBondMatchTable(query, target, table, true); + + for (int q = 0; q < table.nRows; ++q) + for (int t = 0; t < table.nCols; ++t) + EXPECT_FALSE(table.testBit(q, t)); +} + +TEST(FMCSPolicy, UndersizedTargetEdgeLabelsBehaveAsZero) { + const auto query = labeledPath({6, 6, 8}, 1, 2); + auto target = labeledPath({6, 6, 8}, 1, 2); + target.edgeLabels.resize(2); + + MatchTableHost table; + LabeledFMCSPolicy::buildBondMatchTable(query, target, table, true); + + EXPECT_TRUE(table.testBit(0, 0)); + EXPECT_FALSE(table.testBit(0, 1)); + EXPECT_FALSE(table.testBit(1, 0)); + EXPECT_FALSE(table.testBit(1, 1)); +} + TEST(FMCSPolicy, LabelMatchingCanBeDisabled) { const auto query = labeledPath({1, 2, 3}, 4, 5); const auto target = labeledPath({6, 7, 8}, 9, 10);