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()