Source code for tesliper.writing.writer_base

"""Interface for witing data to disk.

This module contains :func:`.writer` factory function that enables to dynamically create
a writer object that's responsible for saving data in a desired output format.
:func:`.writer` instantiates a subclass of :class:`WriterBase`, an Abstract Base Class
also defined here. :class:`WriterBase` provides an interface for all serial data writers
(objects that export conformers' data to multiple files) used by ``tesliper``.

:class:`WriterBase` expects it's subclasses to provide an *extention* class attribute,
which is used as an extension of files produced by this particular writer, and also as
an identifier for the output format, used by the :func:`.writer` factory function.
``tesliper`` is shipped with four such writers: :class:`.TxtWriter` for writting to .txt
files, :class:`.CsvWriter` for writting in CSV format, :class:`.XlsxWriter` for
creating Excel files, and :class:`.GjfWriter` for preparing Gaussian input files.

You may want to export your data to other file formats - in such case you will need to
implement your own writer. To do this, subclass :class:`WriterBase`, provide it's
*extension* as mentioned above, and implement writing methods for data you intend to
support in your writer. The table below lists these methods, along with a brief
description and :class:`.DataArray`-like object, for which the method will be called by
writer's :meth:`~.WriterBase.write` method.

.. list-table:: Methods used by default to write certain data
    :header-rows: 1

    * - Writer's Method
      - Description
      - Associated array
    * - :meth:`~.WriterBase.generic`
      - Generic data: any genre that provides one value for each conformer.
      - :class:`.DataArray`, :class:`.IntegerArray`,
        :class:`.FloatArray`, :class:`.BooleanArray`, :class:`.InfoArray`.
    * - :meth:`~.WriterBase.overview`
      - General information about conformers: energies, imaginary frequencies,
        stoichiometry.
      - :class:`.Energies`
    * - :meth:`~.WriterBase.energies`
      - Detailed information about conformers' relative energy,
        including calculated populations
      - :class:`.Energies`
    * - :meth:`~.WriterBase.single_spectrum`
      - A spectrum - calculated for single conformer or averaged.
      - :class:`.SingleSpectrum`
    * - :meth:`~.WriterBase.spectral_data`
      - Data related to spectral activity, but not convertible to spectra.
      - :class:`.SpectralData`
    * - :meth:`~.WriterBase.spectral_activities`
      - Data that may be used to simulate conformers' spectra.
      - :class:`.SpectralActivities`
    * - :meth:`~.WriterBase.spectra`
      - Spectra for multiple conformers.
      - :class:`.Spectra`
    * - :meth:`~.WriterBase.transitions`
      - Electronic transitions from ground to excited state, contributing to each band.
      - :class:`.Transitions`
    * - :meth:`~.WriterBase.geometry`
      - Geometry (positions of atoms in space) of conformers.
      - :class:`.Geometry`

.. note::

    These methods are not abstract methods, but will still raise a
    ``NotImplementedError`` if called. This is to let you omit implementation of methods
    you don't need or wouldn't make sense for the particular format and still provide an
    abstract interface. ``tesliper`` takes advantage of this in it's implementation of
    :class:`.GjfWriter`, which only implements :meth:`~.GjfWriter.geometry` method,
    because export of, e.g. a calculated spectrum as a Gaussian input would be
    pointless.

Writer object decides which of these methods to call based on the type of each
:class:`.DataArray`-like object passed to the :meth:`~.WriterBase.write` method. For
some of them, it also passes additional :class:`.DataArray`-like objects, referred to as
*extras*, e.g. correspomding :class:`.Bands` for spectral data. See documentation for
particular method to learn, which of its parameters are mandatory, which are optional,
and which should expect ``None`` as a possible value of *extra*.

When implementing one of these methods in your writer, you should take care of opening
and closing file files, formatting data you export, and writing to the file. For the
first part you may use one of the helper methods that provide a ready-to-use file
handles: :meth:`~.WriterBase._iter_handles` for writing to many files in batch or
:meth:`~.WriterBase._get_handle` for writing to one file only. Both require a template
that will be used to generate filename for produced files. To learn more about how these
templates are handled by ``tesliper``, see :meth:`~.WriterBase.make_name` documentation.

As mentioned before, writer object uses type of the :class:`.DataArray`-like object (or,
more precisely, a name of its class) to decide which method to use for writing to disk.
If you introduce a new subclass of :class:`.DataArray` for handling some genres, you
will need to tell the Writer class, how it should handle these new objects. This is done
by implementing a custom handler method. It's name should begin with an underscore,
followed by the name of your subclass in lower case, followed by "_handler". Also, it
should take two parameters: *data* and *extras*. First one is a list of instances of
your subclass, second one is a dictionary of special-case genres, both retrieved from
arguments given to :meth:`~.WriterBase.write` method (for details on which genres as
treated as special cases, see :meth:`~.WriterBase.distribute_data`). Handler is
responsible for calling appropriate writing method with arguments it needs.

Here is an example: let's assume you have implemented a custom :class:`.DataArray`
subclass for "ldip" and "lrot" genres with some additional functionality, but you'd like
``tesliper`` to treat it as the original :class:`.ElectronicActivities` class for
purposes of writing to disk.

.. code-block:: python

    class LengthActivities(ElectronicActivities):
        associated_genres = ("ldip", "lrot")
        ...  # custom functionality implemented here

    class UpdatedTxtWriter(TxtWriter):
        extension = "txt"

        def _lengthactivities_handler(self, data, extras):
            # written like ``ElectronicActivities``, so just delegate to its handler
            self._electronicactivities_handler(data, extras)

If you'd like to treat this new subclass differently, then you should provide a custom
writting method for this kind of data:

.. code-block:: python

    class UpdatedTxtWriter(TxtWriter):
        extension = "txt"

        def length_activities(
            self,
            band: Bands,
            data: List[LengthActivities],
            name_template: Union[str, Template] = "${conf}.${cat}-${det}.${ext}",
        ):
            # we will use ``_iter_handles`` method for opening/closing files
            template_params = {"genre": band.genre, "cat": "activity", "det": "length"}
            handles = self._iter_handles(band.filenames, name_template, template_params)
            # we will iterate conformer by conformer
            values = zip(*[arr.values for arr in data])
            for values, handle in zip(values, handles):
                ...  # writting logic

        def _lengthactivities_handler(self, data, extras):
            self.length_activities(band=extras["wavelengths"], data=data)

In both cases ``UpdatedTxtWriter`` will be picked by the :func:`.writer` instead of the
original :class:`.TxtWriter`, thanks to the automatic registration done by the base
class :class:`.WriterBase`.

.. warning::

    If ``extension = "txt"`` line would be omitted in the ``UpdatedTxtWriter``
    definition, it would be picked by the :func:`.writer` for "txt" format anyway,
    because ``extension``'s value would be inherited from :class:`.TxtWriter`.
    If you want to prevent this, you can provide a falsy value for the ``extension``
    class attribute, i.e. an empty string or ``None``.
    If your custom writer should still use the same extension as one of the default
    writers, provide ``extension`` also as an instance-level attribute:

    .. code-block:: python

        class UpdatedTxtWriter(TxtWriter):
            extension = ""  # do not register

            def __init__(self, destination, mode):
                super().__init__(destination, mode)
                self.extension = "txt"  # use in generated filenames
"""
import logging as lgg
from abc import ABC, abstractmethod
from contextlib import contextmanager
from pathlib import Path
from string import Formatter, Template
from typing import (
    IO,
    Any,
    AnyStr,
    Dict,
    Iterable,
    Iterator,
    List,
    Optional,
    Sequence,
    Tuple,
    Type,
    Union,
)

