MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: PyMOLMutateNucleicAcids.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     PerformMutagenesis()
  84 
  85     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  86     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  87 
  88 
  89 def PerformMutagenesis():
  90     """Mutate specified residues across chains and generate an output file."""
  91 
  92     MiscUtil.PrintInfo("\nApplying mutations...")
  93 
  94     # Load macromolecule from input file...
  95     MolName = OptionsInfo["InfileRoot"]
  96     LoadMolecule(OptionsInfo["Infile"], MolName)
  97 
  98     # Apply mutations...
  99     for Mutation, ChainID, ResNum, NewBaseName in OptionsInfo["SpecifiedMutationsInfo"]:
 100         ApplyMutation(Mutation, MolName, ChainID, ResNum, NewBaseName)
 101 
 102     #  Generate output file...
 103     Outfile = OptionsInfo["Outfile"]
 104     MiscUtil.PrintInfo("\nGenerating output file %s..." % Outfile)
 105     pymol.cmd.save(Outfile, MolName)
 106 
 107     # Delete macromolecule...
 108     DeleteMolecule(MolName)
 109 
 110 
 111 def ApplyMutation(Mutation, MolName, ChainID, ResNum, NewBaseName):
 112     """Apply mutatation."""
 113 
 114     MiscUtil.PrintInfo("\nApplying mutation %s" % Mutation)
 115 
 116     # Setup wizard for nucleic acids mutagenesis...
 117     try:
 118         pymol.cmd.wizard("nucmutagenesis")
 119     except pymol.CmdException as ErrMsg:
 120         MiscUtil.PrintError(
 121             "The nucleic acids mutageneis wizard is not available in your PyMOL environment: %s\n" % ErrMsg
 122         )
 123 
 124     pymol.cmd.refresh_wizard()
 125 
 126     # Setup residue to be mutated...
 127     ResSelection = "/%s//%s/%s" % (MolName, ChainID, ResNum)
 128     pymol.cmd.get_wizard().do_select(ResSelection)
 129 
 130     # Setup new mutated residue...
 131     pymol.cmd.get_wizard().set_mode("%s" % NewBaseName)
 132 
 133     # Mutate...
 134     pymol.cmd.get_wizard().apply()
 135 
 136     # Quit wizard...
 137     pymol.cmd.set_wizard()
 138 
 139 
 140 def RetrieveChainsIDs():
 141     """Retrieve chain IDs."""
 142 
 143     MolName = OptionsInfo["InfileRoot"]
 144     Infile = OptionsInfo["Infile"]
 145 
 146     MiscUtil.PrintInfo("\nRetrieving chains information for input file %s..." % Infile)
 147 
 148     LoadMolecule(Infile, MolName)
 149 
 150     ChainIDs = PyMOLUtil.GetChains(MolName)
 151 
 152     DeleteMolecule(MolName)
 153 
 154     if ChainIDs is None:
 155         ChainIDs = []
 156 
 157     # Print out chain and ligand IDs...
 158     ChainInfo = ", ".join(ChainIDs) if len(ChainIDs) else "None"
 159     MiscUtil.PrintInfo("Chain IDs: %s" % ChainInfo)
 160 
 161     OptionsInfo["ChainIDs"] = ChainIDs
 162 
 163 
 164 def ProcessSpecifiedMutations():
 165     """Process specified mutations."""
 166 
 167     MiscUtil.PrintInfo("\nProcessing specified mutations...")
 168 
 169     CanonicalBaseNameMap = {
 170         "ADENINE": "Adenine",
 171         "CYTOSINE": "Cytosine",
 172         "GUANINE": "Guanine",
 173         "THYMINE": "Thymine",
 174         "URACIL": "Uracil",
 175         "ADE": "Adenine",
 176         "CYT": "Cytosine",
 177         "GUA": "Guanine",
 178         "THY": "Thymine",
 179         "URA": "Uracil",
 180     }
 181 
 182     SpecifiedMutationsInfo = []
 183 
 184     Mutations = re.sub(" ", "", OptionsInfo["Mutations"])
 185     MutationsWords = Mutations.split(",")
 186     if not len(MutationsWords):
 187         MiscUtil.PrintError(
 188             'The number of comma delimited mutations specified using "-m, --mutations" option, "%s",  must be > 0.'
 189             % (OptionsInfo["Mutations"])
 190         )
 191 
 192     # Load macromolecule from input file...
 193     MolName = OptionsInfo["InfileRoot"]
 194     LoadMolecule(OptionsInfo["Infile"], MolName)
 195 
 196     FirstMutation = True
 197     CurrentChainID = None
 198     CanonicalMutationMap = {}
 199     MutationsCount, ValidMutationsCount = [0] * 2
 200 
 201     for Mutation in MutationsWords:
 202         MutationsCount += 1
 203         if not len(Mutation):
 204             MiscUtil.PrintWarning(
 205                 'The mutation, "%s", specified using "-m, --mutations" option is empty.\nIgnoring mutation...'
 206                 % (Mutation)
 207             )
 208             continue
 209 
 210         # Match with a chain ID...
 211         MatchedResults = re.match(r"^([a-z0-9]+):([0-9]+)([a-z]+)$", Mutation, re.I)
 212         if not MatchedResults:
 213             # Match without a chain ID...
 214             MatchedResults = re.match(r"^([0-9]+)([a-z]+)$", Mutation, re.I)
 215 
 216         if not MatchedResults:
 217             MiscUtil.PrintWarning(
 218                 'The format of mutation, "%s", specified using "-m, --mutations" option is not valid. Supported format: <ChainID>:<ResNum><BaseName> or <ResNum><BaseName>\nIgnoring mutation...'
 219                 % (Mutation)
 220             )
 221             continue
 222 
 223         NumOfMatchedGroups = len(MatchedResults.groups())
 224         if NumOfMatchedGroups == 2:
 225             ResNum, NewBaseName = MatchedResults.groups()
 226         elif NumOfMatchedGroups == 3:
 227             CurrentChainID, ResNum, NewBaseName = MatchedResults.groups()
 228         else:
 229             MiscUtil.PrintWarning(
 230                 'The format of mutation, "%s", specified using "-m, --mutations" option is not valid. Supported format: <ChainID>:<ResNum><BaseName> or <ResNum><BaseName>\nIgnoring mutation...'
 231                 % (Mutation)
 232             )
 233             continue
 234 
 235         if FirstMutation:
 236             FirstMutation = False
 237             if CurrentChainID is None:
 238                 MiscUtil.PrintError(
 239                     'The first mutation, "%s", specified using "-m, --mutations" option must be colon delimited and contain only two values, the first value corresponding to chain ID'
 240                     % (Mutation)
 241                 )
 242 
 243         CanonicalBaseName = NewBaseName.upper()
 244         if CanonicalBaseName in CanonicalBaseNameMap:
 245             NewBaseName = CanonicalBaseNameMap[CanonicalBaseName]
 246 
 247         # Check for duplicate mutation specifications...
 248         MutationSpec = "%s:%s%s" % (CurrentChainID, ResNum, NewBaseName)
 249         CanonicalMutation = MutationSpec.lower()
 250         if CanonicalMutation in CanonicalMutationMap:
 251             MiscUtil.PrintWarning(
 252                 'The mutation, "%s", specified using "-m, --mutations" option already exist for the current chain ID %s.\nIgnoring mutation...'
 253                 % (Mutation, CurrentChainID)
 254             )
 255             continue
 256         CanonicalMutationMap[CanonicalMutation] = Mutation
 257 
 258         # Is ResNum and BaseName present in input file?
 259         SelectionCmd = "%s and chain %s and resi %s" % (MolName, CurrentChainID, ResNum)
 260         ResiduesInfo = PyMOLUtil.GetSelectionResiduesInfo(SelectionCmd)
 261         if (ResiduesInfo is None) or (not len(ResiduesInfo["ResNames"])):
 262             MiscUtil.PrintWarning(
 263                 'The residue number, %s, in mutation, "%s", specified using "-m, --mutations" option appears to be missing in input file.\nIgnoring mutation...'
 264                 % (ResNum, Mutation)
 265             )
 266             continue
 267 
 268         ValidMutationsCount += 1
 269 
 270         # Track mutation information...
 271         SpecifiedMutationsInfo.append([Mutation, CurrentChainID, ResNum, NewBaseName])
 272 
 273     # Delete macromolecule...
 274     DeleteMolecule(MolName)
 275 
 276     MiscUtil.PrintInfo("\nTotal number of mutations: %d" % MutationsCount)
 277     MiscUtil.PrintInfo("Number of valid mutations: %d" % ValidMutationsCount)
 278     MiscUtil.PrintInfo("Number of ignored mutations: %d" % (MutationsCount - ValidMutationsCount))
 279 
 280     if not len(SpecifiedMutationsInfo):
 281         MiscUtil.PrintError(
 282             'No valid mutations, "%s" specified using "-m, --mutations" option.' % (OptionsInfo["Mutations"])
 283         )
 284 
 285     OptionsInfo["SpecifiedMutationsInfo"] = SpecifiedMutationsInfo
 286 
 287 
 288 def LoadMolecule(Infile, MolName):
 289     """Load input file."""
 290 
 291     pymol.cmd.reinitialize()
 292     pymol.cmd.load(Infile, MolName)
 293 
 294 
 295 def DeleteMolecule(MolName):
 296     """Delete molecule."""
 297 
 298     pymol.cmd.delete(MolName)
 299 
 300 
 301 def ProcessOptions():
 302     """Process and validate command line arguments and options."""
 303 
 304     MiscUtil.PrintInfo("Processing options...")
 305 
 306     # Validate options...
 307     ValidateOptions()
 308 
 309     OptionsInfo["Infile"] = Options["--infile"]
 310     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Infile"])
 311     OptionsInfo["InfileRoot"] = FileName
 312 
 313     OptionsInfo["Overwrite"] = Options["--overwrite"]
 314     OptionsInfo["Outfile"] = Options["--outfile"]
 315 
 316     RetrieveChainsIDs()
 317 
 318     Mutations = Options["--mutations"]
 319     if re.match("^None$", Mutations, re.I):
 320         MiscUtil.PrintError('No mutations specified using "-m, --mutations" option.')
 321 
 322     OptionsInfo["Mutations"] = Options["--mutations"]
 323     ProcessSpecifiedMutations()
 324 
 325 
 326 def RetrieveOptions():
 327     """Retrieve command line arguments and options."""
 328 
 329     # Get options...
 330     global Options
 331     Options = docopt(_docoptUsage_)
 332 
 333     # Set current working directory to the specified directory...
 334     WorkingDir = Options["--workingdir"]
 335     if WorkingDir:
 336         os.chdir(WorkingDir)
 337 
 338     # Handle examples option...
 339     if "--examples" in Options and Options["--examples"]:
 340         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 341         sys.exit(0)
 342 
 343 
 344 def ValidateOptions():
 345     """Validate option values."""
 346 
 347     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 348     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "pdb")
 349 
 350     MiscUtil.ValidateOptionsDistinctFileNames(
 351         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 352     )
 353     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "pdb")
 354     MiscUtil.ValidateOptionsOutputFileOverwrite(
 355         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 356     )
 357 
 358 
 359 # Setup a usage string for docopt...
 360 _docoptUsage_ = """
 361 PyMOLMutateNucleicAcids.py - Mutate nucleic acids
 362 
 363 Usage:
 364     PyMOLMutateNucleicAcids.py [--mutations <Spec1,Spec2,...>]
 365                             [--overwrite] [-w <dir>] -i <infile> -o <outfile>
 366     PyMOLMutateNucleicAcids.py -h | --help | -e | --examples
 367 
 368 Description:
 369     Mutate nucleic acids in macromolecules. The mutations are performed using
 370     nucleic acids mutagenesis wizard available in PyMOL starting V2.2.
 371 
 372     The supported input and output file format is: PDB (.pdb)
 373  
 374 Options:
 375     -m, --mutations <Spec1,Spec2,...>  [default: None]
 376         Comma delimited list of specifications for mutating nucleic acids.
 377         
 378         The format of mutation specification is as follows:
 379         
 380             <ChainID>:<ResNum><BaseName>,...
 381         
 382         A chain ID in the first specification of a mutation is required. It may be
 383         skipped in subsequent specifications. The most recent chain ID is used
 384         for the missing chain ID. The residue number corresponds to the residue
 385         to be mutated and must be present in the current chain. The base name
 386         represents the new base.
 387         
 388         Examples:
 389         
 390             A:9Thy, A:10Thy
 391             A:9Thy,10Thy,11Thy
 392             A:9Thy,10Thy,B:5Ade,6Ade
 393         
 394         The base names must be valid for mutating nucleic acids. No validation
 395         validation is performed before mutating residues via nucleic acids
 396         mutagenesis wizard available in PyMOL. The current version of the
 397         wizard supports the following base names:
 398         
 399             Adenine, Ade
 400             Cytosine, Cyt
 401             Guanine, Gua
 402             Thymine, Thy
 403             Uracil, Ura
 404         
 405     -e, --examples
 406         Print examples.
 407     -h, --help
 408         Print this help message.
 409     -i, --infile <infile>
 410         Input file name.
 411     -o, --outfile <outfile>
 412         Output file name.
 413     --overwrite
 414         Overwrite existing files.
 415     -w, --workingdir <dir>
 416         Location of working directory which defaults to the current directory.
 417 
 418 Examples:
 419     To mutate a single residue in a specific chain and write a PDB file, type:
 420 
 421         % PyMOLMutateNucleicAcids.py -m "A:9Thy" -i Sample9.pdb
 422           -o Sample9Out.pdb
 423 
 424     To mutate multiple residues in a single chain and write a PDB file, type:
 425 
 426         % PyMOLMutateNucleicAcids.py -m "A:9Thy,10Thy,11Thy" -i Sample9.pdb
 427           -o Sample9Out.pdb
 428 
 429     To mutate multiple residues across multiple chains and write a PDB file, type:
 430 
 431         % PyMOLMutateNucleicAcids.py -m "A:9Thy,10Thy,B:5Ade,6Ade"
 432           -i Sample9.pdb -o Sample9Out.pdb
 433 
 434 Author:
 435     Manish Sud(msud@san.rr.com)
 436 
 437 See also:
 438     DownloadPDBFiles.pl, PyMOLMutateAminoAcids.py,
 439     PyMOLVisualizeMacromolecules.py
 440 
 441 Copyright:
 442     Copyright (C) 2026 Manish Sud. All rights reserved.
 443 
 444     The functionality available in this script is implemented using PyMOL, a
 445     molecular visualization system on an open source foundation originally
 446     developed by Warren DeLano.
 447 
 448     This file is part of MayaChemTools.
 449 
 450     MayaChemTools is free software; you can redistribute it and/or modify it under
 451     the terms of the GNU Lesser General Public License as published by the Free
 452     Software Foundation; either version 3 of the License, or (at your option) any
 453     later version.
 454 
 455 """
 456 
 457 if __name__ == "__main__":
 458     main()