Skip to content

Commit

Permalink
documentation pass
Browse files Browse the repository at this point in the history
  • Loading branch information
wolearyc committed Aug 1, 2024
1 parent 7533d3b commit 13fe5e6
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 80 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,26 @@
[![python](https://img.shields.io/badge/Python-3.12-3776AB.svg?style=flat&logo=python&logoColor=white)](https://www.python.org) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) ![Tests](docs/tests-badge.svg) ![Coverage](docs/coverage-badge.svg) [![Documentation Status](https://readthedocs.org/projects/ramannoodle/badge/?version=latest)](https://ramannoodle.readthedocs.io/en/latest/?badge=latest) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/mit)


**ramannoodle** helps you calculate Raman spectra from DFT calculations. **This software is currently being completely overhauled.** Check out the `legacy` branch for the old (and functional!) version.
**ramannoodle** helps you calculate Raman spectra from first-principles calculations.

> [!NOTE]
> **This software is currently being completely overhauled.** Check out the `legacy` branch for the old (and functional!) version.
ramannoodle is built from the ground up with the goals of being:

1. **EFFICIENT**

ramannoodle provides `PolarizabilityModel`'s to reduce the required number of first-principles polarizability calculations.

2. **FLEXIBLE**

ramannoodle provides a simple, object-oriented API that makes calculations a breeze while offering plenty of flexibility to carry out advanced analyses and add new functionality.

3. **TRANSPARENT**

ramannoodle is designed according to the philosophy that the user should understand *exactly* what is being calculated, without hidden corrections or assumptions.

Supported DFT Software
----------------------
* VASP (currently under development)
* phonopy (planned)
36 changes: 25 additions & 11 deletions ramannoodle/dynamics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,24 @@ class Phonons(Dynamics):
"""Harmonic lattice vibrations.
A phonon can be represented by a wavenumber and corresponding atomic displacement.
The wavenumbers of the eigenvalues of the system's dynamical matrix, while the
The wavenumbers are the eigenvalues of the system's dynamical matrix, while the
atomic displacements are the eigenvectors of the dynamical matrix divided by the
square root of the atomic masses.
Parameters
----------
wavenumbers
1D array with length M
cartesian_displacements
3D array with shape (M,N,3) where N is the number of atoms
"""

def __init__(
self,
wavenumbers: NDArray[np.float64],
cartesian_displacements: NDArray[np.float64],
) -> None:
"""Construct.
Parameters
----------
wavenumber: numpy.ndarray
"""
self._wavenumbers: NDArray[np.float64] = wavenumbers
self._cartesian_displacements: NDArray[np.float64] = cartesian_displacements

Expand All @@ -61,9 +62,22 @@ def get_raman_spectrum(
return PhononRamanSpectrum(self._wavenumbers, np.array(raman_tensors))

def get_wavenumbers(self) -> NDArray[np.float64]:
"""Return wavenumbers in cm-1."""
"""Return wavenumbers.
Returns
-------
:
1D array with length M
"""
return self._wavenumbers

def get_displacements(self) -> NDArray[np.float64]:
"""Return atomic displacements."""
def get_cartesian_displacements(self) -> NDArray[np.float64]:
"""Return cartesian displacements.
Returns
-------
:
3D array with shape (M,N,3) where M is the number of displacements
and N is the number of atoms
"""
return self._cartesian_displacements
2 changes: 1 addition & 1 deletion ramannoodle/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,4 @@
}

RAMAN_TENSOR_CENTRAL_DIFFERENCE = 0.001
BOLTZMANN_CONSTANT = 8.617333262e-5 # TODO: What are these units?
BOLTZMANN_CONSTANT = 8.617333262e-5 # Units: eV/K
35 changes: 11 additions & 24 deletions ramannoodle/io/vasp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@
from ..io_utils import _skip_file_until_line_contains


def load_phonons_from_outcar(path: Path) -> Phonons:
def load_phonons_from_outcar(filepath: Path) -> Phonons:
"""Extract phonons from a VASP OUTCAR file.
Parameters
----------
path : Path
filepath
filepath
Returns
-------
Phonons
:
"""
wavenumbers = []
eigenvectors = []

with open(path, "r", encoding="utf-8") as outcar_file:
with open(filepath, "r", encoding="utf-8") as outcar_file:

# get atom information
atomic_symbols = _read_atomic_symbols_from_outcar(outcar_file)
Expand Down Expand Up @@ -67,21 +66,20 @@ def load_phonons_from_outcar(path: Path) -> Phonons:
def load_positions_and_polarizability_from_outcar(
filepath: Path,
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
"""Extract atom position and polarizability from a VASP OUTCAR file.
"""Extract fractional positions and polarizability from a VASP OUTCAR file.
Technically, the extracted polarizability is, in fact, a dielectric tensor. However,
The polarizability returned by VASP is, in fact, a dielectric tensor. However,
this is inconsequential to the calculation of Raman spectra.
Parameters
----------
path : Path
filepath
filepath
Returns
-------
tuple[numpy.ndarray, numpy.ndarray]
The first element is atomic positions (Nx3) and the second element is the
polarizability tensor (3x3).
:
2-tuple, whose first element is the fractional positions, a 2D array with shape
(N,3). The second element is the polarizability, a 2D array with shape (3,3).
"""
with open(filepath, "r", encoding="utf-8") as outcar_file:
Expand All @@ -94,18 +92,7 @@ def load_positions_and_polarizability_from_outcar(
def load_structural_symmetry_from_outcar(
filepath: Path,
) -> StructuralSymmetry:
"""Extract structural symmetry from a VASP OUTCAR file.
Parameters
----------
path : Path
filepath
Returns
-------
StructuralSymmetry
"""
"""Extract structural symmetry from a VASP OUTCAR file."""
lattice = np.array([])
fractional_positions = np.array([])
atomic_numbers = np.array([], dtype=np.int32)
Expand Down
75 changes: 49 additions & 26 deletions ramannoodle/polarizability/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ class PolarizabilityModel(ABC): # pylint: disable=too-few-public-methods
def get_polarizability(
self, cartesian_displacement: NDArray[np.float64]
) -> NDArray[np.float64]:
"""Return an estimated polarizability for a given cartesian displacement."""
"""Return an estimated polarizability for a given cartesian displacement.
Parameters
----------
cartesian_displacement
2D array with shape (N,3) where N is the number of atoms
Returns
-------
:
2D array with shape (3,3)
"""


class InterpolationPolarizabilityModel(PolarizabilityModel):
Expand All @@ -28,31 +39,30 @@ class InterpolationPolarizabilityModel(PolarizabilityModel):
One is free to specify the interpolation order as well as the precise
form of the degrees of freedom, so long as they are orthogonal. For example, one can
employ first-order (linear) interpolation around phonon displacements to calculate
a conventional Raman spectrum. One can achieve identical results with fewer
calculations by using first-order interpolations around atomic displacements.
a conventional Raman spectrum. One can achieve identical results -- often with fewer
calculations -- by using first-order interpolations around atomic displacements.
This model's key assumption is that each degree of freedom in a system modulates
the polarizability **independently**.
Parameters
----------
structural_symmetry
equilibrium_polarizability
2D array with shape (3,3) giving polarizability of system at equilibrium. This
would usually correspond to the minimum energy structure.
Raman spectra calculated using this model do not explicitly depend on this
value. However, specifying the actual value is recommended in order to
compute the correct polarizability magnitudes.
"""

def __init__(
self,
structural_symmetry: StructuralSymmetry,
equilibrium_polarizability: NDArray[np.float64],
) -> None:
"""Construct model.
Parameters
----------
structural_symmetry: StructuralSymmetry
equilibrium_polarizability: numpy.ndarray[(3,3),dtype=numpy.float64]
Polarizability (3x3) of system at "equilibrium", i.e., minimized structure.
Raman spectra calculated using this model do not explicitly depend on this
value. However, specifying the actual value is recommended in order to
compute the correct polarizability magnitude.
"""
self._structural_symmetry = structural_symmetry
self._equilibrium_polarizability = equilibrium_polarizability
self._basis_vectors: list[NDArray[np.float64]] = []
Expand All @@ -61,7 +71,18 @@ def __init__(
def get_polarizability(
self, cartesian_displacement: NDArray[np.float64]
) -> NDArray[np.float64]:
"""Return an estimated polarizability for a given cartesian displacement."""
"""Return an estimated polarizability for a given cartesian displacement.
Parameters
----------
cartesian_displacement
2D array with shape (N,3) where N is the number of atoms
Returns
-------
:
2D array with shape (3,3)
"""
polarizability: NDArray[np.float64] = np.zeros((3, 3))
for basis_vector, interpolation in zip(
self._basis_vectors, self._interpolations
Expand All @@ -88,19 +109,21 @@ def add_dof( # pylint: disable=too-many-locals
displacement amplitudes and corresponding known polarizabilities for each
amplitude. Alongside the DOF specified, all DOFs related by the system's
symmetry will be added as well. The interpolation order can be specified,
though one must ensure that sufficient data available.
though one must ensure that sufficient data is available.
Parameters
----------
displacement: np.ndarray
Atomic displacement. Must be orthogonal to preexisting DOFs.
amplitudes: np.ndarray
Amplitudes in angstroms.
polarizabilities: NDArray[np.float64]
List of known polarizabilities corresponding to each amplitude.
interpolation_order: int
Interpolation order. Must be less than the number of symmetrically-
equivalent amplitudes.
displacement
2D array with shape (N,3) where N is the number of atoms. Units
are arbitrary.
amplitudes
1D array of length L containing amplitudes in angstroms.
polarizabilities
3D array with shape (L,3,3) containing known polarizabilities for
each amplitude.
interpolation_order
must be less than the number of total number of amplitudes after
symmetry considerations.
"""
parent_displacement = displacement / (np.linalg.norm(displacement) * 10)
Expand Down
20 changes: 17 additions & 3 deletions ramannoodle/spectrum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,27 @@ def measure( # pylint: disable=too-many-arguments
bose_einstein_correction: bool = False,
temperature: float | None = None,
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
"""Calculate and return a largely unprocessed Raman spectrum.
"""Calculate and return a raw Raman spectrum.
Parameters
----------
orientation: str | NDArray[np.float64]
orientation
Currently only "polycrystalline" is supported.
laser_correction
Applies laser-wavelength-dependent intensity correction. If True,
`laser_wavelength` must be specified.
laser_wavelength
bose_einstein_correction
Applies temperature-dependent Bose Einstein correction. If True,
`temperature` must be specified.
Returns
-------
:
2-tuple, whose first element is wavenumbers, a 1D array with length M
where M is the number of normal modes. The second element is intensities,
a 1D array with length M.
laser_correction:
"""
if orientation != "polycrystalline":
raise NotImplementedError(
Expand Down
38 changes: 29 additions & 9 deletions ramannoodle/symmetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,21 @@


class StructuralSymmetry:
"""Crystal structure symmetries."""
"""Crystal structure symmetries.
Parameters
----------
atomic_numbers
1D array of length N where N is the number of atoms.
lattice
Lattice vectors expressed as a 2D array with shape (3,3).
fractional_positions
2D array with shape (N,3) where N is the number of atoms
symprec
Symmetry precision parameter for spglib.
angle_tolerance
Symmetry precision parameter for spglib.
"""

def __init__( # pylint: disable=too-many-arguments
self,
Expand Down Expand Up @@ -48,17 +62,17 @@ def get_equivalent_displacements(
Parameters
----------
displacement : numpy.ndarray
Atomic displacement (Nx3)
displacement
2D array with shape (N,3) where N is the number of atoms.
Returns
-------
list[dict[str,list[numpy.ndarray]]]
List of dictionaries with 'displacement' and 'transformation' keys.
Displacements within each dictionary will be collinear, corresponding to
:
List of dictionaries containing displacements and transformations,
accessed using the 'displacements' and 'transformations' keys. Displacements
within each dictionary will be collinear, corresponding to
the same degree of freedom. The provided transformations are those that
transform the parameter displacements into that degree of freedom.
transform the parameter `displacements` into that degree of freedom.
"""
assert (displacement >= -0.5).all() and (displacement <= 0.5).all()
Expand Down Expand Up @@ -127,7 +141,13 @@ def get_equivalent_displacements(
def get_cartesian_displacement(
self, fractional_displacement: NDArray[np.float64]
) -> NDArray[np.float64]:
"""Convert a fractional displacement into cartesian coordinates."""
"""Convert a fractional displacement into cartesian coordinates.
Parameters
----------
fractional_displacement
2D array with shape (N,3) where N is the number of atoms
"""
assert (fractional_displacement >= -0.5).all() and (
fractional_displacement <= 0.5
).all()
Expand Down
Loading

0 comments on commit 13fe5e6

Please sign in to comment.