def parse_smiles(smiles): """Parse the given smiles string Args: smiles (str): smiles string to be parsed """ logger.info(f'Parsing SMILES string: {smiles}') parser = SmilesParser() for char, char_type in divide_smiles(smiles): parser.analyse_char(char, char_type) if len(parser.ring_dict) != 0: # This means a ring number has only been mentioned once, which is # invalid logger.critical('Invalid SMILES string') raise InvalidSmilesString parser.add_hs() parser.charge = sum(parser.charge_dict.values()) parser.analyse_alkene_stereochem_dict() for atom_no in sorted(parser.stereochem_dict.keys()): parser.add_stereochem(atom_no) return parser
def _check_solvent(self): """Check that all the solvents are the same for reactants and products """ molecules = self.reacs + self.prods if self.solvent is None: if all([mol.solvent is None for mol in molecules]): logger.info('Reaction is in the gas phase') return elif all([mol.solvent is not None for mol in molecules]): if not all([mol.solvent == self.reacs[0].solvent for mol in molecules]): logger.critical('Solvents in reactants and products ' 'don\'t match') raise SolventsDontMatch else: logger.info(f'Setting the reaction solvent to ' f'{self.reacs[0].solvent}') self.solvent = self.reacs[0].solvent else: logger.critical('Some species solvated and some not!') raise SolventsDontMatch if self.solvent is not None: logger.info(f'Setting solvent to {self.solvent.name} for all ' f'molecules in the reaction') for mol in self.reacs + self.prods: mol.solvent = self.solvent logger.info(f'Set the solvent of all species in the reaction to ' f'{self.solvent.name}') return None
def _check_balance(self): """Check that the number of atoms and charge balances between reactants and products. If they don't exit immediately """ def total(molecules, attr): return sum([getattr(m, attr) for m in molecules]) if total(self.reacs, 'n_atoms') != total(self.prods, 'n_atoms'): logger.critical('Number of atoms doesn\'t balance') raise UnbalancedReaction if total(self.reacs, 'charge') != total(self.prods, 'charge'): logger.critical('Charge doesn\'t balance') raise UnbalancedReaction # Ensure the number of unpaired electrons is equal on the left and # right-hand sides of the reaction, for now if (total(self.reacs, 'mult') - len(self.reacs) != total(self.prods, 'mult') - len(self.prods)): raise NotImplementedError('Found a change in spin state – not ' 'implemented yet!') self.charge = total(self.reacs, 'charge') return None
def get_defined_method(name, possibilities): """ Get an electronic structure method defined by it's name Arguments: name (str): possibilities (list(autode.wrappers.base.ElectronicStructureMethod)): Returns: (autode.wrappers.base.ElectronicStructureMethod): Method """ for method in possibilities: if method.name == name: method.set_availability() if method.available: return method else: logger.critical('Electronic structure method is not available') raise MethodUnavailable logger.critical('Requested electronic structure code doesn\'t exist') raise MethodUnavailable
def get_keywords(calc_input, molecule): """Generate a keywords list and adding solvent""" new_keywords = [] scf_block = False for keyword in calc_input.keywords: if isinstance(keyword, kws.Functional): keyword = f'dft\n maxiter 100\n xc {keyword.nwchem}\nend' elif isinstance(keyword, kws.BasisSet): keyword = f'basis\n * library {keyword.nwchem}\nend' elif isinstance(keyword, kws.Keyword): keyword = keyword.nwchem if 'opt' in keyword.lower() and molecule.n_atoms == 1: logger.warning('Cannot do an optimisation for a single atom') # Replace any 'opt' containing word in this keyword with energy words = [] for word in keyword.split(): if 'opt' in word: words.append('energy') else: words.append(word) new_keywords.append(' '.join(words)) elif keyword.lower().startswith('dft'): lines = keyword.split('\n') lines.insert(1, f' mult {molecule.mult}') new_keyword = '\n'.join(lines) new_keywords.append(new_keyword) elif keyword.lower().startswith('scf'): if calc_input.solvent: logger.critical('nwchem only supports solvent for DFT calcs') raise UnsuppportedCalculationInput scf_block = True lines = keyword.split('\n') lines.insert(1, f' nopen {molecule.mult - 1}') new_keyword = '\n'.join(lines) new_keywords.append(new_keyword) elif (any(st in keyword.lower() for st in ['ccsd', 'mp2']) and not scf_block): if calc_input.solvent.keyword is not None: logger.critical('nwchem only supports solvent for DFT calcs') raise UnsuppportedCalculationInput new_keywords.append(f'scf\n nopen {molecule.mult - 1}\nend') new_keywords.append(keyword) else: new_keywords.append(keyword) return new_keywords
def get_first_available_method(possibilities): """ Get the first electronic structure method that is available in a list of possibilities Arguments: possibilities (list(autode.wrappers.base.ElectronicStructureMethod)): Returns: (autode.wrappers.base.ElectronicStructureMethod): Method """ for method in possibilities: if method.available: return method logger.critical('No electronic structure methods available') raise MethodUnavailable
def _check_balance(self): """Check that the number of atoms and charge balances between reactants and products. If they don't exit immediately """ def total(molecules, attr): return sum([getattr(m, attr) for m in molecules]) if total(self.reacs, 'n_atoms') != total(self.prods, 'n_atoms'): logger.critical('Number of atoms doesn\'t balance') raise UnbalancedReaction if total(self.reacs, 'charge') != total(self.prods, 'charge'): logger.critical('Charge doesn\'t balance') raise UnbalancedReaction self.charge = total(self.reacs, 'charge') return None
def get_keywords(calc_input, molecule): """Get the keywords to use for a MOPAC calculation""" # To determine if there is an optimisation or single point the keywords # needs to be a subclass of Keywords assert isinstance(calc_input.keywords, Keywords) keywords = deepcopy(calc_input.keywords) if isinstance(calc_input.keywords, SinglePointKeywords): # Single point calculation add the 1SCF keyword to prevent opt if not any('1scf' in kw.lower() for kw in keywords): keywords.append('1SCF') if isinstance(calc_input.keywords, GradientKeywords): # Gradient calculation needs GRAD if not any('grad' in kw.lower() for kw in keywords): keywords.append('GRAD') # Gradient calculation add the 1SCF keyword to prevent opt if not any('1scf' in kw.lower() for kw in keywords): keywords.append('1SCF') if calc_input.point_charges is not None: keywords.append('QMMM') if calc_input.solvent is not None: dielectric = solvents_and_dielectrics[calc_input.solvent] keywords.append(f'EPS={dielectric}') # Add the charge and multiplicity keywords.append(f'CHARGE={molecule.charge}') if molecule.mult != 1: if molecule.mult == 2: keywords.append('DOUBLET') elif molecule.mult == 3: keywords.append('OPEN(2,2)') else: logger.critical('Unsupported spin multiplicity') raise UnsuppportedCalculationInput return keywords
def classify(reactants, products): if len(reactants) == 2 and len(products) == 1: logger.info('Classifying reaction as addition') return Addition elif len(reactants) == 1 and len(products) in [2, 3]: logger.info('Classifying reaction as dissociation') return Dissociation elif len(reactants) == 2 and len(products) == 2: logger.info('Classifying reaction as substitution') return Substitution elif len(reactants) == 2 and len(products) == 3: logger.info('Classifying reaction as elimination') return Elimination elif len(reactants) == 1 and len(products) == 1: logger.info('Classifying reaction as rearrangement') return Rearrangement elif len(reactants) == 0: logger.critical('Reaction had no reactants – cannot form a reaction') raise ReactionFormationFalied elif len(products) == 0: logger.critical('Reaction had no products – cannot form a reaction') raise ReactionFormationFalied else: logger.critical('Unsupported reaction type') raise NotImplementedError