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..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"); @@ -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..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) + 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( 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;