Source code for tesliper.writing.gjf_writer

"""Export of Gaussian input files (.gjf) for setting up new calculation step."""
import logging
from itertools import cycle
from pathlib import Path
from string import Template
from typing import Dict, Iterable, List, Optional, Sequence, TextIO, Union

from ..datawork.atoms import symbol_of_element
from ..glassware import Geometry, IntegerArray
from .writer_base import WriterBase

# LOGGER
logger = logging.getLogger(__name__)


# FUNCTIONS
def _format_coordinates(coords: Sequence[Sequence[float]], atoms: Sequence[int]):
    for a, (x, y, z) in zip(atoms, coords):
        a = symbol_of_element(a)
        yield f" {a: <2} {x: > 12.8f} {y: > 12.8f} {z: > 12.8f}"


# CLASSES
[docs]class GjfWriter(WriterBase): """Generates Gaussian input files for each conformer given.""" extension = "gjf" _link0_commands = { "Mem", # str specifying required memory "Chk", # str with file path "OldChk", # str with file path "SChk", # str with file path "RWF", # str with file path "OldMatrix", # str with file path "OldRawMatrix", # str with file path "Int", # str with spec "D2E", # str with spec "KJob", # str with link number and, optionally, space-separated number "Save", # boolean "ErrorSave", # boolean "NoSave", # boolean, same as ErrorSave "Subst", # str with link number and space-separated file path } _link0_commands = {k.lower(): k for k in _link0_commands} _parametrized = { "chk", "oldchk", "schk", "rwf", "oldmatrix", "oldrawmatrix", "subst", } empty_lines_at_end = 2 def __init__( self, destination: Union[str, Path], mode: str = "x", link0: Optional[Dict[str, Union[str, bool]]] = None, route: str = "", comment: str = "No information provided.", post_spec: str = "", ): """ Parameters ---------- destination: str or pathlib.Path Directory, to which generated files should be written. mode: str, optional 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". link0 : Dict[str, Union[str, bool]], optional Link0 commands that should be included in generated files, as a dictionary of {"command": "value"}. Refer to :attr:`link0` for more information. If omitted, no link0 commands are added. route : str Calculation directives for Gaussan, refer to the Gaussian documentation for information on how to construct the calculations route. comment : str, optional Additional text, describing the calculations, by default "No information provided." post_spec : str, optional Additional specification written after the molecule specification, written to generated files as provided by the user (you need to take care of line breaks). If omitted, no additional specification is added. """ super().__init__(destination=destination, mode=mode) self.link0 = link0 or {} self.route = route self.comment = comment self.post_spec = post_spec
[docs] def geometry( self, geometry: Geometry, charge: Union[IntegerArray, Sequence[int], int, None] = None, multiplicity: Union[IntegerArray, Sequence[int], int, None] = None, name_template: Union[str, Template] = "${conf}.${ext}", ): """Write given conformers' geometries to multiple Gaussian input files. Parameters ---------- geometry : Geometry :class:`.Geometry` object containing data for each confomer that should be exported as Gaussian input file. charge : Union[IntegerArray, Sequence[int], int, None], optional Molecule's charge for each conformer. May be a sequence of values or one value that will be repeated for each conformer. By default 0 for each. multiplicity : Union[IntegerArray, Sequence[int], int, None], optional Molecule's multiplicity for each conformer. May be a sequence of values or one value that will be repeated for each conformer. By default 1 for each. name_template : Union[str, Template], optional Template that will be used to generate filenames, by default "${conf}.${ext}". Refer to :meth:`.make_name` documentation for details on supported placeholders. """ geom = geometry.values atoms = cycle(geometry.atoms) try: char = charge.values except AttributeError: char = (0,) if charge is None else charge char = [char] if not isinstance(char, Iterable) else char char = cycle(char) try: mult = multiplicity.values except AttributeError: mult = (1,) if multiplicity is None else multiplicity mult = [mult] if not isinstance(mult, Iterable) else mult mult = cycle(mult) template_params = {"genre": geometry.genre, "cat": "geometry"} for *params, handle in zip( geom, atoms, char, mult, self._iter_handles(geometry.filenames, name_template, template_params), ): self._write_conformer(handle, *params, template_params)
def _write_conformer( self, file: TextIO, g: Sequence[Sequence[float]], a: Sequence[int], c: int, m: int, template_params: dict, ): for key, value in self.link0.items(): key = key.lower() if key in self._parametrized: # template_params are updated bt _iter_handles # so we can simply reuse it value = self.make_name(template=value, **template_params) if "save" in key and value: file.write(f"%{self._link0_commands[key]}\n") else: file.write(f"%{self._link0_commands[key]}={value}\n") file.write(self.route) file.write("\n" * 2) file.write(self.comment) file.write("\n" * 2) file.write(f"{c} {m}") for line in _format_coordinates(g, a): file.write("\n" + line) if self.post_spec: file.write("\n\n") file.write(self.post_spec) file.write("\n" * self.empty_lines_at_end) @property def link0(self) -> Dict[str, Union[str, bool]]: """ Link0 commands, in a form of ``{"command": "value"}``, that will be placed in the beginning of each Gaussian input file created. If anny *command* is an unknown keword, an exception will be raised. Accepted *command* keywords are as follows: :Mem: str specifying required memory :Chk: str with file path :OldChk: str with file path :SChk: str with file path :RWF: str with file path :OldMatrix: str with file path :OldRawMatrix: str with file path :Int: str with spec :D2E: str with spec :KJob: str with link number and, optionally, space-separated number :Save: boolean :ErrorSave: boolean :NoSave: boolean, same as ErrorSave :Subst: str with link number and space-separated file path Commands that provide a file path as a value may be parametrized for each conformer. You can put a placeholder inside a given string path, that will be parametrized when writing to file. See :meth:`.make_name` to see available placeholders. You may use any of values listed there, however ``${conf}`` and ``${num}`` will probably be the most useful. """ return self._link0 @link0.setter def link0(self, commands: Dict[str, Union[str, bool]]): unknown = {k for k in commands if k.lower() not in self._link0_commands} if unknown: raise ValueError(f"Unknown link 0 commands provided: {', '.join(unknown)}.") self._link0 = {k: v for k, v in commands.items() if v} @property def route(self) -> str: """Also known as *# lines*, specifies desired calculation type, model chemistry, and other options for Gaussian. If pound sign is missing, it is added in the beginning. For supported keywords and syntax refer to the Gaussian's documentation. """ return " ".join(self._route) @route.setter def route(self, commands: str): try: commands_ = commands.split() except AttributeError as error: raise TypeError("Expected object of type str.") from error length = len(commands_) if not length: commands_ = ["#"] else: first = commands_[0] if not first.startswith("#"): commands_ = ["#"] + commands_ self._route = commands_