Source code for tesliper.tesliper

"""Provides a facade-like interface for easy access to ``tesliper``'s functionality.

There are some conventions that are important to note:

- ``tesliper`` stores multiple data entries of various types for each conformer. To
  prevent confusion with Python's data ``type`` and with data itself, ``tesliper``
  refers to specific kinds of data as "genres". Genres in code are represented by
  specific strings, used as identifiers. To learn about data genres known to
  ``tesliper``, see documentation for
  :class:`.GaussianParser`, which lists them.
- ``tesliper`` identifies conformers using stem of an extracted file (i.e. its filename
  without extension). When files with identical names are extracted in course of
  subsequent :meth:`.Tesliper.extract` calls or in recursive extraction using
  ``tesliper_object.extract(recursive=True)``, they are treated as data for one
  conformer. This enables to join data from subsequent calculations steps, e.g. geometry
  optimization, vibrational spectra simulation, and electronic spectra simulation.
  Please note that if specific data genre is available from more than one calculation
  job, only recently extracted values will be stored.
- ``tesliper`` was designed to deal with multiple conformers of single molecule and may
  not work properly when used to process data concerning different molecules (i.e.
  having different number of atoms, different number of degrees of freedom, etc.). If
  you want to use it for such purpose anyway, you may set
  :attr:`Tesliper.conformers.allow_data_inconsistency
  < .Conformers.allow_data_inconsistency>` to ``True``.
  ``tesliper`` will then stop complaining and try to do its best.
"""

# IMPORTS
import logging as lgg
import os
from pathlib import Path
from typing import (
    Callable,
    Dict,
    Generator,
    Iterable,
    Optional,
    Sequence,
    Set,
    Tuple,
    Union,
)

from tesliper.glassware.spectra import SingleSpectrum

from . import datawork as dw
from . import extraction as ex
from . import glassware as gw
from . import writing as wr
from .datawork.spectra import FittingFunctionType, Number

_DEVELOPMENT = "ENV" in os.environ and os.environ["ENV"] == "prod"


# LOGGER
logger = lgg.getLogger(__name__)

mainhandler = lgg.StreamHandler()
mainhandler.setLevel(lgg.DEBUG)
mainhandler.setFormatter(
    lgg.Formatter("%(levelname)s:%(name)s:%(funcName)s - %(message)s")
)

logger.setLevel(lgg.DEBUG if _DEVELOPMENT else lgg.WARNING)
logger.addHandler(mainhandler)

_activities_types = Union[
    gw.VibrationalActivities,
    gw.ScatteringActivities,
    gw.ElectronicActivities,
]


