MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: PyMOLCalculatePhiPsiAngles.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 PyMOL, a
   9 # molecular visualization system on an open source foundation originally
  10 # developed by Warren DeLano.
  11 #
  12 # This file is part of MayaChemTools.
  13 #
  14 # MayaChemTools is free software; you can redistribute it and/or modify it under
  15 # the terms of the GNU Lesser General Public License as published by the Free
  16 # Software Foundation; either version 3 of the License, or (at your option) any
  17 # later version.
  18 #
  19 # MayaChemTools is distributed in the hope that it will be useful, but without
  20 # any warranty; without even the implied warranty of merchantability of fitness
  21 # for a particular purpose.  See the GNU Lesser General Public License for more
  22 # details.
  23 #
  24 # You should have received a copy of the GNU Lesser General Public License
  25 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
  26 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
  27 # Boston, MA, 02111-1307, USA.
  28 #
  29 
  30 from __future__ import print_function
  31 
  32 import os
  33 import sys
  34 import time
  35 import re
  36 
  37 # PyMOL imports...
  38 try:
  39     import pymol
  40 
  41     # Finish launching PyMOL in  a command line mode for batch processing (-c)
  42     # along with the following options:  disable loading of pymolrc and plugins (-k);
  43     # suppress start up messages (-q)
  44     pymol.finish_launching(["pymol", "-ckq"])
  45 except ImportError as ErrMsg:
  46     sys.stderr.write("\nFailed to import PyMOL module/package: %s\n" % ErrMsg)
  47     sys.stderr.write("Check/update your PyMOL environment and try again.\n\n")
  48     sys.exit(1)
  49 
  50 # MayaChemTools imports...
  51 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  52 try:
  53     from docopt import docopt
  54     import MiscUtil
  55     import PyMOLUtil
  56 except ImportError as ErrMsg:
  57     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  58     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  59     sys.exit(1)
  60 
  61 ScriptName = os.path.basename(sys.argv[0])
  62 Options = {}
  63 OptionsInfo = {}
  64 
  65 
  66 def main():
  67     """Start execution of the script."""
  68 
  69     MiscUtil.PrintInfo(
  70         "\n%s (PyMOL v%s; MayaChemTools v%s; %s): Starting...\n"
  71         % (ScriptName, pymol.cmd.get_version()[0], MiscUtil.GetMayaChemToolsVersion(), time.asctime())
  72     )
  73 
  74     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  75 
  76     # Retrieve command line arguments and options...
  77     RetrieveOptions()
  78 
  79     # Process and validate command line arguments and options...
  80     ProcessOptions()
  81 
  82     # Perform actions required by the script...
  83     CalculatePhiPsiAngles()
  84 
  85     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  86     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  87 
  88 
  89 def CalculatePhiPsiAngles():
  90     """Calculate phi and psi angles for macromolecules containing amino acids."""
  91 
  92     SetupOutputFiles()
  93     WriteColumnLabels()
  94 
  95     Infile = OptionsInfo["Infile"]
  96     MolName = OptionsInfo["InfileRoot"]
  97 
  98     MiscUtil.PrintInfo("\nCalculating phi and psi torsion angles for input file %s..." % Infile)
  99 
 100     # Load infile
 101     pymol.cmd.load(Infile, MolName)
 102 
 103     OutDelim = OptionsInfo["OutDelim"]
 104     Precision = OptionsInfo["Precision"]
 105 
 106     # Go over specified chain IDs..
 107     for ChainID in OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainIDs"]:
 108         # Write out information for combined file...
 109         PhiPsiInfo = PyMOLUtil.GetPhiPsiResiduesInfo(MolName, ChainID, Categorize=True)
 110         OptionsInfo["OutfileResCount"] += len(PhiPsiInfo["ResNums"])
 111         WritePhiPsiInfo(OptionsInfo["OutFH"], MolName, ChainID, PhiPsiInfo, OutDelim, Precision)
 112 
 113         # Write out information for category fies...
 114         if OptionsInfo["MultipleOutFiles"]:
 115             PhiPsiInfoList = []
 116             GeneralPhiPsiInfo, GlycinePhiPsiInfo, ProlinePhiPsiInfo, PreProlinePhiPsiInfo = (
 117                 PyMOLUtil.GetPhiPsiCategoriesResiduesInfo(MolName, ChainID)
 118             )
 119             PhiPsiInfoList.extend([GeneralPhiPsiInfo, GlycinePhiPsiInfo, ProlinePhiPsiInfo, PreProlinePhiPsiInfo])
 120 
 121             for Index, Category in enumerate(OptionsInfo["Categories"]):
 122                 OptionsInfo["CategoriesResCount"][Category] += len(PhiPsiInfoList[Index]["ResNums"])
 123                 WritePhiPsiInfo(
 124                     OptionsInfo["CategoriesOutFHs"][Category],
 125                     MolName,
 126                     ChainID,
 127                     PhiPsiInfoList[Index],
 128                     OutDelim,
 129                     Precision,
 130                 )
 131 
 132     # Delete MolName object
 133     pymol.cmd.delete(MolName)
 134 
 135     # Close all files...
 136     CloseOutputFiles()
 137 
 138     # List number of phi and psi angles in output files...
 139     MiscUtil.PrintInfo(
 140         "\nNumber of phi and psi angles in output file %s: %d"
 141         % (OptionsInfo["Outfile"], OptionsInfo["OutfileResCount"])
 142     )
 143     if OptionsInfo["MultipleOutFiles"]:
 144         MiscUtil.PrintInfo("")
 145         for Index, Category in enumerate(OptionsInfo["Categories"]):
 146             MiscUtil.PrintInfo(
 147                 "Number of phi and psi angles in output file %s: %d"
 148                 % (OptionsInfo["CategoriesOutfiles"][Category], OptionsInfo["CategoriesResCount"][Category])
 149             )
 150 
 151 
 152 def WritePhiPsiInfo(OutFH, MolName, ChainID, PhiPsiResiduesInfo, OutDelim, Precision):
 153     """Write out phi and psi information."""
 154 
 155     for ResNum in PhiPsiResiduesInfo["ResNums"]:
 156         ResName = PhiPsiResiduesInfo["ResName"][ResNum]
 157         Phi = "%.*f" % (Precision, PhiPsiResiduesInfo["Phi"][ResNum])
 158         Psi = "%.*f" % (Precision, PhiPsiResiduesInfo["Psi"][ResNum])
 159         Category = PhiPsiResiduesInfo["Category"][ResNum]
 160 
 161         LineWords = []
 162         if OptionsInfo["OutChainID"]:
 163             LineWords.append(ChainID)
 164 
 165         LineWords.extend([ResNum, ResName, Phi, Psi])
 166 
 167         if OptionsInfo["OutCategory"]:
 168             LineWords.append(Category)
 169 
 170         Line = OutDelim.join(LineWords)
 171 
 172         OutFH.write("%s\n" % Line)
 173 
 174 
 175 def WriteColumnLabels():
 176     """Write out column labels."""
 177 
 178     OutDelim = OptionsInfo["OutDelim"]
 179 
 180     ColLabels = []
 181     if OptionsInfo["OutChainID"]:
 182         ColLabels.append("ChainID")
 183 
 184     ColLabels.extend(["ResNum", "ResName", "Phi", "Psi"])
 185 
 186     if OptionsInfo["OutCategory"]:
 187         ColLabels.append("Category")
 188 
 189     Line = OutDelim.join(ColLabels)
 190 
 191     # Write column labels for combined file...
 192     OutFH = OptionsInfo["OutFH"]
 193     OutFH.write("%s\n" % Line)
 194 
 195     if not OptionsInfo["MultipleOutFiles"]:
 196         return
 197 
 198     # Write columns labels for category files...
 199     for Category in OptionsInfo["Categories"]:
 200         CategoryOutFH = OptionsInfo["CategoriesOutFHs"][Category]
 201         CategoryOutFH.write("%s\n" % Line)
 202 
 203 
 204 def SetupOutputFiles():
 205     """Open output files."""
 206 
 207     if OptionsInfo["MultipleOutFiles"]:
 208         MiscUtil.PrintInfo("\nGenerating output files: %s" % (", ".join(OptionsInfo["OutfilesList"])))
 209     else:
 210         MiscUtil.PrintInfo("\nGenerating output file %s..." % (OptionsInfo["Outfile"]))
 211 
 212     # Open combined output file...
 213     Outfile = OptionsInfo["Outfile"]
 214     OutFH = open(Outfile, "w")
 215     if OutFH is None:
 216         MiscUtil.PrintError("Couldn't open output file: %s.\n" % (Outfile))
 217     OptionsInfo["OutFH"] = OutFH
 218     OptionsInfo["OutfileResCount"] = 0
 219 
 220     if not OptionsInfo["MultipleOutFiles"]:
 221         return
 222 
 223     # Open output files for different categories...
 224     OptionsInfo["CategoriesOutFHs"] = {}
 225     OptionsInfo["CategoriesResCount"] = {}
 226     for Category in OptionsInfo["Categories"]:
 227         CategoryOutfile = OptionsInfo["CategoriesOutfiles"][Category]
 228         CategoryOutFH = open(CategoryOutfile, "w")
 229         if CategoryOutfile is None:
 230             MiscUtil.PrintError("Couldn't open output file: %s.\n" % (CategoryOutfile))
 231 
 232         OptionsInfo["CategoriesOutFHs"][Category] = CategoryOutFH
 233         OptionsInfo["CategoriesResCount"][Category] = 0
 234 
 235 
 236 def CloseOutputFiles():
 237     """Close output files."""
 238 
 239     OptionsInfo["OutFH"].close()
 240 
 241     if not OptionsInfo["MultipleOutFiles"]:
 242         return
 243 
 244     for Category in OptionsInfo["Categories"]:
 245         CategoryOutFH = OptionsInfo["CategoriesOutFHs"][Category]
 246         CategoryOutFH.close()
 247 
 248 
 249 def RetrieveInfileInfo():
 250     """Retrieve information for input file."""
 251 
 252     Infile = OptionsInfo["Infile"]
 253     InfileRoot = OptionsInfo["InfileRoot"]
 254 
 255     ChainsAndLigandsInfo = PyMOLUtil.GetChainsAndLigandsInfo(Infile, InfileRoot)
 256     OptionsInfo["ChainsAndLigandsInfo"] = ChainsAndLigandsInfo
 257 
 258 
 259 def ProcessChainIDs():
 260     """Process specified chain IDs for infile."""
 261 
 262     MiscUtil.PrintInfo("\nProcessing specified chain IDs for input file %s..." % OptionsInfo["Infile"])
 263     ChainsAndLigandsInfo = OptionsInfo["ChainsAndLigandsInfo"]
 264     SpecifiedChainsAndLigandsInfo = PyMOLUtil.ProcessChainsAndLigandsOptionsInfo(
 265         ChainsAndLigandsInfo, "-c, --chainIDs", OptionsInfo["ChainIDs"], None, None
 266     )
 267 
 268     OptionsInfo["SpecifiedChainsAndLigandsInfo"] = SpecifiedChainsAndLigandsInfo
 269 
 270     MiscUtil.PrintInfo("Specified chain IDs: %s" % (", ".join(SpecifiedChainsAndLigandsInfo["ChainIDs"])))
 271 
 272 
 273 def SetupCategoryOutfiles():
 274     """Setup output file names for different categories of phi and psi angles."""
 275 
 276     # Initialize...
 277     OptionsInfo["OutfilesList"] = []
 278     OptionsInfo["OutfilesList"].append(OptionsInfo["Outfile"])
 279 
 280     OptionsInfo["Categories"] = ["General", "Glycine", "Proline", "PreProline"]
 281     OptionsInfo["CategoriesOutfiles"] = {}
 282     for Category in OptionsInfo["Categories"]:
 283         OptionsInfo["CategoriesOutfiles"][Category] = None
 284 
 285     if not OptionsInfo["MultipleOutFiles"]:
 286         return
 287 
 288     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Outfile"])
 289     OutfileRoot = FileName
 290     OutfileExt = FileExt
 291 
 292     for Category in OptionsInfo["Categories"]:
 293         CategoryOutfile = "%s_%s.%s" % (OutfileRoot, Category, OutfileExt)
 294         if os.path.exists(CategoryOutfile):
 295             if not OptionsInfo["Overwrite"]:
 296                 MiscUtil.PrintError(
 297                     'The category output file, %s, already exist. Use option "--ov" or "--overwrite" and try again.\n'
 298                     % (CategoryOutfile)
 299                 )
 300 
 301         OptionsInfo["CategoriesOutfiles"][Category] = CategoryOutfile
 302         OptionsInfo["OutfilesList"].append(CategoryOutfile)
 303 
 304 
 305 def ProcessOptions():
 306     """Process and validate command line arguments and options."""
 307 
 308     MiscUtil.PrintInfo("Processing options...")
 309 
 310     # Validate options...
 311     ValidateOptions()
 312 
 313     OptionsInfo["Infile"] = Options["--infile"]
 314     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Infile"])
 315     OptionsInfo["InfileRoot"] = FileName
 316 
 317     OptionsInfo["Outfile"] = Options["--outfile"]
 318     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Outfile"])
 319     OptionsInfo["OutfileRoot"] = FileName
 320 
 321     OptionsInfo["Overwrite"] = Options["--overwrite"]
 322 
 323     OptionsInfo["OutDelim"] = " "
 324     if MiscUtil.CheckFileExt(OptionsInfo["Outfile"], "csv"):
 325         OptionsInfo["OutDelim"] = ","
 326     elif MiscUtil.CheckFileExt(OptionsInfo["Outfile"], "tsv txt"):
 327         OptionsInfo["OutDelim"] = "\t"
 328     else:
 329         MiscUtil.PrintError(
 330             'The file name specified , %s, for option "--outfile" is not valid. Supported file formats: csv tsv txt\n'
 331             % (OptionsInfo["Outfile"])
 332         )
 333 
 334     OptionsInfo["OutMode"] = Options["--outMode"]
 335     OptionsInfo["MultipleOutFiles"] = True if re.match("^MultipleFiles$", OptionsInfo["OutMode"], re.I) else False
 336 
 337     OptionsInfo["OutChainID"] = True if re.match("^Yes$", Options["--outChainID"], re.I) else False
 338     OptionsInfo["OutCategory"] = True if re.match("^Yes$", Options["--outCategory"], re.I) else False
 339 
 340     OptionsInfo["Overwrite"] = Options["--overwrite"]
 341     OptionsInfo["Precision"] = int(Options["--precision"])
 342 
 343     RetrieveInfileInfo()
 344 
 345     OptionsInfo["ChainIDs"] = Options["--chainIDs"]
 346     ProcessChainIDs()
 347 
 348     SetupCategoryOutfiles()
 349 
 350 
 351 def RetrieveOptions():
 352     """Retrieve command line arguments and options."""
 353 
 354     # Get options...
 355     global Options
 356     Options = docopt(_docoptUsage_)
 357 
 358     # Set current working directory to the specified directory...
 359     WorkingDir = Options["--workingdir"]
 360     if WorkingDir:
 361         os.chdir(WorkingDir)
 362 
 363     # Handle examples option...
 364     if "--examples" in Options and Options["--examples"]:
 365         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 366         sys.exit(0)
 367 
 368 
 369 def ValidateOptions():
 370     """Validate option values."""
 371 
 372     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 373     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "pdb cif")
 374 
 375     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "csv tsv txt")
 376     MiscUtil.ValidateOptionsOutputFileOverwrite(
 377         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 378     )
 379 
 380     MiscUtil.ValidateOptionTextValue("--outMode", Options["--outMode"], "SingleFile MultipleFiles")
 381     MiscUtil.ValidateOptionTextValue("--outChainID", Options["--outChainID"], "yes no")
 382     MiscUtil.ValidateOptionTextValue("--outCategory", Options["--outCategory"], "yes no")
 383     MiscUtil.ValidateOptionIntegerValue("-p, --precision", Options["--precision"], {">": 0})
 384 
 385 
 386 # Setup a usage string for docopt...
 387 _docoptUsage_ = """
 388 PyMOLCalculatePhiPsiAngles.py - Calculate phi and psi torsion angles
 389 
 390 Usage:
 391     PyMOLCalculatePhiPsiAngles.py [--chainIDs <First, All or ID1,ID2...>]
 392                                   [--outMode <SingleFile or MultipleFies>] [--outChainID <yes or no>]
 393                                   [--outCategory <yes or no>] [--overwrite] [--precision <number>]
 394                                   [-w <dir>] -i <infile> -o <outfile>
 395     PyMOLCalculatePhiPsiAngles.py -h | --help | -e | --examples
 396 
 397 Description:
 398     Calculate phi and psi torsion angels for amino acid residues present
 399     in macromolecules.
 400 
 401     The phi and psi angles are categorized into the following groups
 402     corresponding to four types of Ramachandran plots:
 403     
 404         General: All residues except glycine, proline, or pre-proline
 405         Glycine: Only glycine residues
 406         Proline: Only proline residues
 407         Pre-Proline: Only residues before proline not including glycine or
 408             proline
 409     
 410     The supported input  file format are: PDB (.pdb), mmCIF (.cif)
 411 
 412     The supported output file formats are:  CSV (.csv), TSV (.tsv, .txt)
 413 
 414 Options:
 415     -c, --chainIDs <First, All or ID1,ID2...>  [default: All]
 416         List of chain IDs to use for calculating phi and psi angles for residues
 417         in chains. Possible values: First, All, or a comma delimited list of chain
 418         IDs. The default is to use all chain IDs in input file.
 419     -e, --examples
 420         Print examples.
 421     -h, --help
 422         Print this help message.
 423     -i, --infile <infile>
 424         Input file name.
 425     -o, --outfile <outfile>
 426         Output file name for writing out calculated values. Supported text file
 427         extensions: csv, tsv or txt.
 428         
 429         In addition to the specified outfile containing phi and psi angles for all
 430         residues, a set of additional output files is generated for 'MultipleFiles'
 431         value of '--outMode' option. The names of these output files are
 432         automatically generated from the the name of the specified output
 433         file as shown below:
 434         
 435             General: <OutfileRoot>_General.<OutfileExt>
 436             Glycine: <OutfileRoot>_Glycine.<OutfileExt>
 437             Proline: <OutfileRoot>_Proline.<OutfileExt>
 438             Pre-Proline: <OutfileRoot>_PreProline.<OutfileExt>
 439         
 440     --outMode <SingleFile or MultipleFiles>  [default: SingleFile]
 441         A single output file containing phi and psi angles for all residues or
 442         multiple output files corresponding to different categories of angles.
 443         
 444         The phi and psi angles are categorized into the following groups
 445         corresponding to four types of Ramachandran plots:
 446         
 447             General: All residues except glycine, proline, or pre-proline
 448             Glycine: Only glycine residues
 449             Proline: Only proline residues
 450             Pre-Proline: Only residues before proline not including glycine or
 451                 proline
 452         
 453         The output files contain the following information:
 454         
 455             ChainID ResNum ResName Phi Psi Category
 456         
 457     --outChainID <yes or no>  [default: yes]
 458         Write chain IDs to output file.
 459     --outCategory <yes or no>  [default: yes]
 460         Write phi and psi category to output file.
 461     --overwrite
 462         Overwrite existing files.
 463     -p, --precision <number>  [default: 2]
 464         Floating point precision for writing the calculated phi and psi angles.
 465     -w, --workingdir <dir>
 466         Location of working directory which defaults to the current directory.
 467 
 468 Examples:
 469     To calculate phi and psi angles for all residues across all chains in input
 470     file and write out a single CSV file containing calculated values along with
 471     chain IDs, residue names and numbers, and category of angles corresponding
 472     to Ramachandran plots, type:
 473 
 474         % PyMOLCalculatePhiPsiAngles.py -i Sample3.pdb -o Sample3Out.csv
 475 
 476     To calculate phi and psi angles for all residues across all chains in input
 477     file and write out a multiple CSV files corresponding to categories of angles
 478     for Ramachandran plots along with other relevant information, type:
 479 
 480         % PyMOLCalculatePhiPsiAngles.py --outMode MultipleFiles -i Sample3.pdb
 481           -o Sample3Out.csv
 482 
 483     To calculate phi and psi angles for all residues in a specific chain in input
 484     file and write out a single TSV file containing calculated values along with
 485     other relevant information, type:
 486 
 487         % PyMOLCalculatePhiPsiAngles.py -c E  -i Sample3.pdb -o Sample3Out.csv
 488 
 489     To calculate phi and psi angles for all residues in a specific chain in input
 490     file and write out a multiple TSV files containing calculated values at a specific
 491     precision along with other relevant information, type:
 492 
 493         % PyMOLCalculatePhiPsiAngles.py --outMode MultipleFiles --chainIDs I
 494           -i Sample3.pdb -o Sample3Out.csv
 495 
 496 Author:
 497     Manish Sud(msud@san.rr.com)
 498 
 499 See also:
 500     DownloadPDBFiles.pl, PyMOLCalculateRMSD.py, PyMOLCalculateProperties.py,
 501     PyMOLGenerateRamachandranPlots.py
 502 
 503 Copyright:
 504     Copyright (C) 2026 Manish Sud. All rights reserved.
 505 
 506     The functionality available in this script is implemented using PyMOL, a
 507     molecular visualization system on an open source foundation originally
 508     developed by Warren DeLano.
 509 
 510     This file is part of MayaChemTools.
 511 
 512     MayaChemTools is free software; you can redistribute it and/or modify it under
 513     the terms of the GNU Lesser General Public License as published by the Free
 514     Software Foundation; either version 3 of the License, or (at your option) any
 515     later version.
 516 
 517 """
 518 
 519 if __name__ == "__main__":
 520     main()