Source code for schrodinger.application.macromodel.input

"""
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?