diff --git a/src/mcs/fmcs_cuda/fmcs_policy.cuh b/src/mcs/fmcs_cuda/fmcs_policy.cuh new file mode 100644 index 00000000..4563780d --- /dev/null +++ b/src/mcs/fmcs_cuda/fmcs_policy.cuh @@ -0,0 +1,156 @@ +// 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 "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 { + +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 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, + 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; + } + + 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 = 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 = edgeLabel(target, x, 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..73b5601c --- /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 "src/mcs/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 f4647a0c..08676161 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_fmcs_match test_fmcs_match.cu) target_include_directories(test_fmcs_match PRIVATE ${CMAKE_SOURCE_DIR}/src/mcs) target_link_libraries(test_fmcs_match PRIVATE mcs_fmcs_foundations @@ -434,6 +437,7 @@ set(TEST_LIST test_fmcs_foundations test_fmcs_grow test_fmcs_match + 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..5f5ca91f --- /dev/null +++ b/tests/test_fmcs_policy.cpp @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "src/mcs/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, 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); + + 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