from ..glassware.arrays import (
    Bands,
    BooleanArray,
    DataArray,
    ElectronicActivities,
    ElectronicData,
    Energies,
    FloatArray,
    Geometry,
    InfoArray,
    IntegerArray,
    ScatteringActivities,
    ScatteringData,
    SpectralActivities,
    SpectralData,
    Transitions,
    VibrationalActivities,
    VibrationalData,
)
from ..glassware.spectra import SingleSpectrum, Spectra

# LOGGER
logger = lgg.getLogger(__name__)
logger.setLevel(lgg.DEBUG)


_WRITERS: Dict[str, Type["WriterBase"]] = {}


[docs]def writer( fmt: str, destination: Union[str, Path], mode: str = "x", **kwargs ) -> "WriterBase": """Factory function that returns concrete implementation of :class:`.WriterBase` subclass, most recently defined for export to *fmt* file format. Parameters ---------- fmt : str File format, to which export will be done. destination : Union[str, Path] Path to file or direcotry, to which export will be done. mode : str Specifies how writing to file should be handled. Should be one of characters: "a" (append to existing file), "x" (only write if file doesn't exist yet), or "w" (overwrite file if it already exists). Defaults to "x". kwargs Any additional keword arguments will be passed as-is to the constructor of the retrieved :class:`.WriterBase` subclass. Returns ------- WriterBase Initialized :class:`.WriterBase` subclass most recently defined for export to *fmt* file format. Raises ------ ValueError If :class:`.WriterBase` subclass for export to *fmt* file format was not defined. """ try: return _WRITERS[fmt](destination, mode, **kwargs) except KeyError: message = f"Unknown file format: {fmt}." if fmt.startswith(".") and fmt[1:] in _WRITERS: message += f" Did you mean '{fmt[1]}'?" raise ValueError(message)
_GenericArray = Union[DataArray, IntegerArray, FloatArray, BooleanArray, InfoArray] # CLASSES
[docs]class WriterBase(ABC): """Base class for writers that handle export process based on genre of exported data. Subclasses should provide an :attr:`.extension` class-level attribute and writting methods that subclass intend to support (see below). Value of :attr:`.extension` will be used to register subclass as a default writer for export to files that this value indicates ("txt", "csv", *etc.*). Not providing value for this attribute results in a ``TypeError`` exception. If subclass should not be registered, use an empty string as the attribute's value. :class:`.WriterBase` provides a :meth:`.write` method for writing arbitrary :class:`.DataArray`-like objects to disk. It dispatches those objects to appropriate writing methods, based on their type. Those writing methods are: | :meth:`.generic`, | :meth:`.overview`, | :meth:`.energies`, | :meth:`.single_spectrum`, | :meth:`.spectral_data`, | :meth:`.spectral_activities`, | :meth:`.spectra`, | :meth:`.transitions`, | :meth:`.geometry`. To learn more about implementing custom writers, see their documentation and :mod:`.writer_base` documentation or :ref:`extend` section. """ _header = dict( zpecorr="Zero-point Corr.", tencorr="Thermal Corr.", stoichiometry="Stoichiometry", entcorr="Enthalpy Corr.", last_read_geom="Last Read Geometry", optimized_geom="Optimized Geometry", input_geom="Input Geometry", optimization_completed="Optimized", command="Command", multiplicity="Multiplicity", transitions="Transitions", gibcorr="Gibbs Corr.", charge="Charge", normal_termination="Normal Termination", filenames="Filename", freq="Frequencies", mass="Red. masses", frc="Frc consts", ramanactiv="Raman Activ", depolarp="Depolar (P)", depolaru="Depolar (U)", ramact="RamAct", depp="Dep-P", depu="Dep-U", alpha2="Alpha2", beta2="Beta2", alphag="AlphaG", gamma2="Gamma2", delta2="Delta2", cid1="CID1", raman2="Raman2", roa2="ROA2", cid2="CID2", raman3="Raman3", roa3="ROA3", cid3="CID3", rc180="RC180", rot="Rot. Str.", dip="Dip. Str.", roa1="ROA1", raman1="Raman1", ex_en="Excit. Energy", wavelen="Wavelength", vrot="Rot.(velo)", lrot="Rot. (len)", vosc="Osc.(velo)", losc="Osc. (len)", vdip="Dip. (velo)", ldip="Dip. (length)", iri="IR Int.", emang="E-M Angle", eemang="E-M Angle", zpe="Zero-point", ten="Thermal", ent="Enthalpy", gib="Gibbs", scf="SCF", ) _formatters = dict( ir="{:> .4e}", vcd="{:> .4e}", uv="{:> .4e}", ecd="{:> .4e}", raman="{:> .4e}", roa="{:> .4e}", zpecorr="{:> 10.4f}", tencorr="{:> 10.4f}", entcorr="{:> 10.4f}", gibcorr="{:> 10.4f}", stoichiometry="{}", last_read_geom="{}", optimized_geom="{}", input_geom="{}", optimization_completed="{}", command="{}", multiplicity="{:^ 12d}", charge="{:^ 6d}", transitions="{}", normal_termination="{}", filenames="{}", rot="{:> 10.4f}", dip="{:> 10.4f}", roa1="{:> 10.4f}", raman1="{:> 10.4f}", vrot="{:> 10.4f}", lrot="{:> 10.4f}", vosc="{:> 10.4f}", losc="{:> 10.4f}", vdip="{:> 10.4f}", ldip="{:> 10.4f}", iri="{:> 10.4f}", emang="{:> 10.4f}", eemang="{:> 10.4f}", zpe="{:> 13.4f}", ten="{:> 13.4f}", ent="{:> 13.4f}", gib="{:> 13.4f}", scf="{:> 13.4f}", ex_en="{:> 13.4f}", freq="{:> 10.2f}", wavelen="{:> 10.2f}", mass="{:> 11.4f}", frc="{:> 10.4f}", depolarp="{:> 11.4f}", depolaru="{:> 11.4f}", ramanactiv="{:> 10.4f}", ramact="{:> 10.4f}", depp="{:> 9.4f}", depu="{:> 9.4f}", alpha2="{:> 9.4f}", beta2="{:> 9.4f}", alphag="{:> 9.4f}", gamma2="{:> 9.4f}", delta2="{:> 9.4f}", cid1="{:> 8.3f}", raman2="{:> 8.3f}", roa2="{:> 8.3f}", cid2="{:> 8.3f}", raman3="{:> 8.3f}", roa3="{:> 8.3f}", cid3="{:> 8.3f}", rc180="{:> 8.3f}", ) _excel_formats = dict( zpecorr="0.0000", tencorr="0.0000", entcorr="0.0000", gibcorr="0.0000", multiplicity="{0", charge="0", stoichiometry="", last_read_geom="", optimized_geom="", input_geom="", optimization_completed="", command="", transitions="", normal_termination="", filenames="", freq="0.0000", mass="0.0000", frc="0.0000", ramanactiv="0.0000", depolarp="0.0000", depolaru="0.0000", ramact="0.0000", depp="0.0000", depu="0.0000", alpha2="0.0000", beta2="0.0000", alphag="0.0000", gamma2="0.0000", delta2="0.0000", cid1="0.000", raman2="0.000", roa2="0.000", cid2="0.000", raman3="0.000", roa3="0.000", cid3="0.000", rc180="0.000", rot="0.0000", dip="0.0000", roa1="0.000", raman1="0.000", ex_en="0.0000", wavelen="0.0000", vrot="0.0000", lrot="0.0000", vosc="0.0000", losc="0.0000", vdip="0.0000", ldip="0.0000", iri="0.0000", emang="0.0000", eemang="0.0000", zpe="0.000000", ten="0.000000", ent="0.000000", gib="0.000000", scf="0.00000000", ) energies_order = "zpe ten ent gib scf".split(" ") """Default order, in which energy-related data is written to files.""" @property @classmethod @abstractmethod def extension(cls) -> Optional[str]: return "" extension.__doc__ = """ Identifier of this writer, indicating the format of files generated, and a default extension of those files used by the :meth:`.make_name` method. A falsy value, i.e. an empty string or ``None`` prevents this writer from being registered and used by :func:`.writer` factory function. Returns ------- str Default extension of files generated by this writer and it's identifier. """ @classmethod def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if cls.extension: _WRITERS[cls.extension] = cls def __init__(self, destination: Union[str, Path], mode: str = "x"): """ Parameters ---------- destination: str or pathlib.Path Directory, to which generated files should be written. mode: str Specifies how writing to file should be handled. Should be one of characters: 'a' (append to existing file), 'x' (only write if file doesn't exist yet), or 'w' (overwrite file if it already exists). """ self.mode = mode self.destination = destination self._handle = None @property def mode(self): """Specifies how writing to file should be handled. Should be one of characters: "a", "x", or "w". "a" - append to existing file; "x" - only write if file doesn't exist yet; "w" - overwrite file if it already exists. Raises ------ ValueError If given anything other than "a", "x", or "w". """ return self._mode @mode.setter def mode(self, mode): if mode not in ("a", "x", "w"): raise ValueError("Mode should be 'a', 'x', or 'w'.") self._mode = mode def check_file(self, file: Union[str, Path]) -> Path: file = Path(file) if not file.exists() and self.mode == "a": raise FileNotFoundError( "Mode 'a' was specified, but given file doesn't exist." ) elif file.exists() and self.mode == "x": raise FileExistsError( "Mode 'x' was specified, but given file already exists." ) elif not file.parent.exists(): raise FileNotFoundError("Parent directory of specified file doesn't exist.") else: logger.debug(f"File {file} ok for writing.") return file @property def destination(self) -> Path: """pathlib.Path: Directory, to which generated files should be written. Raises ------ FileNotFoundError If given destination doesn't exist or is not a directory. """ return vars(self)["destination"] @destination.setter def destination(self, destination: Union[str, Path]) -> None: destination = Path(destination) if not destination.is_dir(): raise FileNotFoundError( "Given destination doesn't exist or is not a directory." ) vars(self)["destination"] = destination
[docs] @staticmethod def distribute_data(data: List) -> Tuple[Dict[str, List], Dict[str, Any]]: """Sorts given data by genre category for use by specialized writing methods. Returns ------- distr : dict Dictionary with :class:`.DataArray`-like objects, sorted by their type. Each {key: value} pair is {name of the type in lowercase format: list of :class:`.DataArray` objects of this type}. extras : dict Spacial-case genres: extra information used by some writer methods when exporting data. Available {key: value} pairs (if given in *data*) are: | corrections: dict of {"energy genre": :class:`.FloatArray`}, | frequencies: :class:`.Bands`, | wavelengths: :class:`.Bands`, | excitation: :class:`.Bands`, | stoichiometry: :class:`.InfoArray`, | charge: :class:`.IntegerArray`, | multiplicity: :class:`.IntegerArray` """ distr: Dict[str, List] = dict() extras: Dict[str, Any] = dict() for obj in data: if obj.genre.endswith("corr"): corrs = extras["corrections"] = extras.get("corrections", dict()) corrs[obj.genre[:3]] = obj elif obj.genre == "freq": extras["frequencies"] = obj elif obj.genre == "wavelen": extras["wavelengths"] = obj elif obj.genre == "ex_en": extras["excitation"] = obj elif obj.genre == "stoichiometry": extras["stoichiometry"] = obj elif obj.genre == "charge": extras["charge"] = obj elif obj.genre == "multiplicity": extras["multiplicity"] = obj else: name = type(obj).__name__.lower() values = distr[name] = distr.get(name, list()) values.append(obj) return distr, extras
[docs] def make_name( self, template: Union[str, Template], conf: str = "", num: Union[str, int] = "", genre: str = "", cat: str = "", det: str = "", ext: str = "", ) -> str: """Create filename using given template and given or global values for known identifiers. The identifier should be used in the template as ``"${identifier}"`` where "identifier" is the name of identifier. Available names and their meaning are: | ``${ext}`` - appropriate file extension | ``${conf}`` - name of the conformer | ``${num}`` - number of the file according to internal counter | ``${genre}`` - genre of exported data | ``${cat}`` - category of produced output | ``${det}`` - category-specific detail The ``${ext}`` identifier is filled with the value of Writers :attr:`.extension` attribute if not explicitly given as parameter to this method's call. Values for other identifiers should be provided by the caller. Parameters ---------- template : str or string.Template Template that will be used to generate filenames. It should contain only known identifiers, listed above. conf : str value for ``${conf}`` identifier, defaults to empty string. num : str or int value for ``${str}`` identifier, defaults to empty string. genre : str value for ``${genre}`` identifier, defaults to empty string. cat : str value for ``${cat}`` identifier, defaults to empty string. det : str value for ``${det}`` identifier, defaults to empty string. ext : str value for ``${ext}`` identifier, defaults to empty string. Raises ------ ValueError If given template or string contains any unexpected identifiers. Examples -------- Must be first subclassed and instantiated: >>> class MyWriter(WriterBase): >>> extension = "foo" >>> wrt = MyWriter("/path/to/some/directory/") >>> wrt.make_name(template="somefile.${ext}") "somefile.foo" >>> wrt.make_name(template="${conf}.${ext}") ".foo" # conf is empty string by default >>> wrt.make_name(template="${conf}.${ext}", conf="conformer") "conformer.foo" >>> wrt.make_name(template="Unknown_identifier_${bla}.${ext}") Traceback (most recent call last): ValueError: Unexpected identifiers given: bla. """ if isinstance(template, str): template = Template(template) try: return template.substitute( conf=conf, ext=ext or self.extension, num=num, genre=genre, cat=cat, det=det, ) except KeyError as error: known = {"conf", "ext", "num", "genre", "cat", "det"} # second element of each tuple returned is identifier's name ids = {parsed[1] for parsed in Formatter().parse(template.template)} raise ValueError(f"Unexpected identifiers given: {ids-known}.") from error
[docs] @contextmanager def _get_handle( self, template: Union[str, Template], template_params: dict, open_params: Optional[dict] = None, ) -> Iterator[IO[AnyStr]]: """Helper method for creating files. Given additional kwargs will be passed to :meth:`Path.open` method. Implemented as context manager for use with ``with`` statement. Parameters ---------- template : str or string.Template Template that will be used to generate filenames. template_params : dict Dictionary of {identifier: value} for `.make_name` method. open_params : dict, optional Arguments for :meth:`Path.open` used to open file. Yields ------ IO file handle, will be closed automatically after ``with`` statement exits :meta public: """ open_params = open_params or {} # empty dict by default filename = self.make_name(template=template, **template_params) file = self.check_file(self.destination.joinpath(filename)) with file.open(self.mode, **open_params) as handle: self._handle = handle yield handle
[docs] def _iter_handles( self, filenames: Iterable[str], template: Union[str, Template], template_params: dict, open_params: Optional[dict] = None, ) -> Iterator[IO[AnyStr]]: """Helper method for iteration over generated files. Given additional kwargs will be passed to :meth:`Path.open` method. Parameters ---------- filenames: list of str list of source filenames, used as value for `${conf}` placeholder in *name_template* template_params : dict Dictionary of {identifier: value} for `.make_name` method. open_params : dict, optional arguments for :meth:`Path.open` used to open file. Yields ------ TextIO file handle, will be closed automatically on next iteration :meta public: """ open_params = open_params or {} # empty dict by default for num, fnm in enumerate(filenames): template_params.update({"conf": fnm, "num": num}) filename = self.make_name(template=template, **template_params) file = self.check_file(self.destination.joinpath(filename)) with file.open(self.mode, **open_params) as handle: yield handle
def _dataarray_handler(self, data: List[DataArray], extras: Dict[str, Any]) -> None: self.generic(data) def _integerarray_handler( self, data: List[IntegerArray], extras: Dict[str, Any] ) -> None: self.generic(data) def _floatarray_handler( self, data: List[FloatArray], extras: Dict[str, Any] ) -> None: self.generic(data) def _booleanarray_handler( self, data: List[BooleanArray], extras: Dict[str, Any] ) -> None: self.generic(data) def _infoarray_handler(self, data: List[InfoArray], extras: Dict[str, Any]) -> None: self.generic(data) def _energies_handler(self, data: List[Energies], extras: Dict[str, Any]) -> None: self.overview( data, frequencies=extras.get("frequencies"), stoichiometry=extras.get("stoichiometry"), ) for en in data: self.energies( en, corrections=extras.get("corrections", dict()).get(en.genre) ) def _vibrationalactivities_handler( self, data: List[VibrationalActivities], extras: Dict[str, Any] ) -> None: self.spectral_activities(band=extras["frequencies"], data=data) def _scatteringactivities_handler( self, data: List[ScatteringActivities], extras: Dict[str, Any] ) -> None: self.spectral_activities(band=extras["frequencies"], data=data) def _electronicactivities_handler( self, data: List[ElectronicActivities], extras: Dict[str, Any] ) -> None: self.spectral_activities(band=extras["wavelengths"], data=data) def _vibrationaldata_handler( self, data: List[VibrationalData], extras: Dict[str, Any] ) -> None: self.spectral_data(band=extras["frequencies"], data=data) def _scatteringdata_handler( self, data: List[ScatteringData], extras: Dict[str, Any] ) -> None: self.spectral_data(band=extras["frequencies"], data=data) def _electronicdata_handler( self, data: List[ElectronicData], extras: Dict[str, Any] ) -> None: self.spectral_data(band=extras["wavelengths"], data=data) def _transitions_handler( self, data: List[Transitions], extras: Dict[str, Any] ) -> None: if len(data) > 1: raise ValueError( "Got multiple *Transitions* objects, but can write contents " "of only one such object for .write() call." ) self.transitions(transitions=data[0], wavelengths=extras["wavelengths"]) def _geometry_handler(self, data: List[Geometry], extras: Dict[str, Any]) -> None: if len(data) > 1: raise ValueError( "Got multiple *Geometry* objects, but can write contents " "of only one such object for .write() call." ) self.geometry( data[0], charge=extras.get("charge"), multiplicity=extras.get("multiplicity"), ) def _spectra_handler(self, data: List[Spectra], _extras) -> None: for spc in data: self.spectra(spc) def _singlespectrum_handler(self, data: List[SingleSpectrum], _extras) -> None: for spc in data: self.single_spectrum(spc)
[docs] def write(self, data: List) -> None: """Writes :class:`.DataArray`-like objects to disk, decides how to write them based on the type of each object. If some types of given objects are not supported by this writer, data of this type is ignored and a warning is emitted. Parameters ---------- data : List :class:`.DataArray`-like objects that should be written to disk. """ distributed, extras = self.distribute_data(data) for name, data_ in distributed.items(): try: handler = getattr(self, f"_{name}_handler") handler(data_, extras) except (NotImplementedError, AttributeError): logger.warning(f"{type(self)} does not handle '{name}' type data.")
[docs] def generic( self, data: List[_GenericArray], name_template: Union[str, Template] = "", ): """Interface for writing generic data: any that provides one value for each conformer. Evoked when handling :class:`.DataArray`, :class:`.IntegerArray`, :class:`.FloatArray`, :class:`.BooleanArray`, or :class:`.InfoArray`. Parameters ---------- data List of objects that provide one value for each conformer. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def overview( self, energies: Sequence[Energies], frequencies: Optional[Bands] = None, stoichiometry: Optional[InfoArray] = None, name_template: Union[str, Template] = "", ): """Intercafe for generating an overview of known conformers: values of energies, number of imaginary frequencies, and stoichiometry for each conformer. Evoked when handling :class:`.Energies` objects. Parameters ---------- energies List of objects representing different energies genres for each conformer. Mandatory in custom implementation. frequencies :class:`.Bands` of "freq" genre, with list of frequencies for each conformer. Mandatory in custom implementation. May be ``None`` when method evoked by handler. stoichiometry Stoichiometry of each conformer. Mandatory in custom implementation. May be ``None`` when method evoked by handler. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def energies( self, energies: Energies, corrections: Optional[FloatArray] = None, name_template: Union[str, Template] = "", ): """Interface for writing energies values, and optionally their corrections. Evoked when handling :class:`.Energies` objects. Parameters ---------- energies Conformers' energies. Mandatory in custom implementation. corrections Correction of energies values. Mandatory in custom implementation. May be ``None`` when method evoked by handler. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def single_spectrum( self, spectrum: SingleSpectrum, name_template: Union[str, Template] = "" ): """Interface for writing a single spectrum to disk: calculated for one conformer or averaged. Evoked when handling :class:`.SingleSpectrum` objects. Parameters ---------- spectrum Single calculated spectrum. Mandatory in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def spectral_data( self, band: Bands, data: List[SpectralData], name_template: Union[str, Template] = "", ): """Interface for writing multiple objects with spectral data that is not a spectral activity (cannot be converted to signal intensity). Evoked when handling one of the: :class:`.VibrationalData`, :class:`.ElectronicData`, :class:`.ScatteringData` objects. Parameters ---------- band Band at which transitions occur for each conformer. Mandatory in custom implementation. data List of objects representing different spectral data genres (but not spectral activities). Mandatory in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def spectral_activities( self, band: Bands, data: List[SpectralActivities], name_template: Union[str, Template] = "", ): """Interface for writing multiple objects with spectral activities (data that may be converted to signal intensity). Evoked when handling one of the: :class:`.VibrationalActivities`, :class:`.ElectronicActivities`, :class:`.ScatteringActivities` objects. Parameters ---------- band Band at which transitions occur for each conformer. Mandatory in custom implementation. data List of objects representing different spectral activities genres. Mandatory in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def spectra(self, spectra: Spectra, name_template: Union[str, Template] = ""): """Interface for writing a set of spectra of one type calculated for many conformers. Evoked when handling :class:`.Spectra` objects. Parameters ---------- spectra Spectra of one type calculated for multiple conformers. Mandatory in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def transitions( self, transitions: Transitions, wavelengths: Bands, only_highest: bool = True, name_template: Union[str, Template] = "", ): """Interface for writing single object with electronic transitions data. Evoked when handling :class:`.Transitions` objects. Parameters ---------- transitions List of objects representing different spectral data genres (but not spectral_activities). Mandatory in custom implementation. wavelengths Wavelengths at which transitions occur for each conformer. Mandatory in custom implementation. only_highest Boolean flag indicating if all transitions should be written to disk or only these transition that contributes the most for each wavelength/ May be omitted in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")
[docs] def geometry( self, geometry: Geometry, charge: Optional[Union[IntegerArray, Sequence[int], int]] = None, multiplicity: Optional[Union[IntegerArray, Sequence[int], int]] = None, name_template: Union[str, Template] = "", ): """Interface for writing single object with geometry of each conformer. Evoked when handling :class:`.Geometry` objects. Parameters ---------- geometry Positions of atoms in each conformer. Mandatory in custom implementation. charge Value of each structure's charge. Mandatory in custom implementation. multiplicity Value of each structure's multiplicity. Mandatory in custom implementation. name_template Template that defines naming scheme for files generated by this method. May be omitted in custom implementation. Raises ------ NotImplementedError Whenever called, this is an interface that should not be used directly. """ raise NotImplementedError(f"Class {type(self)} does not implement this method.")