// // Copyright (C) 2004-2025 Greg Landrum and other RDKit contributors // // @@ All Rights Reserved @@ // This file is part of the RDKit. // The contents are covered by the terms of the BSD license // which is included in the file license.txt, found at the root // of the RDKit source tree. // #define PY_ARRAY_UNIQUE_SYMBOL rdalignment_array_API #include #include #include #include #include #include #include namespace python = boost::python; namespace RDNumeric { namespace Alignments { class PointVectManager { public: PointVectManager() = default; PointVectManager(const PointVectManager &rhs) = delete; PointVectManager &operator=(const PointVectManager &rhs) = delete; ~PointVectManager() { for (auto &i : m_ptr) { delete i; } } RDGeom::Point3DConstPtrVect &getVect() { return m_ptr; } private: RDGeom::Point3DConstPtrVect m_ptr; }; void GetPointsFromPythonSequence(python::object &points, RDGeom::Point3DConstPtrVect &pts) { PyObject *pyObj = points.ptr(); unsigned int nrows, ncols; double *data; if (PyArray_Check(pyObj)) { // get the dimensions of the array auto *ptsMat = reinterpret_cast(pyObj); nrows = PyArray_DIM(ptsMat, 0); ncols = PyArray_DIM(ptsMat, 1); if (ncols != 3) { throw_value_error("Wrong dimension for the points array"); } data = reinterpret_cast(PyArray_DATA(ptsMat)); for (unsigned int i = 0; i < nrows; i++) { auto *rpt = new RDGeom::Point3D(data[i * 3], data[i * 3 + 1], data[i * 3 + 2]); pts.push_back(rpt); } } else if (PySequence_Check(pyObj)) { nrows = PySequence_Size(pyObj); if (nrows <= 0) { throw_value_error("Empty sequence passed in"); } python::extract ptOk(points[0]); if (!ptOk.check()) { for (unsigned int i = 0; i < nrows; i++) { PySequenceHolder row(points[i]); if (row.size() != 3) { throw_value_error("Wrong number of entries in the list of lists"); } auto *rpt = new RDGeom::Point3D(row[0], row[1], row[2]); pts.push_back(rpt); } } else { for (unsigned int i = 0; i < nrows; i++) { python::extract pt(points[i]); if (pt.check()) { auto *rpt = new RDGeom::Point3D(pt); pts.push_back(rpt); } else { throw_value_error("non-Point3D found in sequence of points"); } } } } else { throw_value_error("non-sequence argument provided"); } } PyObject *AlignPointPairs(python::object refPoints, python::object probePoints, python::object weights = python::list(), bool reflect = false, unsigned int maxIterations = 50) { // The reference and probe points can be specified in two formats // 1. A Numeric array of dimensions (N,3) where N is the number of points // 2. A list (or tuple) or lists (or tuples) // // The similar thing applies to weights // 1. Can be a numeric vector of size N // 2. A list of doubles of size N // first deal with situation where we have Numerics arrays PointVectManager refPts, probePts; GetPointsFromPythonSequence(refPoints, refPts.getVect()); GetPointsFromPythonSequence(probePoints, probePts.getVect()); unsigned int npt = refPts.getVect().size(); if (npt != probePts.getVect().size()) { throw_value_error("Mis-match in number of points"); } PyObject *weightsObj = weights.ptr(); std::unique_ptr wtsVec; double *data; if (PyArray_Check(weightsObj)) { auto *wtsMat = reinterpret_cast(weightsObj); unsigned int nwts = PyArray_DIM(wtsMat, 0); if (nwts != npt) { throw_value_error( "Number of weights supplied do not match the number of points"); } wtsVec.reset(new RDNumeric::DoubleVector(nwts)); data = reinterpret_cast(PyArray_DATA(wtsMat)); for (unsigned int i = 0; i < nwts; i++) { wtsVec->setVal(i, data[i]); } } else { PySequenceHolder wts(weights); unsigned int nwts = wts.size(); if (nwts > 0) { if (nwts != npt) { throw_value_error( "Number of weights supplied do not match the number of points"); } wtsVec.reset(new RDNumeric::DoubleVector(nwts)); for (unsigned int i = 0; i < npt; i++) { wtsVec->setVal(i, wts[i]); } } } RDGeom::Transform3D trans; double ssd = AlignPoints(refPts.getVect(), probePts.getVect(), trans, wtsVec.get(), reflect, maxIterations); npy_intp dims[2]; dims[0] = 4; dims[1] = 4; auto *res = (PyArrayObject *)PyArray_SimpleNew(2, dims, NPY_DOUBLE); auto *resData = reinterpret_cast(PyArray_DATA(res)); const double *tdata = trans.getData(); for (unsigned int i = 0; i < trans.numRows(); ++i) { unsigned int itab = i * 4; for (unsigned int j = 0; j < trans.numRows(); ++j) { resData[itab + j] = tdata[itab + j]; } } PyObject *resTup = PyTuple_New(2); PyObject *ssdItem = PyFloat_FromDouble(ssd); PyTuple_SetItem(resTup, 0, ssdItem); PyTuple_SetItem(resTup, 1, PyArray_Return(res)); return resTup; } } // namespace Alignments } // namespace RDNumeric BOOST_PYTHON_MODULE(rdAlignment) { rdkit_import_array(); python::scope().attr("__doc__") = "Module containing functions to align pairs of points in 3D"; std::string docString = "Compute the optimal alignment (minimum RMSD) between two set of points using the quaternion algorithm \n\n\ \n\ ARGUMENTS:\n\n\ - refPoints : reference points specified as a N by 3 Numeric array or \n\ sequence of 3-sequences or sequence of Point3Ds \n\ - probePoints : probe points to align to reference points - same format \n\ restrictions as reference points apply here \n\ - weights : optional numeric vector or list of weights to associate to each pair of points\n\ - reflect : reflect the probe points before attempting alignment\n\ - maxIteration : maximum number of iterations for the eigen solver \n\ \n\ RETURNS:\n\n\ a 2-tuple:\n\ - SSD value for the alignment\n\ - the 4x4 transform matrix, as a Numeric array\n\ \n"; python::def( "GetAlignmentTransform", RDNumeric::Alignments::AlignPointPairs, (python::arg("refPoints"), python::arg("probePoints"), python::arg("weights") = python::list(), python::arg("reflect") = false, python::arg("maxIterations") = 50), docString.c_str()); }