"""
Facilities for reading and writing simplified MacroModel input files.
It can also be used to write MacroModel `*.com` files.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Matvey Adzhigirey, Shawn Watts
import schrodinger.application.macromodel.utils as mmodutil
import schrodinger.infra.mm as mm
from schrodinger.application.inputconfig import InputConfig
SUPPORTED_FFLD = ["OPLS2.0", "OPLS2.1"]
VALID_FORCEFIELD_LIST = mm.opls_names()
for FORCEFIELD in VALID_FORCEFIELD_LIST:
if FORCEFIELD not in SUPPORTED_FFLD:
SUPPORTED_FFLD.append(FORCEFIELD)
VALID_FORCEFIELD_NAME = '"%s"' % ('", "'.join(SUPPORTED_FFLD))
GENERAL_SPECS = [
i for i in """
JOB_TYPE = option('ENERGY_LISTING', 'MINIMIZATION', 'EMBRACE_MINIMIZATION', 'EMBRACE_CONFSEARCH', 'MINTA', 'RCE', default='CONFSEARCH')
INPUT_STRUCTURE_FILE = string()
OUTPUT_STRUCTURE_FILE = string()
FORCE_FIELD = option("MM2*", "MM3*", "AMBER*", "AMBER94", "OPLS", "MMFF", "MMFFs", VALID_FORCEFIELD_NAME, default='OPLS_2005')
SOLVENT = option('None', 'Water', 'Octanol', 'CHCL3', default='None')
ELECTROSTATIC_TREATMENT = option('Constant dielectric', 'Distance-dependent', 'Force field defined', default='Constant dielectric')
DIELECTRIC_CONSTANT = float(default=1.0)
CHARGES_FROM = option('Force field', 'Structure file', default='Force field')
CUTOFF = option('Normal', 'Extended', 'None', default='Normal')
MULTI_LIGAND = boolean(default=True) # some jobs only. FIXME come up with a better name
USE_SUBSTRUCTURE_FILE = boolean(default=False)
NO_PARTIAL_BLOCKS = boolean(default=False)
RETAIN_MIRROR_IMAGE = boolean(default=True)
""".replace('VALID_FORCEFIELD_NAME', VALID_FORCEFIELD_NAME).split('\n')
]
# RETAIN_MIRROR_IMAGE = True Matches Maestro 9.3 default and is a good
# default, but it can change the output ensemble. mmod-1582.
MINIMIZER_SPECS = [
i for i in """
MINI_METHOD = option('PRCG', 'TNCG', 'OSVM', 'SD', 'FMNR', 'LBFGS', 'Optimal', default='PRCG')
MAXIMUM_ITERATION = integer(default=5000)
CONVERGE_ON = option('Gradient', 'Energy', 'Movement', 'Nothing', default='Gradient')
CONVERGENCE_THRESHOLD = float(default=0.05)
""".split('\n')
]
# EMBRACE will have all MINIMIZER specs as well
EMBRACE_SPECS = [
i for i in """
COMPLEX_MINI_METHOD = option('PRCG', 'TNCG', 'OSVM', 'SD', 'FMNR', 'LBFGS', 'Optimal', default='PRCG')
COMPLEX_MAXIMUM_ITERATION = integer(default=5000)
COMPLEX_CONVERGE_ON = option('Gradient', 'Energy', 'Movement', 'Nothing', default='Gradient')
COMPLEX_CONVERGENCE_THRESHOLD = float(default=0.05)
MODE = option('EnergyDifference', 'InteractionEnergy')
""".split('\n')
]
# MINTA will have all MINIMIZER specs as well
MINTA_SPECS = [
i for i in """
MINTA_ITERATIONS = integer(default=5)
MINTA_ENERGY_EVALUATIONS = integer(default=2000)
MINTA_TEMPERATURE = float(default=300.0)
""".split('\n')
]
# Redundanct Conformer Elimination specs
RCE_SPECS = [
i for i in """
CONF_ELIM_USE_JAGUAR_ENERGIES = boolean(default=False)
CONF_ELIM_MAX_STRUCT_RETAIN = integer(default=0) # In bmin default becomes 10,000
""".split('\n')
]
CONFSEARCH_SPECS = [
i for i in """
CONFSEARCH_METHOD = option('MCMM', 'Confgen', 'Lowmode', 'Mixed', 'mcmm', 'cgen', 'lmod', 'lmcs', 'mixed', default='MCMM')
INCONFS_PER_SEARCH = integer()
OUTCONFS_PER_SEARCH = integer(default=1, min=1)
CONFSEARCH_STEPS = integer(default=200)
CONFSEARCH_SEED = integer(default=0, max=78593)
CONFSEARCH_STEPS_PER_ROTATABLE = integer(default=50)
ENERGY_WINDOW = float(default=5.0)
CONFSEARCH_TORSION_SAMPLING = option('Restricted', 'Intermediate', 'Enhanced', 'Extended', default='Intermediate')
CONFORMER_ELIMINATION_METHOD = option('RMSD', 'Maximum atom deviation', default='Maximum atom deviation')
CONFORMER_ELIMINATION_VALUE = float(default=0.5)
""".split('\n')
]
ENERGY_LISTING_SPECS = [
i for i in """
ENERGY_REPORT_OPTION = option("LOGFILE", "MMOFILE", "FULLREPORT") # FIXME come up with better names
""".split('\n')
]
class _MacroModel(InputConfig):
"""
Class for reading and writing simplified MacroModel input files.
To the constructor, pass either a dictionary of MacroModel keywords,
or a path to a simplified input file. This instance then can be used
like a dictionary to retrieve and set keywords.
Example 1::
config = ConfSearch(keyword_dict)
config.writeComFile("path.com")
Example 2::
config = ConfSearch(simplified_input_file)
inpath = config['INPUT_STRUCTURE_FILE']
Example 3::
config = ConfSearch()
config['INPUT_STRUCTURE_FILE'] = "path.mae"
config.writeSimplified("path.inp")
"""
def __init__(self, infile=None, specs=None):
"""
Accepts one argument which is either a path to a simplified input
file or a keyword dictionary.
"""
if specs is None:
specs = GENERAL_SPECS
InputConfig.__init__(self, infile, specs)
# Make all keywords upper-case in order to make this module
# case-insensitive:
for key in self.keys():
upperkey = key.upper()
if upperkey != key:
self[upperkey] = self[key]
del self[key]
if infile:
# Convert values to expected types:
self.validateValues(preserve_errors=False)
def setupGeneralOptions(self, mcu):
if self['NO_PARTIAL_BLOCKS']:
mcu.DEBG[1] = 3
# Individual searches on each input mol.
mcu.serial = self['MULTI_LIGAND']
mcu.MCOP[1] = 1000 # Smaller log file
# Force Field selection
mcu.FFLD[4] = 0
if self['FORCE_FIELD'] == "MM2*":
mcu.FFLD[1] = 1
elif self['FORCE_FIELD'] == "MM3*":
mcu.FFLD[1] = 2
elif self['FORCE_FIELD'] == "AMBER*":
mcu.FFLD[1] = 3
elif self['FORCE_FIELD'] == "AMBER94":
mcu.FFLD[1] = 4
elif self['FORCE_FIELD'] == "OPLS":
mcu.FFLD[1] = 5
elif self['FORCE_FIELD'] == "MMFF":
mcu.FFLD[1] = 10
elif self['FORCE_FIELD'] == "MMFFs":
mcu.FFLD[1] = 10
mcu.FFLD[4] = 1
elif self['FORCE_FIELD'] == mm.OPLS_NAME_F14:
mcu.FFLD[1] = 14
elif self['FORCE_FIELD'] in ["OPLS2.0",
"OPLS2.1"]: # keep for back compatibility
mcu.FFLD[1] = 16
elif self['FORCE_FIELD'] == mm.OPLS_NAME_F16:
mcu.FFLD[1] = 16
# Solvent
if self['SOLVENT'] == 'None':
mcu.solv = False
else:
mcu.solv = True
if self['SOLVENT'] == 'Water':
mcu.SOLV[2] = 1
elif self['SOLVENT'] == 'CHCL3':
mcu.SOLV[2] = 5
elif self['SOLVENT'] == 'Octanol':
mcu.SOLV[2] = 9
# Electrostatic treatment
if self['ELECTROSTATIC_TREATMENT'] == 'Constant dielectric':
mcu.FFLD[2] = 1
elif self['ELECTROSTATIC_TREATMENT'] == 'Distance-dependent':
mcu.FFLD[2] = 2
elif self['ELECTROSTATIC_TREATMENT'] == 'Force field defined':
mcu.FFLD[2] = 0
# Dielectic constant
mcu.FFLD[5] = self['DIELECTRIC_CONSTANT']
# Charges from: file/ffld
if self['CHARGES_FROM'] == 'Force field':
mcu.chrg = False
elif self['CHARGES_FROM'] == 'Structure file':
mcu.chrg = True
mcu.CHRG[1] = 1
# Electrostatics Cutoff
if self['CUTOFF'] == 'Normal':
mcu.exnb = False
mcu.EXNB[5] = 7.0
mcu.EXNB[6] = 12.0
mcu.EXNB[7] = 4.0
mcu.EXNB[5] = 89.4427
mcu.EXNB[6] = 99999
elif self['CUTOFF'] == 'Extended':
mcu.exnb = True
mcu.EXNB[1] = 0 # just leverage BDCO
mcu.EXNB[5] = 8.0
mcu.EXNB[6] = 20.0
mcu.EXNB[7] = 4.0
mcu.BDCO[5] = 89.4427
mcu.BDCO[6] = 99999
elif self['CUTOFF'] == 'None':
mcu.exnb = True
mcu.EXNB[1] = 2
mcu.EXNB[5] = 0
mcu.EXNB[6] = 0
mcu.EXNB[7] = 0
mcu.BDCO[5] = 99999
mcu.BDCO[6] = 99999
# Substructure File
mcu.subs = self['USE_SUBSTRUCTURE_FILE']
# NANT/retain mirror image confs. mmod-1582.
mcu.nant = self['RETAIN_MIRROR_IMAGE']
def setupEnergyListingOptions(self, mcu):
report = self.get('ENERGY_REPORT_OPTION')
if report == "LOGFILE":
mcu.ELST[1] = -1 # just energy to log file
elif report == "MMOFILE":
mcu.ELST[1] = 1 # write mmo.
elif report == "FULLREPORT":
mcu.ELST[1] = 2 # write verbose report.
def setupMinimizationOptions(self, mcu):
# Minimization method
method = self.get('MINI_METHOD')
if method == 'PRCG':
mcu.MINI[1] = 1
elif method == 'TNCG':
mcu.MINI[1] = 9
elif method == 'OSVM':
mcu.MINI[1] = 3
elif method == 'SD':
mcu.MINI[1] = 0
elif method == 'FMNR':
mcu.MINI[1] = 4
elif method == 'LBFGS':
mcu.MINI[1] = 10
elif method == 'Optimal':
mcu.MINI[1] = 20
# Minimization iterations
if self.get('MAXIMUM_ITERATION'):
mcu.MINI[3] = self['MAXIMUM_ITERATION']
# Converge criteria
converge_on = self.get('CONVERGE_ON')
if converge_on == 'Gradient':
mcu.CONV[1] = 2
elif converge_on == 'Energy':
mcu.CONV[1] = 1
elif converge_on == 'Movement':
mcu.CONV[1] = 3
elif converge_on == 'Nothing':
mcu.CONV[1] = 0
# Mini threshold
if self.get('CONVERGENCE_THRESHOLD'):
mcu.CONV[5] = self['CONVERGENCE_THRESHOLD']
def setupConfSearchOptions(self, mcu):
# Number of outputs per search
if self.get('OUTCONFS_PER_SEARCH'):
mcu.MCOP[6] = self['OUTCONFS_PER_SEARCH']
# Number of inputs per search
if self.get('INCONFS_PER_SEARCH'):
mcu.MCOP[7] = self['INCONFS_PER_SEARCH']
# AUOP (AUTO options) smart search parameters
# print '***** CONFSEARCH_STEPS_PER_ROTATABLE:', type(self.get('CONFSEARCH_STEPS_PER_ROTATABLE')) == type(1)
# print '***** CONFSEARCH_STEPS_PER_ROTATABLE:',
# type(self['CONFSEARCH_STEPS_PER_ROTATABLE']) == type(1)
if self.get('CONFSEARCH_STEPS_PER_ROTATABLE'):
mcu.AUOP[5] = self['CONFSEARCH_STEPS_PER_ROTATABLE']
if self.get('CONFSEARCH_TORSION_SAMPLING'):
if self['CONFSEARCH_TORSION_SAMPLING'] == 'Restricted':
mcu.AUTO[8] = 0
if self['CONFSEARCH_TORSION_SAMPLING'] == 'Intermediate':
mcu.AUTO[8] = 1
if self['CONFSEARCH_TORSION_SAMPLING'] == 'Enhanced':
mcu.AUTO[8] = 2
if self['CONFSEARCH_TORSION_SAMPLING'] == 'Extended':
mcu.AUTO[8] = 3
# Csearch number of step parameters
# Each stage determines which mcu write method to call by
# using the 'CONFSEARCH_METHOD' keyword.
if self.get('CONFSEARCH_STEPS'):
mcu.CGEN[1] = self['CONFSEARCH_STEPS']
mcu.MCMM[1] = self['CONFSEARCH_STEPS']
mcu.LMCS[1] = self['CONFSEARCH_STEPS']
# Energy window
if self.get('ENERGY_WINDOW'):
mcu.DEMX[5] = self['ENERGY_WINDOW']
# Redundant Poses.
# mmod-1582 - MAXIMUM_ATOM_DEVIATION is an undocumented
# keyword, and is superceded by CONFORMER_ELIMINATION_METHOD and
# CONFORMER_ELIMINATION_VALUE.
if self.get('MAXIMUM_ATOM_DEVIATION'):
mcu.CRMS[6] = float(self['MAXIMUM_ATOM_DEVIATION'])
# New keywords will have final say. mmod-1582.
conf_elim_method = self.get('CONFORMER_ELIMINATION_METHOD')
if conf_elim_method == 'Maximum atom deviation':
mcu.CRMS[8] = 0 # maximum distance criterion.
elif conf_elim_method == 'RMSD':
mcu.CRMS[8] = 2 # root mean square distance.
conf_elim_value = self.get('CONFORMER_ELIMINATION_VALUE')
if conf_elim_value is not None:
mcu.CRMS[6] = float(conf_elim_value)
# Maximum minimization iterations. MINIMIZER_MAXIMUM_ITERATION
# is an undocumented keyword and duplicates MAXIMUM_ITERATION.
# This keyword is no longer needed because the ConfSearch.setup()
# method now calls self.setupMinimizationOptions(mcu) (just before
# self.setupConfSearchOptions(mcu)). mmod-1582
if self.get('MINIMIZER_MAXIMUM_ITERATION'):
mcu.MINI[3] = float(self['MINIMIZER_MAXIMUM_ITERATION'])
# MMOD-1809: Add support for SEED.
mcu.SEED[1] = self['CONFSEARCH_SEED']
def setupMbaeOptions(self, mcu):
# Minimization method
method = self.get('COMPLEX_MINI_METHOD')
if method == 'PRCG':
mcu.MINI[9] = 1
elif method == 'TNCG':
mcu.MINI[9] = 9
elif method == 'OSVM':
mcu.MINI[9] = 3
elif method == 'SD':
mcu.MINI[9] = 0
elif method == 'FMNR':
mcu.MINI[9] = 4
elif method == 'LBFGS':
mcu.MINI[9] = 10
elif method == 'Optimal':
mcu.MINI[9] = 20
if self.get('COMPLEX_MAXIMUM_ITERATION'):
mcu.MINI[11] = self['COMPLEX_MAXIMUM_ITERATION']
# Converge criteria
converge_on = self.get('COMPLEX_CONVERGE_ON')
if converge_on == 'Gradient':
mcu.CONV[9] = 2
elif converge_on == 'Energy':
mcu.CONV[9] = 1
elif converge_on == 'Movement':
mcu.CONV[9] = 3
elif converge_on == 'Nothing':
mcu.CONV[9] = 0
# Mini threshold
if self.get('COMPLEX_CONVERGENCE_THRESHOLD'):
mcu.CONV[13] = self['COMPLEX_CONVERGENCE_THRESHOLD']
def setupMintaOptions(self, mcu):
if self.get('MINTA_ITERATIONS'):
mcu.MNTA[1] = self['MINTA_ITERATIONS']
if self.get('MINTA_ENERGY_EVALUATIONS'):
mcu.MNTA[2] = self['MINTA_ENERGY_EVALUATIONS']
if self.get('MINTA_TEMPERATURE'):
mcu.MNTA[5] = self['MINTA_TEMPERATURE']
def setupRCEOptions(self, mcu):
if self.get('CONF_ELIM_USE_JAGUAR_ENERGIES'):
mcu.ADDC[1] = -1
if self.get('CONF_ELIM_MAX_STRUCT_RETAIN'):
mcu.ADDC[2] = self['CONF_ELIM_MAX_STRUCT_RETAIN']
def writeComFile(self, com_file):
"""
Writes a MacroModel `*.com` file from stored keywords to specified path.
"""
if not com_file.endswith('.com'):
raise ValueError("MacroModel com file should end with *.com")
mcu = mmodutil.ComUtil()
self.setupGeneralOptions(mcu)
ligfile = self['INPUT_STRUCTURE_FILE']
out_file = self.get('OUTPUT_STRUCTURE_FILE')
# Call the jobtype-specific setup and write the com file:
self.setup(mcu, ligfile, out_file, com_file)
return com_file
def setup(self, mcu, ligfile, out_file, com_file):
raise RuntimeError("Please reimplement in base class")
def writeSimplified(self, filename):
"""
Writes a MacroModel simplified input file from stored keywords to
specified path.
This input file needs to be run via $SCHRODINGER/macromodel
"""
if not filename.endswith('.inp'):
raise ValueError(
"MacroModel simplified input file should end with *.inp")
self.writeInputFile(filename, ignore_none=True)
return filename
[docs]class ConfSearch(_MacroModel):
"""
A special class for dealing with Conformation Search jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + MINIMIZER_SPECS + CONFSEARCH_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'CONFSEARCH'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
# Honor minimizer settings. mmod-1582.
self.setupMinimizationOptions(mcu)
self.setupConfSearchOptions(mcu)
method = self['CONFSEARCH_METHOD']
if method in ['MCMM', 'mcmm']:
mcu.mcmm(mae_file=ligfile, com_file=com_file, out_file=out_file)
elif method in ['Confgen', 'CGEN', 'cgen']:
mcu.cgen(mae_file=ligfile, com_file=com_file, out_file=out_file)
elif method in ["Lowmode", 'LMOD', 'LMCS', "lmod", "lmcs"]:
mcu.lmod(mae_file=ligfile, com_file=com_file, out_file=out_file)
elif method in ["Mixed", "mixed"]:
mcu.mcmmlmod(mae_file=ligfile, com_file=com_file, out_file=out_file)
else:
raise RuntimeError("Invalid ConfSearch Method: " + method)
[docs]class EnergyListing(_MacroModel):
"""
A special class for dealing with Energy Listing jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + ENERGY_LISTING_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'ENERGY_LISTING'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupEnergyListingOptions(mcu)
mcu.elst(mae_file=ligfile, com_file=com_file, out_file=out_file)
[docs]class RCE(_MacroModel):
"""
A special class for dealing with RCE jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + RCE_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'RCE'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupRCEOptions(mcu)
mcu.addc(mae_file=ligfile, com_file=com_file, out_file=out_file)
[docs]class Minta(_MacroModel):
"""
A special class for dealing with Minta jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + MINIMIZER_SPECS + MINTA_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'MINTA'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupMinimizationOptions(mcu)
self.setupMintaOptions(mcu)
mcu.minta(mae_file=ligfile, com_file=com_file, out_file=out_file)
[docs]class Minimization(_MacroModel):
"""
A special class for dealing with Minimization jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + MINIMIZER_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'MINIMIZATION'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupMinimizationOptions(mcu)
mcu.mini(mae_file=ligfile, com_file=com_file, out_file=out_file)
[docs]class EmbraceMinimization(_MacroModel):
"""
A special class for dealing with Enbrace Minimization jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + MINIMIZER_SPECS + EMBRACE_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'EMBRACE_MINIMIZATION'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupMinimizationOptions(mcu)
self.setupMbaeOptions(mcu)
if self['MODE'] == 'EnergyDifference':
mcu.mbaeMiniEdiff(mae_file=ligfile,
com_file=com_file,
out_file=out_file)
elif self['MODE'] == 'InteractionEnergy':
mcu.mbaeMiniInter(mae_file=ligfile,
com_file=com_file,
out_file=out_file)
else:
pass
# FIXME do we need this? raise ValueError("Invalid MODE")
[docs]class EmbraceConfSearch(_MacroModel):
"""
A special class for dealing with Embrace Conformation Search jobs.
"""
[docs] def __init__(self, infile=None):
"""
infile - Input file path or dictionary of keywords.
"""
specs = GENERAL_SPECS + MINIMIZER_SPECS + \
CONFSEARCH_SPECS + EMBRACE_SPECS
_MacroModel.__init__(self, infile, specs)
self['JOB_TYPE'] = 'EMBRACE_CONFSEARCH'
[docs] def setup(self, mcu, ligfile, out_file, com_file):
self.setupConfSearchOptions(mcu)
self.setupMbaeOptions(mcu)
method = self['CONFSEARCH_METHOD']
if method in ["Lowmode", 'LMOD', 'LMCS', "lmod", "lmcs"]:
mcu.mbaeCsLmod(mae_file=ligfile,
com_file=com_file,
out_file=out_file)
elif method in ['MCMM', 'mcmm']:
mcu.mbaeCsMcmm(mae_file=ligfile,
com_file=com_file,
out_file=out_file)
elif method in ["Mixed", "mixed"]:
mcu.mbaeCsMcmmLmod(mae_file=ligfile,
com_file=com_file,
out_file=out_file)
else:
pass # error?