From 9ee52792cefe260a39aa48e08e9c786c04f53e4a Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Fri, 9 Oct 2026 12:15:21 -0400 Subject: [PATCH 1/2] Support RDKit 2026.09 torsion parameters and owned embedding state Accept both legacy cosine parameter pairs and the new torsion variant, with an explicit error for unsupported Gaussian parameters. Clone prepared embedding arguments, including owned internal coordinates, for each conformer attempt. Add regression coverage for both parameter forms and independent owned state. Validated with a local Release Conda build against RDKit 2026.09.1, CUDA 13.3, and an RTX 5080: GPU smoke and all four new regression cases pass; Python has 1032 passed, 85 skipped, and 20 long cases deselected. The C++ suite still has 12 failures involving reference expectations and ring-count limits. The Conda environment also required the missing upstream BoundsMatrixBuilderDetails.h header; that environment workaround is not included in this patch. --- .../dist_geom_flattened_builder.cpp | 8 ++- rdkit_extensions/torsion_compat.h | 33 +++++++++++ src/embedder_utils.cpp | 42 ++++++++++++++ src/embedder_utils.h | 5 +- src/etkdg.cpp | 2 +- tests/CMakeLists.txt | 2 +- tests/test_distgeom_kernels.cu | 5 +- tests/test_flattened_builder.cu | 55 +++++++++++++++++++ 8 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 rdkit_extensions/torsion_compat.h diff --git a/rdkit_extensions/dist_geom_flattened_builder.cpp b/rdkit_extensions/dist_geom_flattened_builder.cpp index 44605456..9e273171 100644 --- a/rdkit_extensions/dist_geom_flattened_builder.cpp +++ b/rdkit_extensions/dist_geom_flattened_builder.cpp @@ -9,6 +9,7 @@ #include #include +#include "rdkit_extensions/torsion_compat.h" #include "versions.h" // No clang-tidy for 1:1 RDKit ports. @@ -157,8 +158,9 @@ void addExperimentalTorsionTerms(nvMolKit::DistGeom::Energy3DForceContribsHost& contribs.experimentalTorsionTerms.idx4.push_back(atomIdx4); // Add force constants and signs (6 values per torsion) - const auto& signs = etkdgDetails.expTorsionAngles[torsionIdx].first; - const auto& forceConstants = etkdgDetails.expTorsionAngles[torsionIdx].second; + const auto& torsionParameters = detail::getCosineTorsionParameters(etkdgDetails.expTorsionAngles[torsionIdx]); + const auto& signs = torsionParameters.first; + const auto& forceConstants = torsionParameters.second; // Ensure we have 6 values for each torsion for (int term = 0; term < TORSION_TERMS_PER_ANGLE; ++term) { @@ -541,4 +543,4 @@ nvMolKit::DistGeom::Energy3DForceContribsHost construct3DForceFieldContribs( // NOLINTEND } // namespace DistGeom -} // namespace nvMolKit \ No newline at end of file +} // namespace nvMolKit diff --git a/rdkit_extensions/torsion_compat.h b/rdkit_extensions/torsion_compat.h new file mode 100644 index 00000000..f8e2e994 --- /dev/null +++ b/rdkit_extensions/torsion_compat.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#ifndef NVMOLKIT_TORSION_COMPAT_H +#define NVMOLKIT_TORSION_COMPAT_H + +#include +#include +#include +#include + +namespace nvMolKit::detail { + +using CosineTorsionParameters = std::pair, std::vector>; + +inline const CosineTorsionParameters& getCosineTorsionParameters(const CosineTorsionParameters& parameters) { + return parameters; +} + +// RDKit 2026.09 wraps the existing cosine parameters and new Gaussian +// parameters in a variant. nvMolKit supports the cosine form (ETversion 1/2). +template +const CosineTorsionParameters& getCosineTorsionParameters(const std::variant& parameters) { + const auto* cosine = std::get_if(¶meters); + if (cosine == nullptr) { + throw std::invalid_argument("Gaussian experimental torsion parameters are not supported by nvMolKit"); + } + return *cosine; +} + +} // namespace nvMolKit::detail + +#endif // NVMOLKIT_TORSION_COMPAT_H diff --git a/src/embedder_utils.cpp b/src/embedder_utils.cpp index 750c7cf5..98760999 100644 --- a/src/embedder_utils.cpp +++ b/src/embedder_utils.cpp @@ -67,6 +67,48 @@ constexpr double FOURTH_DIM_MINIMIZE_FOURTH_DIM_WEIGHT = } // namespace namespace nvMolKit { +namespace detail { + +EmbedArgs cloneEmbedArgs(const EmbedArgs& source) { +#if RDKIT_VERSION_MAJOR > 2026 || (RDKIT_VERSION_MAJOR == 2026 && RDKIT_VERSION_MINOR >= 9) + EmbedArgs result; + result.dim = source.dim; + result.mmat = source.mmat; + result.chiralCenters = source.chiralCenters; + result.tetrahedralCarbons = source.tetrahedralCarbons; + result.doubleBondEnds = source.doubleBondEnds; + result.stereoDoubleBonds = source.stereoDoubleBonds; + result.posVec = source.posVec; + result.stage = source.stage; + + const auto& original = source.etkdgDetails; + auto& copied = result.etkdgDetails; + copied.torsionParamKind = original.torsionParamKind; + copied.expTorsionAtoms = original.expTorsionAtoms; + copied.expTorsionAngles = original.expTorsionAngles; + copied.torsionIdx = original.torsionIdx; + copied.improperAtoms = original.improperAtoms; + copied.bonds = original.bonds; + copied.angles = original.angles; + copied.atomNums = original.atomNums; + copied.boundsMatForceScaling = original.boundsMatForceScaling; + copied.constrainedAtoms = original.constrainedAtoms; + copied.distMat = original.distMat; + copied.forceConsts = original.forceConsts; + copied.path14Configs = original.path14Configs; + copied.phiToEnergy = original.phiToEnergy; + copied.phiToGrad = original.phiToGrad; + if (original.internalCoords != nullptr) { + copied.internalCoords = std::make_unique(*original.internalCoords); + } + return result; +#else + return source; +#endif +} + +} // namespace detail + namespace DGeomHelpers { namespace EmbeddingOps { bool generateInitialCoords(const std::vector>& positions, diff --git a/src/embedder_utils.h b/src/embedder_utils.h index f8e9f340..399f0803 100644 --- a/src/embedder_utils.h +++ b/src/embedder_utils.h @@ -70,13 +70,16 @@ struct EmbedArgs { ::DistGeom::BoundsMatPtr mmat = nullptr; ::DistGeom::VECT_CHIRALSET chiralCenters; ::DistGeom::VECT_CHIRALSET tetrahedralCarbons; - ForceFields::CrystalFF::CrystalFFDetails etkdgDetails; + ForceFields::CrystalFF::CrystalFFDetails etkdgDetails{}; std::vector> doubleBondEnds; std::vector, int>> stereoDoubleBonds; std::vector posVec; MinimizeStage stage = MinimizeStage::FirstMinimize; }; +//! Copy prepared arguments for one conformer attempt, including owned RDKit state. +EmbedArgs cloneEmbedArgs(const EmbedArgs& source); + } // namespace detail namespace DGeomHelpers { diff --git a/src/etkdg.cpp b/src/etkdg.cpp index d7623307..e1b178df 100644 --- a/src/etkdg.cpp +++ b/src/etkdg.cpp @@ -328,7 +328,7 @@ std::optional embedMolecules(const std::vector for (const int molId : molIds) { // Use the original unique molecules and their prepared eargs batchMolsWithConfs.push_back(sortedMols[molId]); - batchEargs.push_back(eargs[molId]); + batchEargs.push_back(detail::cloneEmbedArgs(eargs[molId])); coordinateDimensions.push_back(eargs[molId].chiralCenters.empty() ? 3 : 4); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 66e73d9b..60db6f80 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -130,7 +130,7 @@ add_executable(test_flattened_builder test_flattened_builder.cu) target_link_libraries( test_flattened_builder PRIVATE ${RDKit_LIBS} rdkit_mmff_flattened rdkit_dist_geom_flattened - device_vector test_utils embedder_utils) + device_vector test_utils embedder_utils nvmolkit_versions) add_executable(test_mmff test_mmff.cu) target_link_libraries( diff --git a/tests/test_distgeom_kernels.cu b/tests/test_distgeom_kernels.cu index 6c449edd..b224d080 100644 --- a/tests/test_distgeom_kernels.cu +++ b/tests/test_distgeom_kernels.cu @@ -30,6 +30,7 @@ #include "rdkit_extensions/bounds_matrix.h" #include "rdkit_extensions/dist_geom_flattened_builder.h" +#include "rdkit_extensions/torsion_compat.h" #include "src/embedder_utils.h" #include "src/forcefields/dist_geom.h" #include "src/forcefields/dist_geom_kernels.h" @@ -120,8 +121,8 @@ void addExperimentalTorsionTerms(ForceFields::ForceField* } else { atomPairs[l * numAtoms + i] = true; } - torsionContribs - ->addContrib(i, j, k, l, etkdgDetails.expTorsionAngles[t].second, etkdgDetails.expTorsionAngles[t].first); + const auto& torsionParameters = nvMolKit::detail::getCosineTorsionParameters(etkdgDetails.expTorsionAngles[t]); + torsionContribs->addContrib(i, j, k, l, torsionParameters.second, torsionParameters.first); } if (!torsionContribs->empty()) { ff->contribs().push_back(std::move(torsionContribs)); diff --git a/tests/test_flattened_builder.cu b/tests/test_flattened_builder.cu index 6641be2e..87fa001d 100644 --- a/tests/test_flattened_builder.cu +++ b/tests/test_flattened_builder.cu @@ -21,11 +21,66 @@ #include #include +#include #include "rdkit_extensions/dist_geom_flattened_builder.h" #include "rdkit_extensions/mmff_flattened_builder.h" +#include "rdkit_extensions/torsion_compat.h" #include "src/embedder_utils.h" #include "tests/test_utils.h" +#include "versions.h" + +TEST(TorsionCompatibilityTest, LegacyCosineParameters) { + const nvMolKit::detail::CosineTorsionParameters parameters{ + { 1, -1}, + {2.5, 3.0} + }; + EXPECT_EQ(&nvMolKit::detail::getCosineTorsionParameters(parameters), ¶meters); +} + +TEST(TorsionCompatibilityTest, VariantCosineParameters) { + using GaussianParameters = std::tuple, std::vector, std::vector, double>; + const std::variant parameters = + nvMolKit::detail::CosineTorsionParameters{ + { 1, -1}, + {2.5, 3.0} + }; + const auto& cosine = nvMolKit::detail::getCosineTorsionParameters(parameters); + EXPECT_EQ(cosine.first, (std::vector{1, -1})); + EXPECT_EQ(cosine.second, (std::vector{2.5, 3.0})); +} + +TEST(TorsionCompatibilityTest, RejectsGaussianParameters) { + using GaussianParameters = std::tuple, std::vector, std::vector, double>; + const std::variant parameters = GaussianParameters{}; + EXPECT_THROW(nvMolKit::detail::getCosineTorsionParameters(parameters), std::invalid_argument); +} + +TEST(FlattenedBuilderTest, ClonesPreparedEmbedArgsForConformerAttempts) { + auto mol = std::unique_ptr(RDKit::SmilesToMol("CCCC")); + ASSERT_NE(mol, nullptr); + RDKit::MolOps::addHs(*mol); + nvMolKit::detail::EmbedArgs original; + ASSERT_TRUE(nvMolKit::DGeomHelpers::prepareEmbedderArgs(*mol, RDKit::DGeomHelpers::ETKDGv3, original)); + + auto copied = nvMolKit::detail::cloneEmbedArgs(original); + EXPECT_EQ(copied.mmat, original.mmat); + EXPECT_EQ(copied.etkdgDetails.expTorsionAtoms, original.etkdgDetails.expTorsionAtoms); + EXPECT_EQ(copied.etkdgDetails.expTorsionAngles, original.etkdgDetails.expTorsionAngles); + copied.etkdgDetails.expTorsionAtoms.clear(); + EXPECT_FALSE(original.etkdgDetails.expTorsionAtoms.empty()); + +#if RDKIT_VERSION_MAJOR > 2026 || (RDKIT_VERSION_MAJOR == 2026 && RDKIT_VERSION_MINOR >= 9) + original.etkdgDetails.internalCoords = std::make_unique(3); + original.etkdgDetails.internalCoords->lengths = {1.1, 1.2, 1.3}; + auto ownedCopy = nvMolKit::detail::cloneEmbedArgs(original); + ASSERT_NE(ownedCopy.etkdgDetails.internalCoords, nullptr); + EXPECT_NE(ownedCopy.etkdgDetails.internalCoords.get(), original.etkdgDetails.internalCoords.get()); + EXPECT_EQ(ownedCopy.etkdgDetails.internalCoords->lengths, original.etkdgDetails.internalCoords->lengths); + ownedCopy.etkdgDetails.internalCoords->lengths[0] = 2.0; + EXPECT_DOUBLE_EQ(original.etkdgDetails.internalCoords->lengths[0], 1.1); +#endif +} TEST(FlattenedBuilderTest, NullMolecule) { RDKit::ROMol mol; From 2bc2dd4888c88f2e60fadb77977aabb9823544d8 Mon Sep 17 00:00:00 2001 From: Kevin Boyd Date: Fri, 9 Oct 2026 12:21:05 -0400 Subject: [PATCH 2/2] Fix copyright and CMake formatting checks for RDKit compatibility patch --- src/embedder_utils.h | 2 +- tests/CMakeLists.txt | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/embedder_utils.h b/src/embedder_utils.h index 399f0803..4601ae78 100644 --- a/src/embedder_utils.h +++ b/src/embedder_utils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 60db6f80..fe4cdd86 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -129,8 +129,13 @@ target_link_libraries(smarts_filter PRIVATE molecules molData ${RDKit_LIBS}) add_executable(test_flattened_builder test_flattened_builder.cu) target_link_libraries( test_flattened_builder - PRIVATE ${RDKit_LIBS} rdkit_mmff_flattened rdkit_dist_geom_flattened - device_vector test_utils embedder_utils nvmolkit_versions) + PRIVATE ${RDKit_LIBS} + rdkit_mmff_flattened + rdkit_dist_geom_flattened + device_vector + test_utils + embedder_utils + nvmolkit_versions) add_executable(test_mmff test_mmff.cu) target_link_libraries(