"""Store the results of a Raman calculation.
This module defines the :class:`~autojob.tasks.raman.Raman`,
:class:`~autojob.tasks.raman.RamanInputs`, and
:class:`~autojob.tasks.raman.RamanOutputs` classes. Instances
of these classes represent the results of a Raman calculation, its inputs,
and its outputs, respectively.
For building the respective documents from a folder, the
:class:`~autojob.tasks.raman.RamanInputs` and
:class:`~autojob.tasks.raman.RamanOutputs` classes are
:class:`PathLoadable`.
Examples:
Create a Raman calculation
.. code-block:: python
from ase.build import add_adsorbate, fcc100, molecule
from autojob.tasks.task import TaskInputs
from autojob.tasks.raman import Raman
slab = fcc100("Cu")
co = molecule("CO")
# All atoms with this tag will NOT be frozen
tag = -99
co.set_tags([tag] * len(co))
add_adsorbate(slab, co, 1.8)
# Freeze the atoms that should not be displaced during the IR
# analysis
freeze_atoms(slab, [tag])
# Create the infrared calculation
tinputs = TaskInputs(atoms=slab)
raman = Raman(task_inputs=tinputs)
Load the results of Raman calculation
.. code-block:: python
from autojob.tasks.raman import Raman
src = "path/to/calculation/directory"
results = Raman.from_directory(src)
"""
import json
import logging
from pathlib import Path
import sys
from typing import Any
from typing import ClassVar
from typing import Self
if sys.version_info < (3, 12):
pass
else:
pass
import ase.io
from ase.vibrations.placzek import PlaczekStatic as Raman_ASE
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from autojob import SETTINGS
from autojob.tasks.vibration import Vibration
logger = logging.getLogger(__name__)
def _construct_raman_data(
dir_name: Path, name: str, intensities_file: str
) -> None:
atoms = ase.io.read(Path(dir_name, SETTINGS.OUTPUT_ATOMS_FILE))
atoms = atoms[-1] if isinstance(atoms, list) else atoms
cache = Path(dir_name, name)
# ASE names forces caches cache-.iXN.json where i is an atom index,
# X is direction (x/y/z) and N is number of steps
nfree = 4 if list(cache.glob("cache.*--.json")) else 2
raman = Raman_ASE(atoms, name=cache, nfree=nfree)
raman_energies = raman.get_energies().real
raman_intensities = raman.get_absolute_intensities()
raman_intensities = raman_intensities[raman_energies.argsort()]
raman_energies.sort()
raman_json = Path(dir_name, intensities_file)
data = {
"raman_energies": raman_energies.tolist(),
"raman_intensities": raman_intensities.tolist(),
}
with raman_json.open(mode="w", encoding="utf-8") as file:
_ = json.dump(data, file, indent=4)
[docs]
class RamanOutputs(BaseModel):
"""The outputs of an infrared calculation."""
raman_energies: list[float] | None = Field(
default=None,
description="The Raman spectrum frequencies in sqrt(amu) * Angstrom.",
)
raman_intensities: list[float] | None = Field(
default=None,
description="The absolute intensities of the Raman spectrum.",
)
model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True)
[docs]
@classmethod
def from_directory(
cls,
src: str | Path,
*,
name: str = "ir",
intensities_file: str = "intensities.json",
strict_mode: bool | None = None,
) -> "RamanOutputs":
"""Extract Raman outputs from a task directory.
Args:
src: The directory of the Raman calculation.
name: The name used to write vibration calculation results. This
should either be the name of the directory containing the cache
files or the filename stem for a JSON containing the output of
a vibration calculation such as that produced by
:meth:`~VibrationData.write`.
intensities_file: The name of the JSON file used to write Raman
intensities.
strict_mode: Whether or not to require all outputs. If True,
errors will be thrown on missing outputs.
Returns:
An instance of :class:`.RamanOutputs`.
"""
if strict_mode is None:
strict_mode = SETTINGS.STRICT_MODE
logger.info("Loading Raman outputs from directory: %s", src)
logger.debug("Strict mode: %sabled", "en" if strict_mode else "dis")
raman_outputs = {}
try:
logger.info("Successfully Raman outputs from directory: %s", src)
raman_file = Path(src, f"{intensities_file}")
if not raman_file.exists():
_construct_raman_data(src, name, intensities_file)
with raman_file.open(mode="r", encoding="utf-8") as file:
raman_outputs = json.load(file)
except (FileNotFoundError, KeyError, RuntimeError, TypeError):
logger.exception(
"Unable to load Raman outputs from directory: %s", src
)
if strict_mode:
raise
return cls(**raman_outputs)
[docs]
class Raman(Vibration):
"""A record representing a Raman calculation."""
raman_inputs: RamanInputs = Field(
default_factory=RamanInputs,
description="The inputs of the Raman calculation",
)
raman_outputs: RamanOutputs | None = Field(
default=None, description="The Raman calculation outputs"
)
[docs]
@classmethod
def from_directory(cls, src: str | Path, **kwargs) -> Self:
"""Generate a ``Raman`` document from a calculation directory.
Args:
src: The directory of a calculation.
kwargs: Additional keyword arguments:
- strict_mode : Whether or not to fail on any error. Defaults to
`SETTINGS.STRICT_MODE`.
- magic_mode : Whether or not to instantiate subclasses. If
True, the task returned must be an instance determined by
metadata in the directory. Defaults to False.
Returns:
A :class:`Raman` or a subclass of a :class:`Raman`.
.. seealso::
:meth:`.vibration.Vibration.from_directory`
"""
strict_mode = kwargs.get("strict_mode", SETTINGS.STRICT_MODE)
magic_mode = kwargs.get("magic_mode", False)
logger.debug("Loading Raman calculation from directory: %s", src)
logger.debug("Magic mode: %sabled", "en" if magic_mode else "dis")
logger.debug("Strict mode: %sabled", "en" if strict_mode else "dis")
if magic_mode:
return cls.load_magic(src, strict_mode=strict_mode)
vibration = Vibration.from_directory(
src=src, strict_mode=strict_mode, magic_mode=False
)
data = vibration.task_inputs.model_extra.pop("raman_inputs", {})
raman_inputs = RamanInputs(**data)
raman_outputs = RamanOutputs.from_directory(
src=src,
name=vibration.vibration_inputs.name,
intensities_file=raman_inputs.intensities_file,
strict_mode=strict_mode,
)
logger.debug(
"Successfully loaded Raman calculation from directory: %s", src
)
return cls(
**vibration.model_dump(),
raman_inputs=raman_inputs,
raman_outputs=raman_outputs,
)