"""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 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()