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