MayaChemTools

   1 #!/bin/env python
   2 #
   3 # File: OpenMMPrepareMacromolecule.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 OpenMM, an
   9 # open source molecuar simulation package.
  10 #
  11 # This file is part of MayaChemTools.
  12 #
  13 # MayaChemTools is free software; you can redistribute it and/or modify it under
  14 # the terms of the GNU Lesser General Public License as published by the Free
  15 # Software Foundation; either version 3 of the License, or (at your option) any
  16 # later version.
  17 #
  18 # MayaChemTools is distributed in the hope that it will be useful, but without
  19 # any warranty; without even the implied warranty of merchantability of fitness
  20 # for a particular purpose.  See the GNU Lesser General Public License for more
  21 # details.
  22 #
  23 # You should have received a copy of the GNU Lesser General Public License
  24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
  25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
  26 # Boston, MA, 02111-1307, USA.
  27 #
  28 
  29 from __future__ import print_function
  30 
  31 import os
  32 import sys
  33 import time
  34 import re
  35 
  36 # OpenMM imports...
  37 try:
  38     import openmm as mm
  39     import pdbfixer
  40     from pdbfixer.pdbfixer import __version__ as pdbfixerversion
  41 except ImportError as ErrMsg:
  42     sys.stderr.write("\nFailed to import OpenMM related module/package: %s\n" % ErrMsg)
  43     sys.stderr.write("Check/update your OpenMM environment and try again.\n\n")
  44     sys.exit(1)
  45 
  46 # MayaChemTools imports...
  47 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "lib", "Python"))
  48 try:
  49     from docopt import docopt
  50     import MiscUtil
  51     import OpenMMUtil
  52 except ImportError as ErrMsg:
  53     sys.stderr.write("\nFailed to import MayaChemTools module/package: %s\n" % ErrMsg)
  54     sys.stderr.write("Check/update your MayaChemTools environment and try again.\n\n")
  55     sys.exit(1)
  56 
  57 ScriptName = os.path.basename(sys.argv[0])
  58 Options = {}
  59 OptionsInfo = {}
  60 
  61 
  62 def main():
  63     """Start execution of the script."""
  64 
  65     MiscUtil.PrintInfo(
  66         "\n%s (OpenMM v%s; PDBFixerVersion v%s; MayaChemTools v%s; %s): Starting...\n"
  67         % (
  68             ScriptName,
  69             mm.Platform.getOpenMMVersion(),
  70             pdbfixerversion,
  71             MiscUtil.GetMayaChemToolsVersion(),
  72             time.asctime(),
  73         )
  74     )
  75 
  76     (WallClockTime, ProcessorTime) = MiscUtil.GetWallClockAndProcessorTime()
  77 
  78     # Retrieve command line arguments and options...
  79     RetrieveOptions()
  80 
  81     # Process and validate command line arguments and options...
  82     ProcessOptions()
  83 
  84     # Perform actions required by the script...
  85     PrepareMacromolecule()
  86 
  87     MiscUtil.PrintInfo("\n%s: Done...\n" % ScriptName)
  88     MiscUtil.PrintInfo("Total time: %s" % MiscUtil.GetFormattedElapsedTime(WallClockTime, ProcessorTime))
  89 
  90 
  91 def PrepareMacromolecule():
  92     """Prepare a macromolecule for simulation and write it out."""
  93 
  94     # Read macromolecule...
  95     PDBFixerObjectHandle = ReadMacromolecule()
  96 
  97     # Identify missing residues...
  98     IdentifyMissingResidues(PDBFixerObjectHandle)
  99 
 100     # Identify and replace non-standard residues...
 101     IdentifyAndReplaceNonStandardResidues(PDBFixerObjectHandle)
 102 
 103     # Identify and delete heterogen residues...
 104     IdentifyAndDeleteHeterogenResidues(PDBFixerObjectHandle)
 105 
 106     # Identify and add missing atoms...
 107     IdentifyAndAddMissingAtoms(PDBFixerObjectHandle)
 108 
 109     if OptionsInfo["WaterBox"]:
 110         AddWaterBox(PDBFixerObjectHandle)
 111     elif OptionsInfo["Membrane"]:
 112         AddLipidMembrane(PDBFixerObjectHandle)
 113     else:
 114         MiscUtil.PrintInfo("\nSkipping addtion of water box or membrane...")
 115 
 116     # Write macromolecule...
 117     WriteMacromolecule(PDBFixerObjectHandle)
 118 
 119 
 120 def ReadMacromolecule():
 121     """Read macromolecule."""
 122 
 123     Infile = OptionsInfo["Infile"]
 124     MiscUtil.PrintInfo("\nProcessing file %s..." % Infile)
 125 
 126     PDBFixerObjectHandle = pdbfixer.pdbfixer.PDBFixer(filename=Infile)
 127 
 128     return PDBFixerObjectHandle
 129 
 130 
 131 def WriteMacromolecule(PDBFixerObjectHandle):
 132     """Write macromolecule."""
 133 
 134     MiscUtil.PrintInfo("\nGenerating output file %s..." % OptionsInfo["Outfile"])
 135     OpenMMUtil.WritePDBFile(
 136         OptionsInfo["Outfile"], PDBFixerObjectHandle.topology, PDBFixerObjectHandle.positions, KeepIDs=True
 137     )
 138 
 139 
 140 def IdentifyMissingResidues(PDBFixerObjectHandle):
 141     """Identify missing residues based on PDB REMARK records."""
 142 
 143     MiscUtil.PrintInfo("\nIdentifying missing residues...")
 144 
 145     PDBFixerObjectHandle.missingResidues = {}
 146     PDBFixerObjectHandle.findMissingResidues()
 147 
 148     ListMissingResidues(PDBFixerObjectHandle)
 149 
 150 
 151 def ListMissingResidues(PDBFixerObjectHandle):
 152     """List missing residues."""
 153 
 154     if len(PDBFixerObjectHandle.missingResidues) == 0:
 155         MiscUtil.PrintInfo("Number of missing residues: 0")
 156         return
 157 
 158     Chains = list(PDBFixerObjectHandle.topology.chains())
 159 
 160     MissingResiduesCount = 0
 161     MissingResiduesInfo = []
 162     for Index, Key in enumerate(sorted(PDBFixerObjectHandle.missingResidues)):
 163         ChainIndex = Key[0]
 164         # ResidueIndex corresponds to the residue after the insertion of missing residues...`
 165         ResidueIndex = Key[1]
 166 
 167         MissingResidues = PDBFixerObjectHandle.missingResidues[Key]
 168         MissingResiduesCount += len(MissingResidues)
 169 
 170         Chain = Chains[ChainIndex]
 171         ChainResidues = list(Chain.residues())
 172 
 173         if ResidueIndex < len(ChainResidues):
 174             ResidueNumOffset = int(ChainResidues[ResidueIndex].id) - len(MissingResidues) - 1
 175         else:
 176             ResidueNumOffset = int(ChainResidues[-1].id)
 177 
 178         StartResNum = ResidueNumOffset + 1
 179         EndResNum = ResidueNumOffset + len(MissingResidues)
 180 
 181         MissingResiduesInfo.append(
 182             "Chain: %s; StartResNum-EndResNum: %s-%s; Count: %s; Residues: %s"
 183             % (Chain.id, StartResNum, EndResNum, len(MissingResidues), ", ".join(MissingResidues))
 184         )
 185 
 186     MiscUtil.PrintInfo("Total number of missing residues: %s" % (MissingResiduesCount))
 187     if OptionsInfo["ListDetails"]:
 188         MiscUtil.PrintInfo("%s" % ("\n".join(MissingResiduesInfo)))
 189 
 190 
 191 def IdentifyAndReplaceNonStandardResidues(PDBFixerObjectHandle):
 192     """Identify and replace non-standard residues."""
 193 
 194     MiscUtil.PrintInfo("\nIdentifying non-standard residues...")
 195 
 196     PDBFixerObjectHandle.findNonstandardResidues()
 197     NonStandardResidues = PDBFixerObjectHandle.nonstandardResidues
 198     NumNonStandardResiues = len(NonStandardResidues)
 199 
 200     MiscUtil.PrintInfo("Total number of non-standard residues: %s" % NumNonStandardResiues)
 201 
 202     if NumNonStandardResiues == 0:
 203         return
 204 
 205     if OptionsInfo["ListDetails"]:
 206         ListNonStandardResidues(PDBFixerObjectHandle)
 207 
 208     if not OptionsInfo["ReplaceNonStandardResidues"]:
 209         MiscUtil.PrintInfo("Skipping replacement of non-standard residues...")
 210         return
 211 
 212     MiscUtil.PrintInfo("Replacing non-standard residues...")
 213     PDBFixerObjectHandle.replaceNonstandardResidues()
 214 
 215 
 216 def ListNonStandardResidues(PDBFixerObjectHandle):
 217     """List non-standard residues."""
 218 
 219     NonStandardResidues = PDBFixerObjectHandle.nonstandardResidues
 220 
 221     if len(NonStandardResidues) == 0:
 222         return
 223 
 224     NonStandardResiduesMappingInfo = []
 225     for Values in NonStandardResidues:
 226         NonStandardRes = Values[0]
 227         StandardResName = Values[1]
 228 
 229         MappingInfo = "<%s %s %s>: %s" % (
 230             NonStandardRes.id,
 231             NonStandardRes.name,
 232             NonStandardRes.chain.id,
 233             StandardResName,
 234         )
 235         NonStandardResiduesMappingInfo.append(MappingInfo)
 236 
 237     if len(NonStandardResiduesMappingInfo):
 238         MiscUtil.PrintInfo("Non-standard residues mapping: %s\n" % ",".join(NonStandardResiduesMappingInfo))
 239 
 240 
 241 def IdentifyAndDeleteHeterogenResidues(PDBFixerObjectHandle):
 242     """Identify and delete heterogen residues."""
 243 
 244     DeleteHeterogens = OptionsInfo["DeleteHeterogens"]
 245     if DeleteHeterogens is None:
 246         MiscUtil.PrintInfo("\nSkipping deletion of any heterogen residues...")
 247         return
 248 
 249     if re.match("^All$", DeleteHeterogens, re.I):
 250         MiscUtil.PrintInfo("\nDeleting all heterogen residues including water...")
 251         PDBFixerObjectHandle.removeHeterogens(keepWater=False)
 252     elif re.match("^AllExceptWater$", DeleteHeterogens, re.I):
 253         MiscUtil.PrintInfo("\nDeleting all heterogen residues except water...")
 254         PDBFixerObjectHandle.removeHeterogens(keepWater=True)
 255     elif re.match("^WaterOnly$", DeleteHeterogens, re.I):
 256         MiscUtil.PrintInfo("\nDeleting water only heterogen residues...")
 257         DeleteWater(PDBFixerObjectHandle)
 258     else:
 259         MiscUtil.PrintError(
 260             'The value specified, %s, for option "--deleteHeterogens" is not valid. Supported values: All AllExceptWater WaterOnly None'
 261             % DeleteHeterogens
 262         )
 263 
 264 
 265 def DeleteWater(PDBFixerObjectHandle):
 266     """Delete water."""
 267 
 268     ModellerObjectHandle = mm.app.Modeller(PDBFixerObjectHandle.topology, PDBFixerObjectHandle.positions)
 269     ModellerObjectHandle.deleteWater()
 270 
 271     PDBFixerObjectHandle.topology = ModellerObjectHandle.topology
 272     PDBFixerObjectHandle.positions = ModellerObjectHandle.positions
 273 
 274 
 275 def IdentifyAndAddMissingAtoms(PDBFixerObjectHandle):
 276     """Identify and missing atoms along with already identified missing residues."""
 277 
 278     MiscUtil.PrintInfo("\nIdentifying missing atoms...")
 279     PDBFixerObjectHandle.findMissingAtoms()
 280 
 281     ListMissingAtoms(PDBFixerObjectHandle)
 282 
 283     if OptionsInfo["AddHeavyAtoms"] and OptionsInfo["AddResidues"]:
 284         Msg = "Adding missing heavy atoms along with missing residues..."
 285     elif OptionsInfo["AddHeavyAtoms"]:
 286         Msg = "Adding missing heavy atoms..."
 287     elif OptionsInfo["AddResidues"]:
 288         Msg = "Adding missing residues..."
 289     else:
 290         Msg = "Skipping addition of any heavy atoms along with any missing residues..."
 291 
 292     if not OptionsInfo["AddHeavyAtoms"]:
 293         PDBFixerObjectHandle.missingAtoms = {}
 294         PDBFixerObjectHandle.missingTerminals = {}
 295 
 296     MiscUtil.PrintInfo("\n%s" % Msg)
 297     if OptionsInfo["AddHeavyAtoms"] or OptionsInfo["AddResidues"]:
 298         PDBFixerObjectHandle.addMissingAtoms()
 299 
 300     if OptionsInfo["AddHydrogens"]:
 301         MiscUtil.PrintInfo("Adding missing hydrogens at pH %s..." % OptionsInfo["AddHydrogensAtpH"])
 302         PDBFixerObjectHandle.addMissingHydrogens(pH=OptionsInfo["AddHydrogensAtpH"], forcefield=None)
 303     else:
 304         MiscUtil.PrintInfo("Skipping addition of any missing hydrogens...")
 305 
 306 
 307 def ListMissingAtoms(PDBFixerObjectHandle):
 308     """List missing atoms."""
 309 
 310     if len(PDBFixerObjectHandle.missingAtoms) == 0 and len(PDBFixerObjectHandle.missingTerminals) == 0:
 311         MiscUtil.PrintInfo("Total number of missing atoms: 0")
 312         return
 313 
 314     ListMissingAtomsInfo(PDBFixerObjectHandle.missingAtoms, TerminalAtoms=False)
 315     ListMissingAtomsInfo(PDBFixerObjectHandle.missingTerminals, TerminalAtoms=True)
 316 
 317 
 318 def ListMissingAtomsInfo(MissingAtoms, TerminalAtoms):
 319     """Setup missing atoms information."""
 320 
 321     MissingAtomsInfo = []
 322     MissingAtomsCount = 0
 323     MissingAtomsResiduesCount = 0
 324     for Residue, Atoms in MissingAtoms.items():
 325         MissingAtomsResiduesCount += 1
 326         MissingAtomsCount += len(Atoms)
 327 
 328         if TerminalAtoms:
 329             AtomsNames = [AtomName for AtomName in Atoms]
 330         else:
 331             AtomsNames = [Atom.name for Atom in Atoms]
 332         AtomsInfo = "<%s %s %s>: %s" % (Residue.id, Residue.name, Residue.chain.id, ", ".join(AtomsNames))
 333         MissingAtomsInfo.append(AtomsInfo)
 334 
 335     AtomsLabel = "terminal atoms" if TerminalAtoms else "atoms"
 336     if MissingAtomsCount == 0:
 337         MiscUtil.PrintInfo("Total number of missing %s: %s" % (MissingAtomsCount, AtomsLabel))
 338     else:
 339         ResiduesLabel = "residue" if MissingAtomsResiduesCount == 1 else "residues"
 340         MiscUtil.PrintInfo(
 341             "Total number of missing %s across %s %s: %s"
 342             % (AtomsLabel, MissingAtomsResiduesCount, ResiduesLabel, MissingAtomsCount)
 343         )
 344         if OptionsInfo["ListDetails"]:
 345             MiscUtil.PrintInfo("Missing %s across residues: %s" % (AtomsLabel, "; ".join(MissingAtomsInfo)))
 346 
 347 
 348 def AddWaterBox(PDBFixerObjectHandle):
 349     """Add water box."""
 350 
 351     MiscUtil.PrintInfo("\nAdding water box...")
 352 
 353     WaterBoxParams = OptionsInfo["WaterBoxParams"]
 354 
 355     Size, Padding, Shape = [None] * 3
 356     if WaterBoxParams["ModeSize"]:
 357         SizeList = WaterBoxParams["SizeList"]
 358         Size = mm.Vec3(SizeList[0], SizeList[1], SizeList[2]) * mm.unit.nanometer
 359     elif WaterBoxParams["ModePadding"]:
 360         Padding = WaterBoxParams["Padding"] * mm.unit.nanometer
 361         Shape = WaterBoxParams["Shape"]
 362     else:
 363         MiscUtil.PrintError(
 364             'The parameter value, %s, specified for parameter name, mode, using "--waterBoxParams" option is not a valid value. Supported values: Size Padding \n'
 365             % (WaterBoxParams["Mode"])
 366         )
 367 
 368     IonicStrength = OptionsInfo["IonicStrength"] * mm.unit.molar
 369     PDBFixerObjectHandle.addSolvent(
 370         boxSize=Size,
 371         padding=Padding,
 372         boxShape=Shape,
 373         positiveIon=OptionsInfo["IonPositive"],
 374         negativeIon=OptionsInfo["IonNegative"],
 375         ionicStrength=IonicStrength,
 376     )
 377 
 378 
 379 def AddLipidMembrane(PDBFixerObjectHandle):
 380     """Add lipid membrane along with water."""
 381 
 382     MiscUtil.PrintInfo("\nAdding membrane along with water...")
 383 
 384     MembraneParams = OptionsInfo["MembraneParams"]
 385 
 386     LipidType = MembraneParams["LipidType"]
 387     MembraneCenterZ = MembraneParams["MembraneCenterZ"] * mm.unit.nanometer
 388     Padding = MembraneParams["Padding"] * mm.unit.nanometer
 389 
 390     IonicStrength = OptionsInfo["IonicStrength"] * mm.unit.molar
 391 
 392     PDBFixerObjectHandle.addMembrane(
 393         lipidType=LipidType,
 394         membraneCenterZ=MembraneCenterZ,
 395         minimumPadding=Padding,
 396         positiveIon=OptionsInfo["IonPositive"],
 397         negativeIon=OptionsInfo["IonNegative"],
 398         ionicStrength=IonicStrength,
 399     )
 400 
 401 
 402 def ProcessMembraneParamsOption():
 403     """Process option for membrane parameters."""
 404 
 405     ParamsOptionName = "--membraneParams"
 406     ParamsOptionValue = Options[ParamsOptionName]
 407     ParamsDefaultInfo = {"LipidType": ["str", "POPC"], "MembraneCenterZ": ["float", 0.0], "Padding": ["float", 1.0]}
 408     MembraneParams = MiscUtil.ProcessOptionNameValuePairParameters(
 409         ParamsOptionName, ParamsOptionValue, ParamsDefaultInfo
 410     )
 411 
 412     for ParamName in ["LipidType", "MembraneCenterZ", "Padding"]:
 413         ParamValue = MembraneParams[ParamName]
 414         if ParamName == "LipidType":
 415             LipidTypes = ["POPC", "POPE", "DLPC", "DLPE", "DMPC", "DOPC", "DPPC"]
 416             if ParamValue not in LipidTypes:
 417                 MiscUtil.PrintError(
 418                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: %s \n'
 419                     % (ParamValue, ParamName, ParamsOptionName, LipidTypes)
 420                 )
 421         elif ParamName == "Padding":
 422             if ParamValue <= 0:
 423                 MiscUtil.PrintError(
 424                     'The parameter value, %s, specified for parameter name, %s, using "%s" option is not a valid value. Supported values: > 0\n'
 425                     % (ParamValue, ParamName, ParamsOptionName)
 426                 )
 427 
 428     OptionsInfo["MembraneParams"] = MembraneParams
 429 
 430 
 431 def ProcessOptions():
 432     """Process and validate command line arguments and options."""
 433 
 434     MiscUtil.PrintInfo("Processing options...")
 435 
 436     # Validate options...
 437     ValidateOptions()
 438 
 439     OptionsInfo["Infile"] = Options["--infile"]
 440     FileDir, FileName, FileExt = MiscUtil.ParseFileName(OptionsInfo["Infile"])
 441     OptionsInfo["InfileRoot"] = FileName
 442 
 443     OptionsInfo["Outfile"] = Options["--outfile"]
 444 
 445     OptionsInfo["AddHeavyAtoms"] = True if re.match("^yes$", Options["--addHeavyAtoms"], re.I) else False
 446 
 447     OptionsInfo["AddHydrogens"] = True if re.match("^yes$", Options["--addHydrogens"], re.I) else False
 448     OptionsInfo["AddHydrogensAtpH"] = float(Options["--addHydrogensAtpH"])
 449 
 450     OptionsInfo["AddResidues"] = True if re.match("^yes$", Options["--addResidues"], re.I) else False
 451 
 452     DeleteHeterogens = Options["--deleteHeterogens"]
 453     if re.match("^All$", DeleteHeterogens, re.I):
 454         DeleteHeterogens = "All"
 455     elif re.match("^AllExceptWater$", DeleteHeterogens, re.I):
 456         DeleteHeterogens = "AllExceptWater"
 457     elif re.match("^WaterOnly$", DeleteHeterogens, re.I):
 458         DeleteHeterogens = "WaterOnly"
 459     elif re.match("^None$", DeleteHeterogens, re.I):
 460         DeleteHeterogens = None
 461     elif re.match("^auto$", DeleteHeterogens, re.I):
 462         DeleteHeterogens = None
 463         if re.match("^yes$", Options["--membrane"], re.I) or re.match("^yes$", Options["--waterBox"], re.I):
 464             DeleteHeterogens = "WaterOnly"
 465     else:
 466         MiscUtil.PrintError(
 467             'The value specified, %s, for option "--deleteHeterogens" is not valid. Supported values: All AllExceptWater WaterOnly None'
 468             % DeleteHeterogens
 469         )
 470     OptionsInfo["DeleteHeterogens"] = DeleteHeterogens
 471 
 472     OptionsInfo["IonPositive"] = Options["--ionPositive"]
 473     OptionsInfo["IonNegative"] = Options["--ionNegative"]
 474     OptionsInfo["IonicStrength"] = float(Options["--ionicStrength"])
 475 
 476     OptionsInfo["ListDetails"] = True if re.match("^yes$", Options["--listDetails"], re.I) else False
 477 
 478     OptionsInfo["Membrane"] = True if re.match("^yes$", Options["--membrane"], re.I) else False
 479     ProcessMembraneParamsOption()
 480 
 481     OptionsInfo["ReplaceNonStandardResidues"] = (
 482         True if re.match("^yes$", Options["--replaceNonStandardResidues"], re.I) else False
 483     )
 484 
 485     OptionsInfo["WaterBox"] = True if re.match("^yes$", Options["--waterBox"], re.I) else False
 486     OptionsInfo["WaterBoxParams"] = OpenMMUtil.ProcessOptionOpenMMWaterBoxParameters(
 487         "--waterBoxParams", Options["--waterBoxParams"]
 488     )
 489 
 490     OptionsInfo["Overwrite"] = Options["--overwrite"]
 491 
 492 
 493 def RetrieveOptions():
 494     """Retrieve command line arguments and options."""
 495 
 496     # Get options...
 497     global Options
 498     Options = docopt(_docoptUsage_)
 499 
 500     # Set current working directory to the specified directory...
 501     WorkingDir = Options["--workingdir"]
 502     if WorkingDir:
 503         os.chdir(WorkingDir)
 504 
 505     # Handle examples option...
 506     if "--examples" in Options and Options["--examples"]:
 507         MiscUtil.PrintInfo(MiscUtil.GetExamplesTextFromDocOptText(_docoptUsage_))
 508         sys.exit(0)
 509 
 510 
 511 def ValidateOptions():
 512     """Validate option values."""
 513 
 514     MiscUtil.ValidateOptionFilePath("-i, --infile", Options["--infile"])
 515     MiscUtil.ValidateOptionFileExt("-i, --infile", Options["--infile"], "pdb cif")
 516 
 517     MiscUtil.ValidateOptionFileExt("-o, --outfile", Options["--outfile"], "pdb cif")
 518     MiscUtil.ValidateOptionsOutputFileOverwrite(
 519         "-o, --outfile", Options["--outfile"], "--overwrite", Options["--overwrite"]
 520     )
 521     MiscUtil.ValidateOptionsDistinctFileNames(
 522         "-i, --infile", Options["--infile"], "-o, --outfile", Options["--outfile"]
 523     )
 524 
 525     MiscUtil.ValidateOptionTextValue("--addHeavyAtoms", Options["--addHeavyAtoms"], "yes no")
 526 
 527     MiscUtil.ValidateOptionTextValue("--addHydrogens", Options["--addHydrogens"], "yes no")
 528     MiscUtil.ValidateOptionFloatValue("--addHydrogensAtpH", Options["--addHydrogensAtpH"], {">=": 0})
 529 
 530     MiscUtil.ValidateOptionTextValue("--addResidues", Options["--addResidues"], "yes no")
 531 
 532     MiscUtil.ValidateOptionTextValue(
 533         "--deleteHeterogens", Options["--deleteHeterogens"], "All AllExceptWater WaterOnly None auto"
 534     )
 535     DeleteHeterogens = Options["--deleteHeterogens"]
 536     if re.match("^(AllExceptWater|None)$", DeleteHeterogens, re.I):
 537         if re.match("^yes$", Options["--membrane"], re.I):
 538             MiscUtil.PrintError(
 539                 'The value specified, %s, for option "--deleteHeterogens" is not valid during "yes" value of option "--membrane". '
 540                 % DeleteHeterogens
 541             )
 542         elif re.match("^yes$", Options["--waterBox"], re.I):
 543             MiscUtil.PrintError(
 544                 'The value specified, %s, for option "--deleteHeterogens" is not valid during "yes" value of option "--waterBox". '
 545                 % DeleteHeterogens
 546             )
 547 
 548     ValidValues = "Li+ Na+ K+ Rb+ Cs+"
 549     EscapedValidValuesPattern = r"Li\+|Na\+|K\+|Rb\+|Cs\+"
 550     Value = Options["--ionPositive"]
 551     if not re.match("^(%s)$" % EscapedValidValuesPattern, Value):
 552         MiscUtil.PrintError(
 553             'The value specified, %s, for option "-ionPositive" is not valid: Supported value(s): %s'
 554             % (Value, ValidValues)
 555         )
 556 
 557     ValidValues = "F- Cl- Br- I-"
 558     ValidValuesPattern = "F-|Cl-|Br-|I-"
 559     Value = Options["--ionNegative"]
 560     if not re.match("^(%s)$" % ValidValuesPattern, Value):
 561         MiscUtil.PrintError(
 562             'The value specified, %s, for option "-ionNegative" is not valid: Supported value(s): %s'
 563             % (Value, ValidValues)
 564         )
 565 
 566     MiscUtil.ValidateOptionFloatValue("--ionicStrength", Options["--ionicStrength"], {">=": 0})
 567 
 568     MiscUtil.ValidateOptionTextValue("--listDetails", Options["--listDetails"], "yes no")
 569     MiscUtil.ValidateOptionTextValue("--replaceNonStandardResidues", Options["--replaceNonStandardResidues"], "yes no")
 570 
 571     MiscUtil.ValidateOptionTextValue("--membrane", Options["--membrane"], "yes no")
 572     MiscUtil.ValidateOptionTextValue("--waterBox", Options["--waterBox"], "yes no")
 573 
 574     if re.match("^yes$", Options["--membrane"], re.I) and re.match("^yes$", Options["--waterBox"], re.I):
 575         MiscUtil.PrintError(
 576             'You\'ve specified, "Yes" for both "--membrane" and "--waterBox" options. It\'s not allowed. You must choose between adding a water box or a membrane. The water is automatically added during the construction of the membrane.'
 577         )
 578 
 579 
 580 # Setup a usage string for docopt...
 581 _docoptUsage_ = """
 582 OpenMMPrepareMacromolecule.py - Prepare a macromolecule for simulation
 583 
 584 Usage:
 585     OpenMMPrepareMacromolecule.py [--addHeavyAtoms <yes or no>] [--addHydrogens <yes or no>] [--addHydrogensAtpH <number>]
 586                                   [--addResidues <yes or no>] [--deleteHeterogens <All, AllExceptWater, WaterOnly, None>] [--ionPositive <text>]
 587                                   [--ionNegative <text>] [--ionicStrength <number>] [--listDetails <yes or no> ] [--membrane <yes or no>]
 588                                   [--membraneParams <Name,Value,..>] [--overwrite] [--replaceNonStandardResidues <yes or no>] [--waterBox <yes or no>]
 589                                   [--waterBoxParams <Name,Value,..>] [-w <dir>] -i <infile> -o <outfile>
 590     OpenMMPrepareMacromolecule.py -h | --help | -e | --examples
 591 
 592 Description:
 593     Prepare a macromolecute in an input file for molecular simulation and
 594     and write it out to an output file. The macromolecule is prepared by
 595     automatically performing the following actions:
 596 
 597         . Identify and replace non-standard residues
 598         . Add missing residues
 599         . Add missing heavy atoms
 600         . Add missing hydrogens
 601 
 602     You may optionally remove heterogens, add a water box, and add a lipid
 603     membrane.
 604 
 605     The supported input file format are:  PDB (.pdb) and CIF (.cif)
 606 
 607     The supported output file formats are:  PDB (.pdb) and CIF (.cif)
 608 
 609 Options:
 610     --addHeavyAtoms <yes or no>  [default: yes]
 611         Add missing non-hydrogen atoms based on the templates. The terminal atoms
 612         are also added.
 613     --addHydrogens <yes or no>  [default: yes]
 614         Add missing hydrogens at pH value specified using '-addHydrogensAtpH'
 615         option. The missing hydrogens are added based on the templates and pKa
 616         calculations are performed.
 617     --addHydrogensAtpH <number>  [default: 7.0]
 618         pH value to use for adding missing hydrogens.
 619     --addResidues <yes or no>  [default: yes]
 620         Add missing residues unidentified based on the PDB records.
 621     --deleteHeterogens <All, AllExceptWater, WaterOnly, None>  [default: auto]
 622         Delete heterogens corresponding to  non-standard names of amino acids, dna,
 623         and rna along with any ligand names. 'N' and 'UNK' also consider standard
 624         residues. Default value: WaterOnly during addition of WaterBox or Membrane;
 625         Otherwise, None.
 626         
 627         The 'AllExceptWater' or 'None' values are not allowed during the addition of
 628         a water box or membrane. The waters must be deleted as they are explicitly
 629         added during the construction of a water box and membrane.
 630     -e, --examples
 631         Print examples.
 632     -h, --help
 633         Print this help message.
 634     -i, --infile <infile>
 635         Input file name.
 636     --ionPositive <text>  [default: Na+]
 637         Type of positive ion to add during the addition of a water box or membrane.
 638         Possible values: Li+, Na+, K+, Rb+, or Cs+.
 639     --ionNegative <text>  [default: Cl-]
 640         Type of negative ion to add during the addition of a water box or membrane.
 641         Possible values: Cl-, Br-, F-, or I-.
 642     --ionicStrength <number>  [default: 0.0]
 643         Total concentration of both positive and negative ions to add excluding
 644         the ions added to neutralize the system during the addition of a water box
 645         or a membrane.
 646     -l, --listDetails <yes or no>  [default: no]
 647         List details about missing and non-standard residues along with residues
 648         containing missing atoms.
 649     --membrane <yes or no>  [default: no]
 650         Add lipid membrane along with a water box. The script replies on OpenMM
 651         method modeller.addMebrane() to perform the task. The membrane is added
 652         in the XY plane. The existing macromolecule must be oriented and positioned
 653         correctly.
 654         
 655         A word to the wise: You may want to start with a model from the Orientations
 656         of Proteins in Membranes (OPM) database at http://opm.phar.umich.edu.
 657         
 658         The size of the membrane and water box are determined by the value of
 659         'padding' parameter specified using '--membraneParams' option. All atoms
 660         in macromolecule are guaranteed to be at least this far from any edge of
 661         the periodic box.
 662     --membraneParams <Name,Value,..>  [default: auto]
 663         A comma delimited list of parameter name and value pairs for adding
 664         a lipid membrane and water.
 665         
 666         The supported parameter names along with their default values are
 667         are shown below:
 668             
 669             lipidType, POPC  [ Possible values: POPC, POPE, DLPC, DLPE,  DMPC, 
 670                 DOPC or DPPC ]
 671             membraneCenterZ, 0.0
 672             padding, 1.0
 673             
 674         A brief description of parameters is provided below:
 675             
 676             lipidType: Type of lipid to use for constructing the membrane.
 677             membraneCenterZ: Position along the Z axis of the center of
 678                 the membrane in nanomertes.
 679             padding: Minimum padding distance to use in nanomertes. It's used
 680                 to determine the size of the membrane and water box. All atoms
 681                 in the macromolecule are guaranteed to be at least this far from
 682                 any edge of the periodic box.
 683             
 684     -o, --outfile <outfile>
 685         Output file name.
 686     --overwrite
 687         Overwrite existing files.
 688     --replaceNonStandardResidues <yes or no>  [default: yes]
 689         Replace non-standard residue names by standard residue names based on the
 690         list of non-standard residues available in pdbfixer.
 691     --waterBox <yes or no>  [default: no]
 692         Add water box.
 693     --waterBoxParams <Name,Value,..>  [default: auto]
 694         A comma delimited list of parameter name and value pairs for adding
 695         a water box.
 696         
 697         The supported parameter names along with their default values are
 698         are shown below:
 699             
 700             mode, Padding  [ Possible values: Size or Padding ]
 701             size, None  [ Possible value: xsize ysize zsize ]
 702             padding, 1.0
 703             shape, cube  [ Possible values: cube, dodecahedron, or octahedron ]
 704             
 705         A brief description of parameters is provided below:
 706             
 707             mode: Specify the size of the waterbox explicitly or calculate it
 708                 automatically for a macromolecule along with adding padding
 709                 around ther macromolecule.
 710             size: A space delimited triplet of values corresponding to water
 711                 size in nanometers. It must be specified during 'Size' value of
 712                 'mode' parameter.
 713             padding: Padding around a macromolecule in nanometers for filling
 714                 box with water. It must be specified during 'Padding' value of
 715                 'mode' parameter.
 716             
 717     -w, --workingdir <dir>
 718         Location of working directory which defaults to the current directory.
 719 
 720 Examples:
 721     To prepare a macromolecule in a PDB file by replacing non-standard residues,
 722     adding missing residues, adding missing heavy atoms and missing hydrogens,
 723     and generate a PDB file, type:
 724 
 725         % OpenMMPrepareMacromolecule.py -i Sample11.pdb -o Sample11Out.pdb
 726 
 727     To run the first example for listing additional details about missing atoms and
 728     residues, and generate a PDB file, type:
 729 
 730         % OpenMMPrepareMacromolecule.py --listDetails yes -i Sample11.pdb
 731           -o Sample11Out.pdb
 732 
 733     To run the first example for deleting all heterogens including water along
 734     with performing all default actions, and generate a PDB file, type:
 735 
 736         % OpenMMPrepareMacromolecule.py --deleteHeterogens All -i Sample11.pdb
 737           -o Sample11Out.pdb
 738 
 739     To run the first example for deleting water only heterogens along with performing
 740     all default actions, and generate a PDB file, type:
 741 
 742         % OpenMMPrepareMacromolecule.py --deleteHeterogens WaterOnly
 743           -i Sample11.pdb -o Sample11Out.pdb --ov
 744 
 745     To run the first example for adding a water box by automatically calculating its
 746     size, along with performing all default actions, and generate a PDB file, type:
 747 
 748         % OpenMMPrepareMacromolecule.py --waterBox yes -i Sample11.pdb
 749           -o Sample11Out.pdb
 750 
 751     To run the previous example by explcitly specifying various water box parameters,
 752     and generate a PDB file, type:
 753 
 754         % OpenMMPrepareMacromolecule.py --waterBox yes
 755           --waterBoxParams "mode,Padding, padding, 1.0, shape, cube"
 756           -i Sample11.pdb -o Sample11Out.pdb
 757 
 758     To run the first example for adding a water box of a specific size along with
 759     performing all default actions, and generate a PDB file, type:
 760 
 761         % OpenMMPrepareMacromolecule.py --waterBox yes
 762           --waterBoxParams "mode,Size, size, 7.635 7.077 7.447"
 763           -i Sample11.pdb -o Sample11Out.pdb
 764 
 765     To add a lipid membrane around a correctly oriented and positioned macromolecule,
 766     along with performing all default actions,  and generate a PDB file, type:
 767 
 768         % OpenMMPrepareMacromolecule.py --membrane yes
 769           -i Sample12.pdb -o Sample12Out.pdb
 770 
 771     To add a lipid membrane around a correctly oriented and positioned macromolecule,
 772     deleting all heterogens along with performing all default actions,  and generate a
 773     PDB file, type:
 774 
 775         % OpenMMPrepareMacromolecule.py --membrane yes --deleteHeterogens All 
 776           -i Sample12.pdb -o Sample12Out.pdb
 777 
 778     To run the previous example by explcitly specifying various membrane parameters,
 779     and generate a PDB file, type:
 780 
 781         % OpenMMPrepareMacromolecule.py --membrane yes
 782           --membraneParams "lipidType, POPC, membraneCenterZ, 0.0, padding, 1.0"
 783           -i Sample12.pdb -o Sample12Out.pdb
 784 
 785 Author:
 786     Manish Sud(msud@san.rr.com)
 787 
 788 See also:
 789     InfoPDBFiles.pl, ExtractFromPDBFiles.pl, PyMOLExtractSelection.py,
 790     PyMOLInfoMacromolecules.py, PyMOLSplitChainsAndLigands.py
 791 
 792 Copyright:
 793     Copyright (C) 2026 Manish Sud. All rights reserved.
 794 
 795     The functionality available in this script is implemented using OpenMM, an
 796     open source molecuar simulation package.
 797 
 798     This file is part of MayaChemTools.
 799 
 800     MayaChemTools is free software; you can redistribute it and/or modify it under
 801     the terms of the GNU Lesser General Public License as published by the Free
 802     Software Foundation; either version 3 of the License, or (at your option) any
 803     later version.
 804 
 805 """
 806 
 807 if __name__ == "__main__":
 808     main()