# CLASSES
[docs]class Tesliper: """This class is a main access point to ``tesliper``'s functionality. It allows you to extract data from specified files, provides a proxy to the trimming functionality, gives access to data in form of specialized arrays, enables you to calculate and average desired spectra, and provides an easy way to export data. Most basic use might look like this: >>> tslr = Tesliper() >>> tslr.extract() >>> tslr.calculate_spectra() >>> tslr.average_spectra() >>> tslr.export_averaged() This extracts data from files in the current working directory, calculates available spectra using standard parameters, averages them using available energy values, and exports to current working directory in .txt format. You can customize this process by specifying call parameters for used methods and modifying :class:`Tesliper`'s configuration attributes: - to change source directory or location of exported files instantiate :class:`Tesliper` object with :attr:`input_dir` and :attr:`output_dir` parameters specified, respectively. You can also set appropriate attributes on the instance directly. - To extract only selected files in :attr:`input_dir` use :attr:`wanted_files` init parameter. It should be given an iterable of filenames you want to parse. Again, you can also directly set an identically named attribute. - To change parameters used for calculation of spectra, modify appropriate entries of :attr:`parameters` attribute. - Use other export methods to export more data and specify ``fmt`` parameter in method's call to export to other file formats. >>> tslr = Tesliper(input_dir="./myjob/optimization/", output_dir="./myjob/output/") >>> tslr.wanted_files = ["one", "two", "three"] # only files with this names >>> tslr.extract() # use tslr.input_dir as source >>> tslr.extract(path="./myjob/vcd_sim/") # use other input_dir >>> tslr.conformers.trim_not_optimized() # trimming out unwanted conformers >>> tslr.parameters["vcd"].update({"start": 500, "stop": 2500, "width": 2}) >>> tslr.calculate_spectra(genres=["vcd"]) # we want only VCD spectrum >>> tslr.average_spectra() >>> tslr.export_averaged(mode="w") # overwrite previously exported files >>> tslr.export_activities(fmt="csv") # save activities for analysis elsewhere >>> tslr.output_dir = "./myjob/ecd_sim/" >>> tslr.export_job_file( # prepare files for next step of calculations ... route="# td=(singlets,nstates=80) B3LYP/Def2TZVP" ... ) When modifying :attr:`Tesliper.parameters` be careful to not delete any of the parameters. If you need to revert to standard parameters values, you can find them in :attr:`Tesliper.standard_parameters`. >>> tslr.parameters["ir"] = { ... "start": 500, "stop": 2500, "width": 2 ... } # this will cause problems! >>> tslr.parameters = tslr.standard_parameters # revert to default values Trimming functionality, used in previous example in ``tslr.conformers.trim_not_optimized()``, allows you to filter out conformers that shouldn't be used in further processing and analysis. You can trim off conformers that were not optimized, contain imaginary frequencies, or have other unwanted qualities. Conformers with similar geometry may be discarded using an RMSD sieve. For more information about trimming, please refer to the documentation of :class:`.Conformers` class. For more exploratory analysis, :class:`Tesliper` provides an easy way to access desired data as an instance of specialized :class:`.DataArray` class. Those objects implement a number of convenience methods for dealing with specific data genres. A more detailed information on :class:`.DataArray` see :mod:`.arrays` module documentation. To get data in this form use ``array = tslr["genre"]`` were ``"genre"`` is string with the name of desired data genre. For more control over instantiation of :class:`.DataArray` you may use :meth:`Tesliper.conformers.arrayed <.Conformers.arrayed>` factory method. >>> energies = tslr["gib"] >>> energies.values array([-304.17061762, -304.17232455, -304.17186735]) >>> energies.populations array([0.0921304 , 0.56174031, 0.3461293 ]) >>> energies.full_name 'Thermal Free Energy' Please note, that if some conformers do not provide values for a specific data genre, it will be ignored when retriving data for :class:`.DataArray` instantiation, regardles if it were trimmed off or not. >>> tslr = Tesliper() >>> tslr.conformers.update([ >>> ... ('one', {'gib': -304.17061762}), >>> ... ('two', {'gib': -304.17232455}), >>> ... ('three', {'gib': -304.17186735}), >>> ... ('four', {}) >>> ... ]) >>> tslr.conformers.kept [True, True, True, True] >>> energies = tslr["gib"] >>> energies.filenames array(['one', 'two', 'three'], dtype='<U5') Attributes ---------- conformers : Conformers Container for data extracted from Gaussian output files. It provides trimming functionality, enabling to filter out conformers of unwanted qualities. spectra : dict of str: Spectra Spectra calculated so far, using :meth:`.calculate_spectra` method. Possible keys are spectra genres: "ir", "vcd", "uv", "ecd", "raman", and "roa". Values are :class:`.Spectra` instances with lastly calculated spetra of this genre. averaged : dict of str: (dict of str: float or callable) Spectra averaged using available energies genres, calculated with last call to :meth:`.average_spectra` method. Keys are tuples of two strings: averaged spectra genre and energies genre used for averaging. experimental : dict of str: Spectra Experimental spectra loaded from disk. Possible keys are spectra genres: "ir", "vcd", "uv", "ecd", "raman", and "roa". Values are :class:`.Spectra` instances with experimental spetra of this genre. quantum_software : str A name, lower case, of the quantum chemical computations software used to obtain data. Used by ``tesliper`` to figure out, which parser to use to extract data, if custom parsers are available. Only "gaussian" is supported out-of-the-box. parameters : dict of str: (dict of str: float or callable) Parameters for calculation of each spectra genres: "ir", "vcd", "uv", "ecd", "raman", and "roa". Avaliable parameters are: - "start": float or int, the beginning of the spectral range, - "stop": float or int, the end of the spectral range, - "step": float or int, step of the abscissa, - "width": float or int, width of the peak, - "fitting": callable, function used to simulate peaks as curves, preferably one of :func:`datawork.gaussian <.gaussian>` or :func:`datawork.lorentzian <.lorentzian>`. "start", "stop", and "step" expect its values to by in cm^-1 units for vibrational and scattering spectra, and nm units for electronic spectra. "width" expects its value to be in cm^-1 units for vibrational and scattering spectra, and eV units for electronic spectra. """ # TODO?: add proxy for trimming ? # TODO?: make it inherit mapping ? _standard_parameters = { "ir": { "width": 6, "start": 800, "stop": 2900, "step": 2, "fitting": dw.lorentzian, }, "uv": { "width": 0.35, "start": 150, "stop": 800, "step": 1, "fitting": dw.gaussian, }, } _standard_parameters["vcd"] = _standard_parameters["ir"].copy() _standard_parameters["raman"] = _standard_parameters["ir"].copy() _standard_parameters["roa"] = _standard_parameters["ir"].copy() _standard_parameters["ecd"] = _standard_parameters["uv"].copy() # TODO: introduce more sophisticated parameters proxy that enables using # same or different params for genres of same type (e.g. "vibrational") def __init__( self, input_dir: str = ".", output_dir: str = ".", wanted_files: Optional[Iterable[Union[str, Path]]] = None, quantum_software: str = "gaussian", ): """ Parameters ---------- input_dir : str or path-like object, optional Path to directory containing files for extraction, defaults to current working directory. output_dir : str or path-like object, optional Path to directory for output files, defaults to current working directory. wanted_files : list of str or list of Path, optional List of files or filenames representing wanted files. If not given, all files are considered wanted. File extensions are ignored. quantum_software : str A name of the quantum chemical computations software used to obtain data. Used by ``tesliper`` to figure out, which parser to use, if custom parsers are available. """ self.conformers = gw.Conformers() self.wanted_files = wanted_files self.input_dir = input_dir self.output_dir = output_dir self.spectra = dict() self.averaged = dict() self.experimental = dict() self.parameters = self.standard_parameters self.quantum_software = quantum_software.lower() if self.quantum_software not in ex.parser_base._PARSERS: logger.warning( f"Unsupported quantum chemistry software: {quantum_software}. " "Automatic data extraction will not be available." ) def __getitem__(self, item: str) -> gw.conformers.AnyArray: try: return self.conformers.arrayed(item) except ValueError: raise KeyError(f"Unknown genre '{item}'.")
[docs] def clear(self): """Remove all data from the instance.""" self.conformers.clear() self.wanted_files = [] self.input_dir = "" self.output_dir = "" self.spectra = dict() self.averaged = dict() self.experimental = dict() self.parameters = self.standard_parameters
@property def temperature(self) -> float: """Temperature of the system expressed in Kelvin units. Value of this parameter is passed to :term:`data array`\\s created with the :meth:`.Conformers.arrayed` method, provided that the target data array class supports a parameter named *t* in it's constructor. .. versionadded:: 0.9.1 Raises ------ ValueError if set to a value lower than zero. Notes ----- It's actually just a proxy to :meth:`self.conformers.temperatue <.Conformers.temperature>`. """ return self.conformers.temperature @temperature.setter def temperature(self, value): self.conformers.temperature = value @property def energies(self) -> Dict[str, gw.Energies]: """Data for each energies' genre as :class:`.Energies` data array. Returned dictionary is of form {"genre": :class:`.Energies`} for each of the genres: "scf", "zpe", "ten", "ent", and "gib". If no values are available for a specific genre, an empty :class:`.Energies` array is produced as corresponding dictionary value. >>> tslr = Tesliper() >>> tslr.energies { "scf": Energies(genre="scf", ...), "zpe": Energies(genre="zpe", ...), "ten": Energies(genre="ten", ...), "ent": Energies(genre="ent", ...), "gib": Energies(genre="gib", ...), } Returns ------- dict Dictionary with genre names as keys and :class:`.Energies` data arrays as values. """ keys = gw.Energies.associated_genres return {k: self[k] for k in keys} @property def activities(self) -> Dict[str, _activities_types]: """Data for default activities used to calculate spectra as appropriate :class:`.SpectralActivities` subclass. Returned dictionary is of form {"genre": :class:`.SpectralActivities`} for each of the genres: "dip", "rot", "vosc", "vrot", "raman1", and "roa1". If no values are available for a specific genre, an empty data array is produced as corresponding dictionary value. >>> tslr = Tesliper() >>> tslr.activities { "dip": VibrationalActivities(genre="dip", ...), "rot": VibrationalActivities(genre="rot", ...), "vosc": ElectronicActivities(genre="vosc", ...), "vrot": ElectronicActivities(genre="vrot", ...), "raman1": ScatteringActivities(genre="raman1", ...), "roa1": ScatteringActivities(genre="roa1", ...), } Returns ------- dict Dictionary with genre names as keys and :class:`.SpectralActivities` data arrays as values. """ keys = dw.DEFAULT_ACTIVITIES.values() return {k: self[k] for k in keys} @property def wanted_files(self) -> Optional[Set[str]]: """Set of files that are desired for data extraction, stored as filenames without an extension. Any iterable of strings or Path objects is transformed to this form. >>> tslr = Tesliper() >>> tslr.wanted_files = [Path("./dir/file_one.out"), Path("./dir/file_two.out")] >>> tslr.wanted_files {"file_one", "file_two"} May also be set to ``None`` or other "falsy" value, in such case it is ignored. """ return self._wanted_files @wanted_files.setter def wanted_files(self, files: Optional[Iterable[Union[str, Path]]]): self._wanted_files = None if not files else {Path(f).stem for f in files} @property def standard_parameters(self) -> Dict[str, Dict[str, Union[int, float, Callable]]]: """Default parameters for spectra calculation for each spectra genre (ir, vcd, uv, ecd, raman, roa). This returns a dictionary, but in fact it is a convenience, read-only attribute, modifying it will have no persisting effect. """ return {key: params.copy() for key, params in self._standard_parameters.items()}
[docs] def update(self, other: Optional[Dict[str, dict]] = None, **kwargs): """Update stored conformers with given data. Works like ``dict.update``, but if key is already present, it updates dictionary associated with given key rather than assigning new value. Keys of dictionary passed as positional parameter (or additional keyword arguments given) should be conformers' identifiers and its values should be dictionaries of ``{"genre": values}`` for those conformers. Please note, that values of status genres like 'optimization_completed' and 'normal_termination' will be updated as well for such key, if are present in given new values. >>> tslr.conformers Conformers([('one', {'scf': -100, 'stoichiometry': 'CH4'})]) >>> tslr.update( ... {'one': {'scf': 97}, 'two': {'scf': 82, 'stoichiometry': 'CH4'}} ... ) >>> tslr.conformers Conformers([ ('one', {'scf': 97, 'stoichiometry': 'CH4'}), ('two', {'scf': 82, 'stoichiometry': 'CH4'}), ]) """ self.conformers.update(other, **kwargs)
@property def input_dir(self) -> Path: """Directory, from which files should be read.""" return self.__input_dir @input_dir.setter def input_dir(self, path: Union[Path, str] = "."): path = Path(path).resolve() if not path.is_dir(): raise FileNotFoundError( "Invalid path or directory not found: {}".format(path) ) logger.info("Current working directory is: {}".format(path)) self.__input_dir = path @property def output_dir(self) -> Path: """Directory, to which generated files should be written.""" return self.__output_dir @output_dir.setter def output_dir(self, path: Union[Path, str] = "."): path = Path(path).resolve() path.mkdir(exist_ok=True) logger.info("Current output directory is: {}".format(path)) self.__output_dir = path
[docs] def extract_iterate( self, path: Optional[Union[str, Path]] = None, wanted_files: Optional[Iterable[str]] = None, extension: Optional[str] = None, recursive: bool = False, ) -> Generator[Tuple[str, dict], None, None]: """Extracts data from chosen Gaussian output files present in given directory and yields data for each conformer found. Uses :attr:`Tesliper.input_dir` as source directory and :attr:`Tesliper.wanted_files` list of chosen files if these are not explicitly given as 'path' and 'wanted_files' parameters. Parameters ---------- path : str or pathlib.Path, optional Path to directory, from which Gaussian files should be read. If not given or is ``None``, :attr:`Tesliper.output_dir` will be used. wanted_files : list of str, optional Filenames (without a file extension) of conformers that should be extracted. If not given or is ``None``, :attr:`Tesliper.wanted_files` will be used. If :attr:`Tesliper.wanted_files` is also ``None``, all found Gaussian output files will be parsed. extension : str, optional Only files with given extension will be parsed. If omitted, Tesliper will try to guess the extension from contents of input directory. recursive : bool If ``True``, also subdirectories are searched for files to parse, otherwise subdirectories are ignored. Defaults to ``False``. Yields ------ tuple Two item tuple with name of parsed file as first and extracted data as second item, for each Gaussian output file parsed. """ soxhlet = ex.Soxhlet( path=path or self.input_dir, purpose=self.quantum_software, wanted_files=wanted_files or self.wanted_files, extension=extension, recursive=recursive, ) for file, data in soxhlet.extract_iter(): self.update(((file, data),)) yield file, data
[docs] def extract( self, path: Optional[Union[str, Path]] = None, wanted_files: Optional[Iterable[str]] = None, extension: Optional[str] = None, recursive: bool = False, ): """Extracts data from chosen Gaussian output files present in given directory. Uses :attr:`Tesliper.input_dir` as source directory and :attr:`Tesliper.wanted_files` list of chosen files if these are not explicitly given as *path* and *wanted_files* parameters. Parameters ---------- path : str or pathlib.Path, optional Path to directory, from which Gaussian files should be read. If not given or is ``None``, :attr:`Tesliper.output_dir` will be used. wanted_files : list of str, optional Filenames (without a file extension) of conformers that should be extracted. If not given or is ``None``, :attr:`Tesliper.wanted_files` will be used. extension : str, optional Only files with given extension will be parsed. If omitted, Tesliper will try to guess the extension from contents of input directory. recursive : bool If ``True``, also subdirectories are searched for files to parse, otherwise subdirectories are ignored. Defaults to ``False``. """ for f, d in self.extract_iterate(path, wanted_files, extension, recursive): _ = f, d
[docs] def load_parameters( self, path: Union[str, Path], spectra_genre: Optional[str] = None, ) -> dict: """Load calculation parameters from a file. Parameters ---------- path : str or pathlib.Path, optional Path to the file with desired parameters specification. spectra_genre : str, optional Genre of spectra that loaded parameters concerns. If given, should be one of "ir", "vcd", "uv", "ecd", "raman", or "roa" -- parameters for that spectra will be updated with loaded values. Otherwise no update is done, only parsed data is returned. Returns ------- dict Parameters read from the file. Notes ----- For information on supported format of parameters configuration file, please refer to :class:`.ParametersParser` documentation. """ soxhlet = ex.Soxhlet(self.input_dir, purpose="parameters") settings = soxhlet.parse_one(path) if spectra_genre is not None: self.parameters[spectra_genre].update(settings) return settings
[docs] def load_experimental( self, path: Union[str, Path], spectrum_genre: str, ) -> SingleSpectrum: """Load experimental spectrum from a file. Data read from file is stored as :class:`.SingleSpectrum` instance in :attr:`.Tesliper.experimental` dictionary under *spectrum_genre* key. Parameters ---------- path : str or pathlib.Path Path to the file with experimental spectrum. spectrum_genre : str Genre of the experimental spectrum that will be loaded. Should be one of "ir", "vcd", "uv", "ecd", "raman", or "roa". Returns ------- SingleSpectrum Experimental spectrum loaded from the file. """ soxhlet = ex.Soxhlet(self.input_dir, purpose="spectra") spc = soxhlet.parse_one(path) self.experimental[spectrum_genre] = gw.SingleSpectrum( genre=spectrum_genre, values=spc[1], abscissa=spc[0] ) return self.experimental[spectrum_genre]
[docs] def calculate_single_spectrum( self, genre: str, conformer: Union[str, int], start: Number = None, stop: Number = None, step: Number = None, width: Number = None, fitting: FittingFunctionType = None, ) -> gw.SingleSpectrum: """Calculates spectrum for requested conformer. 'start', 'stop', 'step', 'width', and 'fitting' parameters, if given, will be used instead of the parameters stored in :attr:`Tesliper.parameters` attribute. 'start', 'stop', and 'step' values will be interpreted as cm^-1 for vibrational or scattering spectra/activities and as nm for electronic ones. Similarly, 'width' will be interpreted as cm^-1 or eV. If not given, values stored in appropriate :attr:`Tesliper.parameters` are used. Parameters ---------- genre : str Spectra genre (or related spectral activities genre) that should be calculated. If given spectral activity genre, this genre will be used to calculate spectra instead of the default activities. conformer : str or int Conformer, specified as it's identifier or it's index, for which spectrum should be calculated. start : int or float, optional Number representing start of spectral range. stop : int or float, optional Number representing end of spectral range. step : int or float, optional Number representing step of spectral range. width : int or float, optional Number representing half width of maximum peak height. fitting : function, optional Function, which takes spectral data, freqs, abscissa, width as parameters and returns numpy.array of calculated, non-corrected spectrum points. Basically one of :func:`datawork.gaussian <.gaussian>` or :func:`datawork.lorentzian <.lorentzian>`. Returns ------- SingleSpectrum Calculated spectrum. """ try: bar_name = dw.DEFAULT_ACTIVITIES[genre] except KeyError: bar_name = genre with self.conformers.trimmed_to([conformer]): bar = self[bar_name] sett_from_args = { k: v for k, v in zip( ("start", "stop", "step", "width", "fitting"), (start, stop, step, width, fitting), ) if v is not None } sett = self.parameters[bar.spectra_name].copy() sett.update(sett_from_args) spc = bar.calculate_spectra(**sett) # TODO: maybe Spectra class should provide such conversion ? return gw.SingleSpectrum( spc.genre, spc.values[0], spc.abscissa, spc.width, spc.fitting, scaling=spc.scaling, offset=spc.offset, filenames=spc.filenames, )
[docs] def calculate_spectra(self, genres: Iterable[str] = ()) -> Dict[str, gw.Spectra]: """Calculates spectra for each requested genre using parameters stored in :attr:`Tesliper.parameters` attribute. Parameters ---------- genres : iterable of str List of spectra genres (or related spectral activities genres) that should be calculated. If given spectral activity genre, this genre will be used to calculate spectra instead of the default activities. If given empty sequence (default), all available spectra will be calculated using default activities. Returns ------- dict of str: Spectra Dictionary with calculated spectra genres as keys and :class:`.Spectra` objects as values. """ if not genres: # use default genres, ignoring empty bars = (v for v in self.activities.values() if v) else: # convert to spectra name if bar name passed default_act = dw.DEFAULT_ACTIVITIES genres = genres.split() if isinstance(genres, str) else genres query = [default_act[v] if v in default_act else v for v in genres] query_set = set(query) # ensure no duplicates bars = (self[g] for g in query_set) output = {} for bar in bars: spectra = bar.calculate_spectra(**self.parameters[bar.spectra_name]) if spectra: output[bar.spectra_name] = spectra else: # should empty spectra be included in output? logger.warning( f"No data for {bar.spectra_name} calculation; " f"appropriate data is not available or was trimmed off." ) self.spectra.update(output) return output
[docs] def get_averaged_spectrum( self, spectrum: str, energy: str, temperature: Optional[float] = None ) -> gw.SingleSpectrum: """Average previously calculated spectra using populations derived from specified energies. .. versionadded:: 0.9.1 The optional *temperature* parameter. .. versionchanged:: 0.9.1 If spectra needed for averaging was not calulated so far, it will try to calulate it instead of raising a KeyError. Parameters ---------- spectrum : str Genre of spectrum that should be averaged. This spectrum should be previously calculated using :meth:`.calculate_spectra` method. energy : str Genre of energies, that should be used to calculate populations of conformers. These populations will be used as weights for averaging. temperature : float, optional Temperature used for calculation of the Boltzmann distribution for spectra averaging. If not given, :meth:`Tesliper.temperature` value is used. Returns ------- SingleSpectrum Calculated averaged spectrum. Raises ------ ValueError If no data for calculation of requested spectrum is available. """ try: spectra = self.spectra[spectrum] except KeyError: array = self[dw.DEFAULT_ACTIVITIES[spectrum]] spectra = array.calculate_spectra(**self.parameters[spectrum]) if not spectra: raise ValueError( f"No data for {spectrum} calculation; " f"appropriate data is not available or was trimmed off." ) with self.conformers.trimmed_to(spectra.filenames): en = ( self[energy] if temperature is None else self.conformers.arrayed(genre=energy, t=temperature) ) output = spectra.average(en) return output
[docs] def average_spectra(self) -> Dict[Tuple[str, str], gw.SingleSpectrum]: """For each previously calculated spectra (stored in :attr:`Tesliper.spectra` attribute) calculate it's average using population derived from each available energies genre. Returns ------- dict Averaged spectrum for each previously calculated spectra and energies known as a dictionary. It's keys are tuples of genres used for averaging and values are :class:`.SingleSpectrum` instances (so this dictionary is of form {tuple("spectra", "energies"): :class:`.SingleSpectrum`}). """ for genre, spectra in self.spectra.items(): with self.conformers.trimmed_to(spectra.filenames): for energies in self.energies.values(): if energies: av = spectra.average(energies) self.averaged[(genre, energies.genre)] = av return self.averaged
[docs] def export_data(self, genres: Sequence[str], fmt: str = "txt", mode: str = "x"): """Saves specified data genres to disk in given file format. File formats available by default are: "txt", "csv", "xlsx", "gjf". Note that not all formats may are compatible with every genre (e.g. only genres associated with :class:`.Geometry` may be exported fo .gjf format). In such case genres unsupported by given format are ignored. Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. If there are no values for given genre, no files will be created for this genre. Parameters ---------- genres : list of str List of genre names, that will be saved to disk. fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) data = (self[g] for g in genres) data = [d for d in data if d] if any(isinstance(arr, gw.arrays._VibData) for arr in data): data += [self["freq"]] if any(isinstance(arr, (gw.ElectronicData, gw.Transitions)) for arr in data): data += [self["wavelen"]] wrt.write(data)
[docs] def export_energies(self, fmt: str = "txt", mode: str = "x"): """Saves energies and population data to disk in given file format. File formats available by default are: "txt", "csv", "xlsx". Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. Parameters ---------- fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) energies = [e for e in self.energies.values() if e] corrections = (self[f"{e.genre}corr"] for e in energies if e.genre != "scf") frequencies = self["freq"] stoichiometry = self["stoichiometry"] wrt.write(data=[*energies, frequencies, stoichiometry, *corrections])
[docs] def export_spectral_data(self, fmt: str = "txt", mode: str = "x"): """Saves unprocessed spectral data to disk in given file format. File formats available by default are: "txt", "csv", "xlsx". Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. Parameters ---------- fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) bands = [self["freq"], self["wavelen"]] genres = ( *gw.VibrationalData.associated_genres, *gw.ElectronicData.associated_genres, *gw.ScatteringData.associated_genres, ) data = (self[g] for g in genres) data = [d for d in data if d] # ignore empty DataArrays data += [b for b in bands if b] wrt.write(data)
[docs] def export_activities(self, fmt: str = "txt", mode: str = "x"): """Saves unprocessed spectral activities to disk in given file format. File formats available by default are: "txt", "csv", "xlsx". Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. Parameters ---------- fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) bands = [self["freq"], self["wavelen"]] genres = ( *gw.VibrationalActivities.associated_genres, *gw.ElectronicActivities.associated_genres, *gw.ScatteringActivities.associated_genres, ) data = (self[g] for g in genres) data = [d for d in data if d] # ignore empty DataArrays data += [b for b in bands if b] wrt.write(data)
[docs] def export_spectra(self, fmt: str = "txt", mode: str = "x"): """Saves spectra calculated previously to disk in given file format. File formats available by default are: "txt", "csv", "xlsx". Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. Parameters ---------- fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) data = [s for s in self.spectra.values() if s] wrt.write(data)
[docs] def export_averaged(self, fmt: str = "txt", mode: str = "x"): """Saves spectra calculated and averaged previously to disk in given file format. File formats available by default are: "txt", "csv", "xlsx". Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using adequate genre's name and conformers' identifiers. In case of "xlsx" format only one file is produced and different data genres are written to separate sheets. Parameters ---------- fmt : str File format of output files, defaults to "txt". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". """ wrt = wr.writer(fmt=fmt, destination=self.output_dir, mode=mode) data = [s for s in self.averaged.values() if s] wrt.write(data)
[docs] def export_job_file( self, fmt: str = "gjf", mode: str = "x", geometry_genre: str = "last_read_geom", **kwargs, ): """Saves conformers to disk as job files for quantum chemistry software in given file format. Currently only "gjf" format is provided, used by Gaussian software. Files produced are written to :attr:`Tesliper.output_dir` directory with filenames automatically generated using conformers' identifiers. Parameters ---------- fmt : str File format of output files, defaults to "gjf". mode : str Specifies how writing to file should be handled. May be one of: "a" (append to existing file), "x" (only write if file doesn't exist yet), "w" (overwrite file if it already exists). Defaults to "x". geometry_genre : str Name of the data genre representing conformers' geometry that should be used as input geometry. Please note that the default value "last_read_geom" is not necessarily an optimized geometry. Use "optimized_geom" if this is what you need. kwargs Any additional keyword parameters are passed to the writer object, relevant to the *fmt* requested. Keyword supported by the default :class:`"gjf"-format writer <.GjfWriter>` are as follows: route A calculations route: keywords specifying calculations directives for quantum chemical calculations software. link0 Dictionary with "link zero" commands, where each key is command's name and each value is this command's parameter. comment Contents of title section, i.e. a comment about the calculations. post_spec Anything that should be placed after conformer's geometry specification. Will be written to the file as given. """ wrt = wr.writer( fmt=fmt, destination=self.output_dir, mode=mode, **kwargs, ) wrt.geometry( geometry=self[geometry_genre], multiplicity=self["multiplicity"], charge=self["charge"], )
[docs] def serialize(self, filename: str = ".tslr", mode: str = "x") -> None: """Serialize instance of :class:`Tesliper` object to a file in :attr:`.output_dir`. Parameters ---------- filename: str Name of the file, to which content will be written. Defaults to ".tslr". mode: str Specifies how writing to file should be handled. Should be one of characters: "x" or "w". "x" - only write if file doesn't exist yet; "w" - overwrite file if it already exists. Defaults to "x". Raises ------ ValueError If given any other ``mode`` than "x" or "w". Notes ----- If :attr:`.output_dir` is ``None``, current working directory is assumed. """ path = self.output_dir / filename if mode not in {"x", "w"}: raise ValueError( f"'{mode}' is not a valid mode for serializing Tesliper object. " f"It should be 'x' or 'w'." ) writer = wr.ArchiveWriter(destination=path, mode=mode) writer.write(self)
[docs] @classmethod def load(cls, source: Union[Path, str]) -> "Tesliper": """Load serialized :class:`Tesliper` object from given file. Parameters ---------- source: pathlib.Path or str Path to the file with serialized Tesliper object. Returns ------- Tesliper New instance of Tesliper class containing data read from the file. """ path = Path(source) loader = wr.ArchiveLoader(source=path) return loader.load()