Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/mcs/fmcs_cuda/fmcs_policy.cuh
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <utility>
#include <vector>

#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<std::size_t>(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<std::pair<int, int>> enumerateBonds(const Graph& g) {
std::vector<std::pair<int, int>> out;
out.reserve(static_cast<std::size_t>(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<int>(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<std::size_t>(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<int>(query.vertexLabels.size())) ? query.vertexLabels[i] : 0;
for (int j = 0; j < nT; ++j) {
const uint16_t lt = (j < static_cast<int>(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<int>(qBonds.size()), static_cast<int>(tBonds.size()));
if (!matchEdgeLabels) {
fillAllCompatible(out, static_cast<int>(qBonds.size()), static_cast<int>(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<int>(i), static_cast<int>(j));
}
}
}
}
};

} // namespace fmcs
} // namespace mcs

#endif // FMCS_CUDA_FMCS_POLICY_CUH
34 changes: 34 additions & 0 deletions src/mcs/labeled_graph.h
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <vector>

#include "src/mcs/mcs_common/mcs_types.cuh"

namespace mcs {

struct LabeledGraph {
Graph graph;
std::vector<std::uint16_t> vertexLabels;
std::vector<std::uint16_t> edgeLabels;
};

} // namespace mcs

#endif // NVMOLKIT_MCS_LABELED_GRAPH_H
4 changes: 4 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
155 changes: 155 additions & 0 deletions tests/test_fmcs_policy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include <gtest/gtest.h>

#include <cstdint>
#include <utility>
#include <vector>

#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<std::uint16_t>& 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<std::pair<int, int>>{
{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
Loading