diff --git a/README.md b/README.md index 6e925da..3cc8a53 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/ramannoodle/dynamics/__init__.py b/ramannoodle/dynamics/__init__.py index d584227..29cb239 100644 --- a/ramannoodle/dynamics/__init__.py +++ b/ramannoodle/dynamics/__init__.py @@ -24,9 +24,17 @@ 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__( @@ -34,13 +42,6 @@ def __init__( 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 @@ -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 diff --git a/ramannoodle/globals.py b/ramannoodle/globals.py index 3ca14fb..85efc93 100644 --- a/ramannoodle/globals.py +++ b/ramannoodle/globals.py @@ -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 diff --git a/ramannoodle/io/vasp/__init__.py b/ramannoodle/io/vasp/__init__.py index 2993165..c373dc6 100644 --- a/ramannoodle/io/vasp/__init__.py +++ b/ramannoodle/io/vasp/__init__.py @@ -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) @@ -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: @@ -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) diff --git a/ramannoodle/polarizability/__init__.py b/ramannoodle/polarizability/__init__.py index 9697ec7..6734493 100644 --- a/ramannoodle/polarizability/__init__.py +++ b/ramannoodle/polarizability/__init__.py @@ -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): @@ -28,12 +39,23 @@ 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__( @@ -41,18 +63,6 @@ def __init__( 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]] = [] @@ -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 @@ -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) diff --git a/ramannoodle/spectrum/__init__.py b/ramannoodle/spectrum/__init__.py index e31453e..42b56ff 100644 --- a/ramannoodle/spectrum/__init__.py +++ b/ramannoodle/spectrum/__init__.py @@ -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( diff --git a/ramannoodle/symmetry/__init__.py b/ramannoodle/symmetry/__init__.py index 089a0b3..ab2183e 100644 --- a/ramannoodle/symmetry/__init__.py +++ b/ramannoodle/symmetry/__init__.py @@ -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, @@ -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() @@ -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() diff --git a/test/tests/test_vasp.py b/test/tests/test_vasp.py index 69ee4c7..fc61907 100644 --- a/test/tests/test_vasp.py +++ b/test/tests/test_vasp.py @@ -45,15 +45,17 @@ def test_load_phonons_from_outcar( known_degrees_of_freedom = known_num_atoms * 3 assert phonons.get_wavenumbers().shape == (known_degrees_of_freedom,) assert np.isclose(phonons.get_wavenumbers()[0:4], known_wavenumbers).all() - assert phonons.get_displacements().shape == ( + assert phonons.get_cartesian_displacements().shape == ( known_degrees_of_freedom, known_num_atoms, 3, ) - assert np.isclose(phonons.get_displacements()[0, 0], known_first_displacement).all() - print(phonons.get_displacements()[-1, -1]) assert np.isclose( - phonons.get_displacements()[-1, -1], known_last_displacement + phonons.get_cartesian_displacements()[0, 0], known_first_displacement + ).all() + print(phonons.get_cartesian_displacements()[-1, -1]) + assert np.isclose( + phonons.get_cartesian_displacements()[-1, -1], known_last_displacement ).all() diff --git a/tests-badge.svg b/tests-badge.svg deleted file mode 100644 index 26a42b1..0000000 --- a/tests-badge.svg +++ /dev/null @@ -1 +0,0 @@ -tests: 26tests26