Source code for pyssianutils.utils

"""
This module provides a set of functions with general utilities. as 
well as the basic variables for registering the subparsers and main functions
"""
import os
import re
import argparse
from pathlib import Path
from ._version import __version__
from pyssian.gaussianclasses import GaussianOutFile

from typing import Callable

# Core functions/Gobals for pyssianutils command line inner workings
MAINS = dict()
[docs] def register_main(f:Callable,name:None|str=None) -> Callable: """ Function used to store the main functions of each command of pyssianutils, abstracting it from how it is actually stored. Parameters ---------- f : Callable any function to be stored name : None | str, optional name used to store the function. If none is provided it will assume that the function name follows the convention "{something}_name", by default None Returns ------- Callable The function that was added. Allows the usage of register_main as a decorator. """ if name is None: command = f.__name__.split('_')[1] else: command = name MAINS[command] = f return f
[docs] def create_parser()-> tuple[argparse.ArgumentParser,argparse._SubParsersAction]: """ Creates the highest-level parser of the pyssianutils as well as the _SubparsersAction to which each subparser must be added. Returns ------- tuple[argparse.ArgumentParser,argparse._SubParsersAction] parser,subparsers """ parser = argparse.ArgumentParser(description=__doc__,prog='pyssianutils') parser.add_argument('--version', action='version', version=f'pyssianutils {__version__}') subparsers = parser.add_subparsers(help='sub-command help',dest='command') return parser, subparsers
[docs] def add_parser_as_subparser(subparsers:argparse._SubParsersAction, parser:argparse.ArgumentParser, name:str, **kwargs) -> argparse.ArgumentParser: """ Adds a previously existing parser as a subparser. This function is copied and adapted from the implementation of the function argparse._SubParsersAction.add_parser in python 3.12.11. Parameters ---------- subparsers : argparse._SubParsersAction Subparser action (which allows the standard .add_parser method) where the new parser will be added to. parser : argparse.ArgumentParser Existing parser that is going to be added as subparser. name : str value in the main parser to invoke the existing parser added as subparser **kwargs Returns ------- argparse.ArgumentParser The same existing parser that was provided as input. In order to match the behavior of argparse._SubParsersAction.add_parser Raises ------ argparse.ArgumentError Conflict in subparser name argparse.ArgumentError Conflict in subparser alias """ # set prog from the existing prefix if kwargs.get('prog') is None: kwargs['prog'] = '%s %s' % (subparsers._prog_prefix, name) aliases = kwargs.pop('aliases', ()) if name in subparsers._name_parser_map: raise argparse.ArgumentError(subparsers, f'conflicting subparser: {name}') for alias in aliases: if alias in subparsers._name_parser_map: raise argparse.ArgumentError(subparsers, f'conflicting subparser alias: {alias}') if 'help' in kwargs: help = kwargs.pop('help') choice_action = subparsers._ChoicesPseudoAction(name, aliases, help) subparsers._choices_actions.append(choice_action) # create the parser and add it to the map # This is uneeded cause it already exists: # parser = subparsers._parser_class(**kwargs) subparsers._name_parser_map[name] = parser # make parser available under aliases also for alias in aliases: subparsers._name_parser_map[alias] = parser return parser
# Other functions utility variables ALLOWEDMETHODS = ['oniom','mp2','mp2scs','mp4','ccsdt','default'] SCFCYCLE_PATTERN = re.compile(r'^\sE=\s?(-?[0-9]*\.[0-9]*)\s*Delta',re.MULTILINE) # Class utils
[docs] class DirectoryTree(object): """ Class that provides recursive file iteration search, iteration and recursive creation of directories following the same structure as the original one. """ def __init__(self,path:str|Path|os.PathLike,in_suffix:str,out_suffix:str): self.root = Path(path) self.cwd = Path(os.getcwd()) self.newroot = self.root self.in_suffix = in_suffix self.out_suffix = out_suffix def set_newroot(self,newroot:str|Path|os.PathLike): self.newroot = Path(newroot)
[docs] def newpath(self,path:str|Path|os.PathLike) -> Path: """ Takes a path rooted to the previous root and prepares it to be rooted in the new root. Parameters ---------- path : str | Path | os.PathLike path to a file relative to the DirectoryTree's root Returns ------- Path path of the file as if it was relative to the newroot instead of the root """ return self.newroot.joinpath(path.relative_to(self.root))
[docs] @staticmethod def RecursiveFileSystemGenerator(path,key=None): """ Generator that traverses the filesystem in depth first order and yields the paths that satisfy the key function condition. If no key function is provided, returns all folders and items. The first yield is always None, and the second yield is always the path value if it satisfies the key condition. Parameters ---------- path : Path Path pointing to a directory. key : function A function that returns a boolean. Used in a similar fashion as the key paramer of the 'sorted' python builtin. """ cls = DirectoryTree if key is None: key=lambda x: True yield None if key(path): yield path if path.is_dir(): for subpath in path.iterdir(): subgen =cls.RecursiveFileSystemGenerator(subpath,key=key) _ = next(subgen) for i in subgen: yield i
@property def folders(self): generator = self.RecursiveFileSystemGenerator _iter = generator(self.root,key=lambda x: x.is_dir()) _ = next(_iter) return _iter @property def infiles(self): def f_in(x): Out = False if x.exists() and x.suffix == self.in_suffix: Out = True return Out _iter = self.RecursiveFileSystemGenerator(self.root,key=f_in) _ = next(_iter) return _iter @property def outfiles(self): def f_out(x): Out = False if x.exists() and x.suffix == self.out_suffix: Out = True return Out _iter = self.RecursiveFileSystemGenerator(self.root,key=f_out) _ = next(_iter) return _iter
[docs] def create_folders(self): """ Creates the folders of a new Directory tree, rooted in self.newroot instead of self.root """ for folder in self.folders: newpath = self.newpath(folder) newpath.mkdir(parents=True,exist_ok=True)
# General Utils
[docs] def write_2_file(filepath:Path) -> Callable[[str],None]: """ Creates a wrapper for appending text to a certain File. Assumes that each call is equivalent to writing a single line. Parameters ---------- filepath : Path file where the output will be appended. Returns ------- Callable[[str],None] a function where each call writes the text and attaches a newline at the end of it. """ def Writer(txt): with open(filepath,'a') as F: F.write(txt) F.write('\n') return Writer
# GaussianOutFile utils
[docs] def thermochemistry(GOF:GaussianOutFile) -> tuple[float|None,float|None,float|None]: """ Returns the Zero Point Energy, Enthalpy and Free Energy from a frequency calculation. Parameters ---------- GOF : GaussianOutFile Gaussian Output File Instance. (It assumes that previously the .read() or .update() methods have been used) Returns ------- tuple[float|None,float|None,float|None] Zero point energy, Enthalpy, Free Energy """ Link = GOF[-1].get_links(716)[-1] Z = Link.zeropoint[-1] H = Link.enthalpy[-1] G = Link.gibbs[-1] return Z, H, G
def potential_energy(GOF:GaussianOutFile,method:str='default')-> float|None: f""" Returns the last potential energy of a GaussianOutFile of a certain method. The default is the energy of the SCF cycle ('SCF Done:') Parameters ---------- GOF : GaussianOutFile method : string For DFT and HF the default behavior is correct. For Post-HF methods it needs to be specified. Currently: {ALLOWEDMETHODS} Returns ------- float Energy """ assert method in ALLOWEDMETHODS if method == 'mp2': # Search for MP2 energy energy = GOF.get_links(804)[-1].MP2 elif method == 'mp2scs': HF = GOF.get_links(502)[-1].energy SCS_corr = GOF.get_links(804)[-1].get_SCScorr() energy = HF + SCS_corr elif method == 'ccsdt': # Search for CCSD(T) energy or default to MP4 Aux = GOF.get_links(913)[-1] energy = Aux.CCSDT elif method == 'mp4': Aux = GOF.get_links(913)[-1] energy = Aux.MP4 elif method == 'oniom': Aux = GOF.get_links(120)[-1] energy = Aux.energy else: # Otherwise go to the "Done(...)" Energy energy = None links = GOF.get_links(502,508) if links: energy = links[-1].energy if links and energy is None: energy = links[-2].energy return energy def potential_energies(GOF:GaussianOutFile,method:str='default',withscf:bool=False)-> list[float|None]: f""" Returns all potential energies of a GaussianOutFile of a certain method. The default is the energy of the SCF cycle ('SCF Done:') Parameters ---------- GOF : GaussianOutFile method : str For DFT and HF the default behavior is correct. For Post-HF methods it needs to be specified. Currently: {ALLOWEDMETHODS} withscf : bool If Enabled (and method is 'default') it will include the energies of the scf iterations. Returns ------- list[float] Energies """ assert method in ALLOWEDMETHODS if method == 'mp2': energies = [l.MP2 for l in GOF.get_links(804)] elif method == 'mp2scs': links502 = GOF.get_links(502) links804 = GOF.get_links(804) energies = [l0.energy + l1.get_SCScorr() for l0,l1 in zip(links502,links804)] elif method == 'ccsdt': energies = [l.CCSDT for l in GOF.get_links(913)] elif method == 'mp4': energies = [l.MP4 for l in GOF.get_links(913)] elif method == 'oniom': energies = [l.energy for l in GOF.get_links(120)] else: # Otherwise go to the "Done(...)" Energy energies = [] links502 = GOF.get_links(502) links508 = GOF.get_links(508) if not links508: for l502 in links502: if withscf: scf_energies = list(map(float,SCFCYCLE_PATTERN.findall(l502.text))) energies.extend(scf_energies) if scf_energies and (scf_energies[-1] != l502.energy): energies.append(l502.energy) energies.append(l502.energy) else: for l502,l508 in zip(links502,links508): if withscf: scf_energies = list(map(float,SCFCYCLE_PATTERN.findall(l502.text))) energies.extend(scf_energies) if l508.energy is not None: energies.append(l508.energy) elif scf_energies and (scf_energies[-1] != l502.energy): energies.append(l502.energy) else: energy = l502.energy if l508.energy is not None: energy = l508.energy energies.append(energy) return energies # Console Utils