MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: PyMOLSplitChainsAndLigands.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     SplitChainsAndLigands()
  84 
  85     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  86     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  87 
  88 
  89 def SplitChainsAndLigands():
  90     """Split input file into output files corresponding to chains and ligands."""
  91 
  92     MiscUtil.PrintInfo("\nGenerating output files...")
  93 
  94     # Load macromolecule from input file...
  95     MolName = OptionsInfo["InfileRoot"]
  96     pymol.cmd.load(OptionsInfo["Infile"], MolName)
  97 
  98     for ChainID in OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainIDs"]:
  99         ChainFile = OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainOutfiles"][ChainID]
 100         WriteChainFile(MolName, ChainID, ChainFile)
 101 
 102         for LigandID in OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandIDs"][ChainID]:
 103             LigandFile = OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandOutfiles"][ChainID][LigandID]
 104             WriteLigandFile(MolName, ChainID, LigandID, LigandFile)
 105 
 106     # Delete macromolecule...
 107     pymol.cmd.delete(MolName)
 108 
 109 
 110 def WriteChainFile(MolName, ChainID, ChainFile):
 111     """Write chain file."""
 112 
 113     MiscUtil.PrintInfo("\nGenerating output file %s..." % ChainFile)
 114 
 115     ChainName = "%s_Chain%s" % (MolName, ChainID)
 116 
 117     ChainSelection = "%s and (chain %s)" % (MolName, ChainID)
 118     if not OptionsInfo["ChainsMode"]:
 119         ChainSelection += " and (not organic)"
 120 
 121     if not OptionsInfo["KeepSolvents"]:
 122         ChainSelection += " and (not solvent)"
 123 
 124     if not OptionsInfo["KeepInorganics"]:
 125         ChainSelection += " and (not inorganic)"
 126 
 127     ChainSelection = "(%s)" % ChainSelection
 128     MiscUtil.PrintInfo("Chain selection: %s" % ChainSelection)
 129 
 130     pymol.cmd.create(ChainName, ChainSelection)
 131     pymol.cmd.save(ChainFile, ChainName)
 132     pymol.cmd.delete(ChainName)
 133 
 134     if not os.path.exists(ChainFile):
 135         MiscUtil.PrintWarning("Failed to generate Chain file, %s..." % (ChainFile))
 136 
 137 
 138 def WriteLigandFile(MolName, ChainID, LigandID, LigandFile):
 139     """Write ligand file."""
 140 
 141     MiscUtil.PrintInfo("\nGenerating output file %s..." % LigandFile)
 142 
 143     LigandName = "%s_Chain%s_%s" % (MolName, ChainID, LigandID)
 144     LigandSelection = "(%s and (chain %s) and organic and (resn %s))" % (MolName, ChainID, LigandID)
 145     MiscUtil.PrintInfo("Ligand selection: %s" % LigandSelection)
 146 
 147     pymol.cmd.create(LigandName, LigandSelection)
 148     pymol.cmd.save(LigandFile, LigandName)
 149     pymol.cmd.delete(LigandName)
 150 
 151     if not os.path.exists(LigandFile):
 152         MiscUtil.PrintWarning("Failed to generate ligand file, %s..." % (LigandFile))
 153 
 154 
 155 def ProcessChainAndLigandIDs():
 156     """Process chain and ligand IDs."""
 157 
 158     MolName = OptionsInfo["InfileRoot"]
 159     ChainsAndLigandsInfo = PyMOLUtil.GetChainsAndLigandsInfo(OptionsInfo["Infile"], MolName)
 160     OptionsInfo["ChainsAndLigandsInfo"] = ChainsAndLigandsInfo
 161 
 162     MiscUtil.PrintInfo("\nProcessing specified chain and ligand IDs for input file %s..." % OptionsInfo["Infile"])
 163 
 164     SpecifiedChainsAndLigandsInfo = PyMOLUtil.ProcessChainsAndLigandsOptionsInfo(
 165         ChainsAndLigandsInfo, "-c, --chainIDs", OptionsInfo["ChainIDs"], "-l, --ligandIDs", OptionsInfo["LigandIDs"]
 166     )
 167     OptionsInfo["SpecifiedChainsAndLigandsInfo"] = SpecifiedChainsAndLigandsInfo
 168 
 169     CheckPresenceOfValidLigandIDs(ChainsAndLigandsInfo, SpecifiedChainsAndLigandsInfo)
 170 
 171 
 172 def CheckPresenceOfValidLigandIDs(ChainsAndLigandsInfo, SpecifiedChainsAndLigandsInfo):
 173     """Check presence of valid ligand IDs."""
 174 
 175     MiscUtil.PrintInfo("\nSpecified chain IDs: %s" % (", ".join(SpecifiedChainsAndLigandsInfo["ChainIDs"])))
 176 
 177     for ChainID in SpecifiedChainsAndLigandsInfo["ChainIDs"]:
 178         if len(SpecifiedChainsAndLigandsInfo["LigandIDs"][ChainID]):
 179             MiscUtil.PrintInfo(
 180                 "Chain ID: %s; Specified LigandIDs: %s"
 181                 % (ChainID, ", ".join(SpecifiedChainsAndLigandsInfo["LigandIDs"][ChainID]))
 182             )
 183         else:
 184             MiscUtil.PrintInfo("Chain IDs: %s; Specified LigandIDs: None" % (ChainID))
 185             MiscUtil.PrintWarning("No valid ligand IDs found for chain ID, %s." % (ChainID))
 186 
 187 
 188 def SetupChainAndLigandOutfiles():
 189     """Setup output file names for chains and ligands."""
 190 
 191     OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainOutfiles"] = {}
 192     OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandOutfiles"] = {}
 193 
 194     InfileRoot = OptionsInfo["InfileRoot"]
 195     LigandFileExt = OptionsInfo["LigandFileExt"]
 196 
 197     for ChainID in OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainIDs"]:
 198         ChainOutfileRoot = "%s_Chain%s" % (InfileRoot, ChainID)
 199         ChainOutfile = "%s.pdb" % (ChainOutfileRoot)
 200         OptionsInfo["SpecifiedChainsAndLigandsInfo"]["ChainOutfiles"][ChainID] = ChainOutfile
 201         if os.path.exists(ChainOutfile):
 202             if not OptionsInfo["Overwrite"]:
 203                 MiscUtil.PrintError(
 204                     'The chain output file, %s, already exist. Use option "--ov" or "--overwrite" and try again.\n'
 205                     % (ChainOutfile)
 206                 )
 207 
 208         OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandOutfiles"][ChainID] = {}
 209         for LigandID in OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandIDs"][ChainID]:
 210             LigandOutfile = "%s_%s.%s" % (ChainOutfileRoot, LigandID, LigandFileExt)
 211             OptionsInfo["SpecifiedChainsAndLigandsInfo"]["LigandOutfiles"][ChainID][LigandID] = LigandOutfile
 212             if os.path.exists(LigandOutfile):
 213                 if not OptionsInfo["Overwrite"]:
 214                     MiscUtil.PrintError(
 215                         'The ligand output file, %s, already exist. Use option "--ov" or "--overwrite" and try again.\n'
 216                         % (LigandOutfile)
 217                     )
 218 
 219 
 220 def ProcessOptions():
 221     """Process and validate command line arguments and options."""
 222 
 223     MiscUtil.PrintInfo("Processing options...")
 224 
 225     # Validate options...
 226     ValidateOptions()
 227 
 228     OptionsInfo["Mode"] = Options["--mode"]
 229     OptionsInfo["ChainsMode"] = False
 230     if re.match("^Chains$", OptionsInfo["Mode"], re.I):
 231         OptionsInfo["ChainsMode"] = True
 232 
 233     OptionsInfo["LigandFileFormat"] = Options["--ligandFileFormat"]
 234     LigandFileExt = "mol"
 235     if re.match("^PDB$", OptionsInfo["LigandFileFormat"], re.I):
 236         LigandFileExt = "pdb"
 237     elif re.match("^(SD|SDF)$", OptionsInfo["LigandFileFormat"], re.I):
 238         LigandFileExt = "sdf"
 239     elif re.match("^MOL$", OptionsInfo["LigandFileFormat"], re.I):
 240         LigandFileExt = "mol"
 241     OptionsInfo["LigandFileExt"] = LigandFileExt
 242 
 243     OptionsInfo["KeepInorganics"] = True if re.match("^Yes$", Options["--keepInorganics"], re.I) else False
 244     OptionsInfo["KeepSolvents"] = True if re.match("^Yes$", Options["--keepSolvents"], re.I) else False
 245 
 246     OptionsInfo["Infile"] = Options["--infile"]
 247     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Infile"])
 248     OptionsInfo["InfileRoot"] = FileName
 249 
 250     OptionsInfo["Overwrite"] = Options["--overwrite"]
 251 
 252     OptionsInfo["ChainIDs"] = Options["--chainIDs"]
 253     OptionsInfo["LigandIDs"] = Options["--ligandIDs"]
 254     ProcessChainAndLigandIDs()
 255 
 256     SetupChainAndLigandOutfiles()
 257 
 258 
 259 def RetrieveOptions():
 260     """Retrieve command line arguments and options."""
 261 
 262     # Get options...
 263     global Options
 264     Options = docopt(_docoptUsage_)
 265 
 266     # Set current working directory to the specified directory...
 267     WorkingDir = Options["--workingdir"]
 268     if WorkingDir:
 269         os.chdir(WorkingDir)
 270 
 271     # Handle examples option...
 272     if "--examples" in Options and Options["--examples"]:
 273         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 274         sys.exit(0)
 275 
 276 
 277 def ValidateOptions():
 278     """Validate option value.s"""
 279 
 280     MiscUtil.ValidateOptionTextValue("--ligandFileFormat", Options["--ligandFileFormat"], "PDB SDF SD MDLMOL")
 281 
 282     MiscUtil.ValidateOptionTextValue("-m, --mode", Options["--mode"], "Chains ChainsLigands")
 283 
 284     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 285     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "pdb cif")
 286 
 287     MiscUtil.ValidateOptionTextValue("--keepInorganics", Options["--keepInorganics"], "yes no")
 288     MiscUtil.ValidateOptionTextValue("--keepSolvents", Options["--keepSolvents"], "yes no")
 289 
 290 
 291 # Setup a usage string for docopt...
 292 _docoptUsage_ = """
 293 PyMOLSplitChainsAndLigands.py - Split macromolecule into chains and ligands
 294 
 295 Usage:
 296     PyMOLSplitChainsAndLigands.py [--chainIDs <First, All or ID1,ID2...>]
 297                                   [--ligandIDs <Largest, All or ID1,ID2...>] [--ligandFileFormat <PDB, SDF, MDLMOL>]
 298                                   [--mode <Chains or ChainsLigands>] [--keepInorganics <yes or no>]
 299                                   [--keepSolvents <yes or no>] [--overwrite] [-w <dir>] -i <infile>
 300     PyMOLSplitChainsAndLigands.py -h | --help | -e | --examples
 301 
 302 Description:
 303     Spit a macromolecule into chains and ligands, and write them out to different
 304     files. The solvents and inorganic molecules may be optionally removed from
 305     chains. You may also skip the generation of ligand files and write out a chain
 306     along with associated ligands into the same chain file.
 307  
 308     The supported input file format is:  PDB (.pdb), CIF (.cif)
 309  
 310     The supported output file formats are: Chains - PDB (.pdb); Ligands: PDB (.pdb),
 311     SD file (.sdf, .sd), MDL MOL (.mol)
 312 
 313     The names of the output files are automatically generated from the name of
 314     input file as shown below:
 315     
 316         Chains: <InfileRoot>_<ChainID>.pdb
 317         Ligands: <InfileRoot>_<ChainID>.{pdb,sdf,sd,mol}
 318     
 319 Options:
 320     -c, --chainIDs <First, All or ID1,ID2...>  [default: All]
 321         List of chain IDs for splitting input file. Possible values: First, All,
 322         or a comma delimited list of chain IDs. The default is to use
 323         all chain IDs in input file.
 324     -e, --examples
 325         Print examples.
 326     -h, --help
 327         Print this help message.
 328     -i, --infile <infile>
 329         Input file name.
 330     -l, --ligandIDs <Largest, All or ID1,ID2...>  [default: Largest]
 331         List of ligand IDs present in chains for splitting input file. Possible
 332         values: Largest, All, or a comma delimited list of ligand IDs. The default
 333         is to use the largest ligand present in all or specified chains in input file.
 334         This option is ignored during 'Chains' value of '--mode' option.
 335         
 336         Ligands are identified using organic selection operator available in PyMOL.
 337         It'll also  identify buffer molecules as ligands. The largest ligand contains
 338         the highest number of heavy atoms.
 339     --ligandFileFormat <PDB, SDF, MDLMOL>  [default: SDF]
 340         Ligand file format.
 341     -m, --mode <Chains or ChainsLigands>  [default: ChainsLigands]
 342         Split input file into chains or chains and ligands. The ligands are kept
 343         together with chains in the output files for 'Chains' mode. Separate files
 344         are generated for ligands during 'ChainsAndLigands' mode.
 345     --keepInorganics <yes or no>  [default: yes]
 346         Keep inorganic molecules during splitting of input file and write them to
 347         output files. The inorganic molecules are identified using inorganic selection
 348         operator available in PyMOL.
 349     --keepSolvents <yes or no>  [default: yes]
 350         Keep solvent molecules during splitting of input file and write them to
 351         output files. The solvent molecules are identified using solvent selection
 352         operator available in PyMOL.
 353     --overwrite
 354         Overwrite existing files.
 355     -w, --workingdir <dir>
 356         Location of working directory which defaults to the current directory.
 357 
 358 Examples:
 359     To split a macromolecule into the first chain and the largest ligand in the
 360     first chain along with solvent and inorganic molecules, and write chain PDB
 361     and ligand SDF files, type:
 362 
 363         % PyMOLSplitChainsAndLigands.py -i Sample3.pdb
 364 
 365     To split a macromolecule into all chains and all ligands across all chains
 366     along with solvent and inorganic molecules, and write out corresponding
 367     chain and ligand files, type:
 368 
 369         % PyMOLSplitChainsAndLigands.py -i Sample3.pdb -c All -l All
 370 
 371     To split a macromolecule into all chains along with any associated ligands
 372     without any solvent and inorganic molecules, and write corresponding
 373     PDB files for chains and skipping generation of any ligand files, type:
 374 
 375         % PyMOLSplitChainsAndLigands.py -c all -m Chains --keepSolvents no
 376           --keepInorganics no -i Sample3.pdb
 377 
 378     To split a macromolecule into a specific chain and a specific ligand in the
 379     chain along with solvent and inorganic molecules, and write chain PDB
 380     and ligand MDLMOL files, type:
 381 
 382         % PyMOLSplitChainsAndLigands.py -c E -l ADP --ligandFileFormat MDLMOL
 383           -i Sample3.pdb 
 384 
 385 Author:
 386     Manish Sud(msud@san.rr.com)
 387 
 388 See also:
 389     PyMOLAlignChains.py, PyMOLVisualizeMacromolecules.py
 390 
 391 Copyright:
 392     Copyright (C) 2026 Manish Sud. All rights reserved.
 393 
 394     The functionality available in this script is implemented using PyMOL, a
 395     molecular visualization system on an open source foundation originally
 396     developed by Warren DeLano.
 397 
 398     This file is part of MayaChemTools.
 399 
 400     MayaChemTools is free software; you can redistribute it and/or modify it under
 401     the terms of the GNU Lesser General Public License as published by the Free
 402     Software Foundation; either version 3 of the License, or (at your option) any
 403     later version.
 404 
 405 """
 406 
 407 if __name__ == "__main__":
 408     main()