1 #!/bin/env python 2 # 3 # File: RDKitCalculateRMSD.py 4 # Author: Manish Sud <msud@san.rr.com> 5 # 6 # Copyright (C) 2026 Manish Sud. All rights reserved. 7 # 8 # The functionality available in this script is implemented using RDKit, an 9 # open source toolkit for cheminformatics developed by Greg Landrum. 10 # 11 # This file is part of MayaChemTools. 12 # 13 # MayaChemTools is free software; you can redistribute it and/or modify it under 14 # the terms of the GNU Lesser General Public License as published by the Free 15 # Software Foundation; either version 3 of the License, or (at your option) any 16 # later version. 17 # 18 # MayaChemTools is distributed in the hope that it will be useful, but without 19 # any warranty; without even the implied warranty of merchantability of fitness 20 # for a particular purpose. See the GNU Lesser General Public License for more 21 # details. 22 # 23 # You should have received a copy of the GNU Lesser General Public License 24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or 25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330, 26 # Boston, MA, 02111-1307, USA. 27 # 28 29 from __future__ import print_function 30 31 import os 32 import sys 33 import time 34 import re 35 36 # RDKit imports... 37 try: 38 from rdkit import rdBase 39 from rdkit.Chem import AllChem, rdMolAlign 40 except ImportError as ErrMsg: 41 sys.stderr.write("\nFailed to import RDKit module/package: %s\n" % ErrMsg) 42 sys.stderr.write("Check/update your RDKit environment and try again.\n\n") 43 sys.exit(1) 44 45 # MayaChemTools imports... 46 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python")) 47 try: 48 from docopt import docopt 49 import MiscUtil 50 import RDKitUtil 51 except ImportError as ErrMsg: 52 sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg) 53 sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n") 54 sys.exit(1) 55 56 ScriptName = os.path.basename(sys.argv[0]) 57 Options = {} 58 OptionsInfo = {} 59 60 61 def main(): 62 """Start execution of the script.""" 63 64 MiscUtil.PrintInfo( 65 "\n%s (RDKit v%s; MayaChemTools v%s; %s): Starting...\n" 66 % (ScriptName, rdBase.rdkitVersion, MiscUtil.GetMayaChemToolsVersion(), time.asctime()) 67 ) 68 69 (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime() 70 71 # Retrieve command line arguments and options... 72 RetrieveOptions() 73 74 # Process and validate command line arguments and options... 75 ProcessOptions() 76 77 # Perform actions required by the script... 78 CalculateRMSD() 79 80 MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName) 81 MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime)) 82 83 84 def CalculateRMSD(): 85 """Calculate RMSD values.""" 86 87 if not re.match("^(OneToOne|AllToAll|FirstToAll)$", OptionsInfo["Mode"], re.I): 88 MiscUtil.PrintError("RMSD couldn't be calculated: Specified mode, %s, is not supported" % OptionsInfo["Mode"]) 89 90 RefFile = OptionsInfo["RefFile"] 91 ProbeFile = OptionsInfo["ProbeFile"] 92 93 Outfile = OptionsInfo["Outfile"] 94 OutDelim = OptionsInfo["OutDelim"] 95 96 # Read reference and probe molecules... 97 OptionsInfo["InfileParams"]["AllowEmptyMols"] = False 98 99 MiscUtil.PrintInfo("\nProcessing file %s..." % (RefFile)) 100 ValidRefMols, RefMolCount, ValidRefMolCount = RDKitUtil.ReadAndValidateMolecules( 101 RefFile, **OptionsInfo["InfileParams"] 102 ) 103 104 MiscUtil.PrintInfo("Processing file %s..." % (ProbeFile)) 105 ValidProbeMols, ProbeMolCount, ValidProbeMolCount = RDKitUtil.ReadAndValidateMolecules( 106 ProbeFile, **OptionsInfo["InfileParams"] 107 ) 108 109 # Set up output file... 110 MiscUtil.PrintInfo("\nGenerating file %s...\n" % Outfile) 111 OutFH = open(Outfile, "w") 112 if OutFH is None: 113 MiscUtil.PrintError("Couldn't open output file: %s.\n" % (Outfile)) 114 115 Line = "RefMolID%sProbeMolID%sRMSD\n" % (OutDelim, OutDelim) 116 OutFH.write(Line) 117 118 if re.match("^OneToOne$", OptionsInfo["Mode"], re.I): 119 CalculateOneToOneRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim) 120 elif re.match("^AllToAll$", OptionsInfo["Mode"], re.I): 121 CalculateAllToAllRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim) 122 elif re.match("^FirstToAll$", OptionsInfo["Mode"], re.I): 123 CalculateFirstToAllRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim) 124 else: 125 MiscUtil.PrintError("RMSD couldn't be calculated: Specified mode, %s, is not supported" % OptionsInfo["Mode"]) 126 127 OutFH.close() 128 129 MiscUtil.PrintInfo("\nTotal number of molecules: Reference - %d; Probe - %d" % (RefMolCount, ProbeMolCount)) 130 MiscUtil.PrintInfo("Number of valid molecules: Reference - %d; Probe - %d" % (ValidRefMolCount, ValidProbeMolCount)) 131 MiscUtil.PrintInfo( 132 "Number of ignored molecules: Reference - %d; Probe - %d" 133 % ((RefMolCount - ValidRefMolCount), (ProbeMolCount - ValidProbeMolCount)) 134 ) 135 136 137 def CalculateOneToOneRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim): 138 """Calculate pairwise RMSD values.""" 139 140 ValidRefMolCount = len(ValidRefMols) 141 ValidProbeMolCount = len(ValidProbeMols) 142 143 MolCount = ValidRefMolCount 144 if ValidRefMolCount > ValidProbeMolCount: 145 MolCount = ValidProbeMolCount 146 147 if ValidRefMolCount != ValidProbeMolCount: 148 MiscUtil.PrintWarning( 149 "Number of valid reference molecules, %d, is not equal to number of valid probe molecules, %d .\n" 150 % (ValidRefMolCount, ValidProbeMolCount) 151 ) 152 MiscUtil.PrintWarning("Pairwise RMSD will be calculated only for first %s molecules.\n" % (MolCount)) 153 154 # Process molecules... 155 for MolIndex in range(0, MolCount): 156 RefMol = ValidRefMols[MolIndex] 157 ProbeMol = ValidProbeMols[MolIndex] 158 159 RefMolName = RDKitUtil.GetMolName(RefMol, (MolIndex + 1)) 160 ProbeMolName = RDKitUtil.GetMolName(ProbeMol, (MolIndex + 1)) 161 162 RMSD = CalculateRMSDValue(RefMol, ProbeMol) 163 164 Line = "%s%s%s%s%s\n" % (RefMolName, OutDelim, ProbeMolName, OutDelim, RMSD) 165 OutFH.write(Line) 166 167 168 def CalculateAllToAllRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim): 169 """Calculate RMSD values between all pairs of molecules.""" 170 171 # Process molecules... 172 RefMolCount = 0 173 for RefMol in ValidRefMols: 174 RefMolCount += 1 175 RefMolName = RDKitUtil.GetMolName(RefMol, RefMolCount) 176 177 ProbeMolCount = 0 178 for ProbeMol in ValidProbeMols: 179 ProbeMolCount += 1 180 ProbeMolName = RDKitUtil.GetMolName(ProbeMol, ProbeMolCount) 181 182 RMSD = CalculateRMSDValue(RefMol, ProbeMol) 183 184 Line = "%s%s%s%s%s\n" % (RefMolName, OutDelim, ProbeMolName, OutDelim, RMSD) 185 OutFH.write(Line) 186 187 188 def CalculateFirstToAllRMSDValues(ValidRefMols, ValidProbeMols, OutFH, OutDelim): 189 """Calculate RMSD values between first reference molecues and all probe molecules.""" 190 191 # Process molecules... 192 RefMol = ValidRefMols[0] 193 RefMolCount = 1 194 RefMolName = RDKitUtil.GetMolName(RefMol, RefMolCount) 195 196 ProbeMolCount = 0 197 for ProbeMol in ValidProbeMols: 198 ProbeMolCount += 1 199 ProbeMolName = RDKitUtil.GetMolName(ProbeMol, ProbeMolCount) 200 201 RMSD = CalculateRMSDValue(RefMol, ProbeMol) 202 203 Line = "%s%s%s%s%s\n" % (RefMolName, OutDelim, ProbeMolName, OutDelim, RMSD) 204 OutFH.write(Line) 205 206 207 def CalculateRMSDValue(RefMol, ProbeMol): 208 """Calculate RMSD value for a pair of molecules and return it as a string.""" 209 210 try: 211 if OptionsInfo["UseBestRMSD"]: 212 RMSD = AllChem.GetBestRMS(ProbeMol, RefMol) 213 else: 214 RMSD = rdMolAlign.AlignMol(ProbeMol, RefMol, maxIters=OptionsInfo["MaxIters"]) 215 RMSD = "%.2f" % RMSD 216 except (RuntimeError, ValueError): 217 RMSD = "None" 218 219 return RMSD 220 221 222 def ProcessOptions(): 223 """Process and validate command line arguments and options.""" 224 225 MiscUtil.PrintInfo("Processing options...") 226 227 # Validate options... 228 ValidateOptions() 229 230 OptionsInfo["CalcRMSD"] = Options["--calcRMSD"] 231 OptionsInfo["UseBestRMSD"] = False 232 if re.match("^BestRMSD$", OptionsInfo["CalcRMSD"], re.I): 233 OptionsInfo["UseBestRMSD"] = True 234 235 OptionsInfo["MaxIters"] = int(Options["--maxIters"]) 236 237 OptionsInfo["Mode"] = Options["--mode"] 238 239 OptionsInfo["RefFile"] = Options["--reffile"] 240 OptionsInfo["ProbeFile"] = Options["--probefile"] 241 242 # No need for any RDKit specific --outfileParams.... 243 OptionsInfo["InfileParams"] = MiscUtil.ProcessOptionInfileParameters("--infileParams", Options["--infileParams"]) 244 245 OptionsInfo["Outfile"] = Options["--outfile"] 246 247 OptionsInfo["Overwrite"] = Options["--overwrite"] 248 249 OptionsInfo["OutDelim"] = " " 250 if MiscUtil.CheckFileExt(OptionsInfo["Outfile"], "csv"): 251 OptionsInfo["OutDelim"] = "," 252 elif MiscUtil.CheckFileExt(OptionsInfo["Outfile"], "tsv txt"): 253 OptionsInfo["OutDelim"] = "\t" 254 else: 255 MiscUtil.PrintError( 256 'The file name specified , %s, for option "--outfile" is not valid. Supported file formats: csv tsv txt\n' 257 % (OptionsInfo["Outfile"]) 258 ) 259 260 261 def RetrieveOptions(): 262 """Retrieve command line arguments and options.""" 263 264 # Get options... 265 global Options 266 Options = docopt(_docoptUsage_) 267 268 # Set current working directory to the specified directory... 269 WorkingDir = Options["--workingdir"] 270 if WorkingDir: 271 os.chdir(WorkingDir) 272 273 # Handle examples option... 274 if "--examples" in Options and Options["--examples"]: 275 MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_)) 276 sys.exit(0) 277 278 279 def ValidateOptions(): 280 """Validate option values.""" 281 282 MiscUtil.ValidateOptionTextValue("--calcRMSD", Options["--calcRMSD"], "RMSD BestRMSD") 283 284 MiscUtil.ValidateOptionIntegerValue("--maxIters", Options["--maxIters"], {">": 0}) 285 MiscUtil.ValidateOptionTextValue("--mode", Options["--mode"], "OneToOne AllToAll FirstToAll") 286 287 MiscUtil.ValidateOptionFilePath("-r, --reffile", Options["--reffile"]) 288 MiscUtil.ValidateOptionFileExt("-r, --reffile", Options["--reffile"], "sdf sd mol") 289 290 MiscUtil.ValidateOptionFilePath("-p, --probefile", Options["--probefile"]) 291 MiscUtil.ValidateOptionFileExt("-p, --probefile", Options["--probefile"], "sdf sd mol") 292 293 MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "csv tsv txt") 294 MiscUtil.ValidateOptionsOutputFileOverwrite( 295 "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"] 296 ) 297 MiscUtil.ValidateOptionsDistinctFileNames( 298 "-r, --reffile", Options["--reffile"], "-o, --outfile", Options["--outfile"] 299 ) 300 MiscUtil.ValidateOptionsDistinctFileNames( 301 "-p, --probefile", Options["--probefile"], "-o, --outfile", Options["--outfile"] 302 ) 303 304 305 # Setup a usage string for docopt... 306 _docoptUsage_ = """ 307 RDKitCalculateRMSD.py - Calculate RMSD between molecules 308 309 Usage: 310 RDKitCalculateRMSD.py [--calcRMSD <RMSD, BestRMSD>] [--infileParams <Name,Value,...>] 311 [--maxIters <number>] [--mode <OneToOne, AllToAll, FirstToAll>] 312 [--overwrite] [-w <dir>] -r <reffile> -p <probefile> -o <outfile> 313 RDKitCalculateRMSD.py -h | --help | -e | --examples 314 315 Description: 316 Calculate Root Mean Square Distance (RMSD) between a set of similar molecules in 317 reference and probe input files. The RDKit function fails to calculate RMSD values for 318 dissimilar molecules. Consequently, a text string 'None' is written out as a RMSD value 319 for dissimilar molecule pairs. 320 321 The supported input file formats are: Mol (.mol), SD (.sdf, .sd) 322 323 The supported output file formats are: CSV (.csv), TSV (.tsv, .txt) 324 325 Options: 326 -c, --calcRMSD <RMSD, BestRMSD> [default: RMSD] 327 Methodology for calculating RMSD values. Possible values: RMSD, BestRMSD. 328 During BestRMSMode mode, the RDKit 'function AllChem.GetBestRMS' is used to 329 align and calculate RMSD. This function calculates optimal RMSD for aligning two 330 molecules, taking symmetry into account. Otherwise, the RMSD value is calculated 331 using 'AllChem.AlignMol function' without changing the atom order. A word to the 332 wise from RDKit documentation: The AllChem.GetBestRMS function will attempt to 333 align all permutations of matching atom orders in both molecules, for some molecules 334 it will lead to 'combinatorial explosion'. 335 --infileParams <Name,Value,...> [default: auto] 336 A comma delimited list of parameter name and value pairs for reading 337 molecules from files. The supported parameter names for different file 338 formats, along with their default values, are shown below: 339 340 SD, MOL: removeHydrogens,yes,sanitize,yes,strictParsing,yes 341 342 --maxIters <number> [default: 50] 343 Maximum number of iterations to perform for each molecule pair during minimization 344 of RMSD values. This option is ignored during BestRMSD mode. 345 -m, --mode <OneToOne, AllToAll, FirstToAll> [default: OneToOne] 346 Specify how molecules are handled in reference and probe input files during 347 calculation of RMSD between reference and probe molecules. Possible values: 348 OneToOne, AllToAll and AllToFirst. For OneToOne mode, the number of molecules 349 in reference file must be equal to the number of molecules in probe file. The RMSD 350 is calculated for each pair of molecules in the reference and probe file and written 351 to the output file. For AllToAll mode, the RMSD is calculated for each reference 352 molecule against all probe molecules. For FirstToAll mode, however, the RMSD 353 is only calculated for the first reference molecule against all probe molecules. 354 -e, --examples 355 Print examples. 356 -h, --help 357 Print this help message. 358 -p, --probefile <probefile> 359 Probe input file name. 360 -r, --reffile <reffile> 361 Reference input file name. 362 -o, --outfile <outfile> 363 Output file name for writing out RMSD values. Supported text file extensions: csv or tsv. 364 --overwrite 365 Overwrite existing files. 366 -w, --workingdir <dir> 367 Location of working directory which defaults to the current directory. 368 369 Examples: 370 To calculate RMSD between pair of molecules in reference and probe input 371 3D SD files and write out a CSV file containing calculated RMSD values along with 372 appropriate molecule IDs, type: 373 374 % RDKitCalculateRMSD.py -r Sample3DRef.sdf -p Sample3DProb.sdf 375 -o SampleOut.csv 376 377 To calculate RMSD between all molecules in reference and probe input 378 3D SD files and write out a CSV file containing calculated RMSD values along with 379 appropriate molecule IDs, type: 380 381 % RDKitCalculateRMSD.py -m AllToAll -r Sample3DRef.sdf -p 382 Sample3DProb.sdf -o SampleOut.csv 383 384 To calculate best RMSD between first molecule in reference all probe molecules 385 in 3D SD files and write out a TSV file containing calculated RMSD values along with 386 appropriate molecule IDs, type: 387 388 % RDKitCalculateRMSD.py -m FirstToAll --calcRMSD BestRMSD -r 389 Sample3DRef.sdf -p Sample3DProb.sdf -o SampleOut.tsv 390 391 To calculate RMSD between all molecules in reference and probe molecules input 392 3D SD files without removing hydrogens and write out a TSV file containing 393 calculated RMSD values along with appropriate molecule IDs, type: 394 395 % RDKitCalculateRMSD.py -m AllToAll --infileParams 396 "removeHydrogens,no" -r Sample3DRef.sdf -p Sample3DProb.sdf 397 -o SampleOut.tsv 398 399 Author: 400 Manish Sud(msud@san.rr.com) 401 402 See also: 403 RDKitCalculateMolecularDescriptors.py, RDKitCompareMoleculeShapes.py, RDKitConvertFileFormat.py, 404 RDKitGenerateConformers.py, RDKitPerformMinimization.py 405 406 Copyright: 407 Copyright (C) 2026 Manish Sud. All rights reserved. 408 409 The functionality available in this script is implemented using RDKit, an 410 open source toolkit for cheminformatics developed by Greg Landrum. 411 412 This file is part of MayaChemTools. 413 414 MayaChemTools is free software; you can redistribute it and/or modify it under 415 the terms of the GNU Lesser General Public License as published by the Free 416 Software Foundation; either version 3 of the License, or (at your option) any 417 later version. 418 419 """ 420 421 if __name__ == "__main__": 422 main()