From a11c908447b14739d27320e234eb0efad7704721 Mon Sep 17 00:00:00 2001 From: Sean Colby Date: Thu, 21 Sep 2023 15:38:09 -0700 Subject: [PATCH] Fix XTB single point optimization functionality --- isicle/md.py | 152 ++++++++++++++++++++++++++++++------------------ isicle/parse.py | 28 +++++---- 2 files changed, 110 insertions(+), 70 deletions(-) diff --git a/isicle/md.py b/isicle/md.py index 21f0562..5507a31 100644 --- a/isicle/md.py +++ b/isicle/md.py @@ -134,23 +134,29 @@ def save_geometry(self, fmt='xyz'): isicle.io.save(geomfile, self.geom) self.geom.path = geomfile - def _configure_xtb(self, forcefield='gfn2', optlevel='normal', charge=None, solvation=None): + def _configure_xtb(self, forcefield='gfn2', optlevel='normal', charge=0, solvation=None, + ignore_topology=False, cycles=None, dryrun=False): """ Set command line for xtb simulations. Parameters ---------- forcefield : str - GFN forcefield for the optimization - Default: gff - Supported forcefields: gfn2, gfn1, gff + GFN forcefield for the optimization. + Supported : gfn2, gfn1, gff optlevel : str - Optimization convergence level - Default : normal + Optimization convergence level. Supported : crude, sloppy, loose, lax, normal, tight, vtight extreme charge : int Charge of molecular system. - Default : 0 (Neutral charge) + solvation : str + Specify solvent for simulation. + ignore_topolgy : bool + Turn off only the initial topology check prior to the conformational search. + cycles : int + Maximum number of optimization cycles. + dryrun : bool + Signal whether to perform a dry run. """ @@ -166,55 +172,69 @@ def _configure_xtb(self, forcefield='gfn2', optlevel='normal', charge=None, solv # Add forcefield s += '--' + forcefield + ' ' - # Add optional charge + # Add charge if charge is not None: - s += '--chrg ' + charge + ' ' + s += '--chrg ' + str(charge) + ' ' + + # Add dryrun option + if dryrun: + s += '--dryrun ' # Add optional implicit solvation if solvation is not None: s += '--alpb ' + solvation + ' ' + if ignore_topology: + s += '--noreftopo ' + + if cycles: + s += '--cycles ' + str(cycles) + ' ' + # Add output s += '&>' + ' ' s += '{}.{}'.format(self.basename, "out") return s - def _configure_crest(self, ewin=6, optlevel='Normal', forcefield='gfn2', + def _configure_crest(self, forcefield='gfn2', optlevel='Normal', ewin=6, protonate=False, deprotonate=False, tautomerize=False, - ion=None, charge=None, dryrun=False, processes=1, - solvation=None, ignore_topology=False): + ion=None, charge=None, solvation=None, ignore_topology=False, + cycles=None, dryrun=False, processes=1): """ Set command line for crest simulations. Parameters ---------- - ewin : int - Energy window (kcal/mol) for conformer, (de)protomer, or tautomer search. - Default : 6 + forcefield : str + GFN forcefield for the optimization + Supported : gfn2, gfn1, gff optlevel : str Optimization convergence level - Default : normal Supported : crude, sloppy, loose, lax, normal, tight, vtight extreme - forcefield : str - GFN forcefield for the optimization - Default: gff - Supported forcefields: gfn2, gfn1, gff + ewin : int + Energy window (kcal/mol) for conformer, (de)protomer, or tautomer search. protonate : bool Signal to initiate protomer search. Suggested ewin = 30. - Default : False deprotonate : bool Signal to initiate deprotonated conformers. Suggesting ewin = 30. - Default : False - tautomer : bool + tautomerize : bool Signal to initiate tautomer search. - Default : False ion : str Keyword to couple with protonate to ionize molecule with an ion other than a proton. See :obj:`~isicle.adduct.parse_ion` for list of ion options. charge : int Charge of molecular system. - Default : 0 (Neutral charge) + solvation : str + Specify solvent for simulation. + ignore_topolgy : bool + Turn off only the initial topology check prior to the conformational search. + cycles : int + Maximum number of optimization cycles. + dryrun : bool + Signal whether to perform a dry run. + processes : int + Number of parallel processes. + """ # Start base command @@ -237,8 +257,9 @@ def _configure_crest(self, ewin=6, optlevel='Normal', forcefield='gfn2', if ion is not None: s += '-swel ' + ion + ' ' + # Add charge if charge is not None: - s += '-chrg ' + str(charge) + ' ' + s += '--chrg ' + str(charge) + ' ' # Add dryrun option if dryrun: @@ -263,6 +284,9 @@ def _configure_crest(self, ewin=6, optlevel='Normal', forcefield='gfn2', if ignore_topology: s += '--noreftopo ' + if cycles: + s += '--cycles ' + str(cycles) + ' ' + # Add output s += '&>' + ' ' @@ -272,34 +296,39 @@ def _configure_crest(self, ewin=6, optlevel='Normal', forcefield='gfn2', return s - def configure(self, task='optimize', forcefield='gfn2', charge=None, - ewin=6, ion=None, optlevel='Normal', dryrun=False, processes=1, - solvation=None, ignore_topology=False): + def configure(self, task='optimize', forcefield='gfn2', optlevel='Normal', + ewin=6, charge=None, ion=None, solvation=None, + ignore_topology=False, cycles=None, dryrun=False, processes=1): """ Generate command line Parameters ---------- - tasks : str + task : str Set task to "optimize", "conformer", "protonate", "deprotonate", or "tautomerize". - Default : "optimize" forcefield : str GFN forcefield for the optimization - Default: gff - Supported forcefields: gfn2, gfn1, gff - ewin : int - Energy window (kcal/mol) for conformer(set to 6), (de)protomer(set to 30), or tautomer(set to 30) search. - Default : 6 - ion : str - Ion for protomer calculation. + Supported : gfn2, gfn1, gff optlevel : str or list of str Set optimization level. Supply globally or per task. + ewin : int + Energy window (kcal/mol) for conformer(set to 6), (de)protomer(set to 30), or tautomer(set to 30) search + charge : int + Charge of molecular system. ion : str Keyword to couple with protonate to ionize molecule with an ion other than a proton. See :obj:`~isicle.adduct.parse_ion` for list of ion options. - charge : int - Charge of molecular system. - Default : 0 (Neutral charge) + solvation : str + Specify solvent for simulation. + ignore_topolgy : bool + Turn off only the initial topology check prior to the conformational search. + cycles : int + Maximum number of optimization cycles. + dryrun : bool + Signal whether to perform a dry run. + processes : int + Number of parallel processes. + """ if type(task) == list: @@ -311,7 +340,12 @@ def configure(self, task='optimize', forcefield='gfn2', charge=None, if task == 'optimize': config = self._configure_xtb(optlevel=optlevel, - forcefield=forcefield) + forcefield=forcefield, + charge=charge, + dryrun=dryrun, + solvation=solvation, + ignore_topology=ignore_topology, + cycles=cycles) else: if task == 'conformer': @@ -338,7 +372,8 @@ def configure(self, task='optimize', forcefield='gfn2', charge=None, dryrun=dryrun, processes=processes, solvation=solvation, - ignore_topology=ignore_topology) + ignore_topology=ignore_topology, + cycles=cycles) else: raise Error( 'Task not assigned properly, please choose optimize, conformer, protonate, deprotonate, or tautomerize') @@ -369,7 +404,6 @@ def finish(self): self.temp_dir, self.basename + '.out')) result = parser.parse() - self.__dict__.update(result) for i in self.geom: @@ -385,9 +419,9 @@ def finish(self): else: self.geom = self.geom[0] - def run(self, geom, task='optimize', forcefield='gfn2', charge=None, - ewin=6, ion=None, optlevel='Normal', dryrun=False, processes=1, - solvation=None, ignore_topology=False): + def run(self, geom, task='optimize', forcefield='gfn2', optlevel='Normal', + ewin=6, charge=None, ion=None, solvation=None, ignore_topology=False, + cycles=None, dryrun=False, processes=1): """ Optimize geometry via density functional theory using supplied functional and basis set. @@ -396,21 +430,29 @@ def run(self, geom, task='optimize', forcefield='gfn2', charge=None, ---------- geom : :obj:`~isicle.geometry.Geometry` Molecule representation. - tasks : str + task : str Set task to "optimize", "conformer", "protonate", "deprotonate", or "tautomerize". forcefield : str GFN forcefield for the optimization, including "gfn2", "gfn1", "gff". - ewin : int - Energy window (kcal/mol) for conformer(set to 6), (de)protomer(set to 30), or tautomer(set to 30) search. - ion : str - Ion for protomer calculation. optlevel : str or list of str Set optimization level. Supply globally or per task. + ewin : int + Energy window (kcal/mol) for conformer(set to 6), (de)protomer(set to 30), or tautomer(set to 30) search. + charge : int + Charge of molecular system. ion : str Keyword to couple with protonate to ionize molecule with an ion other than a proton. See :obj:`~isicle.adduct.parse_ion` for list of ion options. - charge : int - Charge of molecular system. Defaults to 0 (neutral). + solvation : str + Specify solvent for simulation. + ignore_topolgy : bool + Turn off only the initial topology check prior to the conformational search. + cycles : int + Maximum number of optimization cycles. + dryrun : bool + Signal whether to perform a dry run. + processes : int + Number of parallel processes. Returns ------- @@ -428,7 +470,7 @@ def run(self, geom, task='optimize', forcefield='gfn2', charge=None, # Configure self.configure(task=task, forcefield=forcefield, charge=charge, ewin=ewin, ion=ion, optlevel=optlevel, dryrun=dryrun, processes=processes, - solvation=solvation, ignore_topology=ignore_topology) + solvation=solvation, ignore_topology=ignore_topology, cycles=cycles) # Run QM simulation self.submit() diff --git a/isicle/parse.py b/isicle/parse.py index 8841032..9a69055 100644 --- a/isicle/parse.py +++ b/isicle/parse.py @@ -739,9 +739,12 @@ def parse(self): # Check that the file is valid first if len(self.contents) == 0: raise RuntimeError('No contents to parse: {}'.format(self.path)) - if "terminated normally" not in self.contents[-1]: - if "ratio" not in self.contents[-2]: - raise RuntimeError('XTB job failed: {}'.format(self.path)) + + last_lines = ''.join(self.contents[-10:]) + if ("terminat" not in last_lines) \ + & ("normal" not in last_lines) \ + & ("ratio" not in last_lines): + raise RuntimeError('XTB job failed: {}'.format(self.path)) self.parse_crest = False self.parse_opt = False @@ -749,11 +752,8 @@ def parse(self): # Initialize result object to store info result = {} + result['protocol'] = self._parse_protocol() - try: - result['protocol'] = self._parse_protocol() - except: - pass try: result['timing'] = self._parse_timing() except: @@ -767,14 +767,12 @@ def parse(self): # Parse geometry from assoc. XYZ file try: if self.path.endswith('xyz'): - - if 'geometry' in to_parse: - try: - self.xyz_path = self.path - result['geom'] = self._parse_xyz() - - except: - pass + try: + self.xyz_path = self.path + result['geom'] = self._parse_xyz() + + except: + pass if self.path.endswith('out') or self.path.endswith('log'):