diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..fff2f4b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: "2" + +build: + os: "ubuntu-22.04" + tools: + python: "3.12" + +python: + install: + - requirements: docs/requirements.txt + +sphinx: + configuration: docs/source/conf.py diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 1391e26..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Required -version: 2 - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/source/conf.py - -# Optionally build your docs in additional formats such as PDF and ePub -formats: all - -# Optionally set the version of Python and requirements required to build your docs -python: - version: 3.8 - install: - - method: pip - path: . diff --git a/README.md b/README.md index 0dc48ce..32f2994 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ramannoodle ![Tests](reports/tests-badge.svg) ![Coverage](reports/coverage-badge.svg) - +[![Documentation Status](https://readthedocs.org/projects/ramannoodle/badge/?version=latest)](https://ramannoodle.readthedocs.io/en/latest/?badge=latest) `ramannoodle` helps you calculate Raman spectra quickly and efficiency based on VASP calculations. **This software is currently being completely overhauled.** Check out the `legacy` branch for the old (and functional!) version. diff --git a/docs/docs/requirements.txt b/docs/docs/requirements.txt new file mode 100644 index 0000000..b122e86 --- /dev/null +++ b/docs/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx==7.4.7 +sphinx-rtd-theme==2.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 49289a5..7aa48b4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,11 +5,11 @@ # -- Project information project = 'ramannoodle' -copyright = "2023, Willis O'Leary" +copyright = "2023-present Willis O'Leary" author = "Willis O'Leary" -release = '1.0' -version = '1.0' +release = 'alpha_0.1' +version = 'alpha_0.1' # -- General configuration @@ -20,13 +20,13 @@ 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', + 'sphinx_rtd_theme', ] intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'numpy': ('http://docs.scipy.org/doc/numpy', None), 'scipy': ('http://docs.scipy.org/doc/scipy/reference', None), - 'matplotlib': ('http://matplotlib.org/stable', None), } intersphinx_disabled_domains = ['std'] @@ -35,7 +35,7 @@ # -- Options for HTML output -# html_theme = 'sphinx_rtd_theme' +html_theme = 'sphinx_rtd_theme' # -- Options for EPUB output epub_show_urls = 'footnote' diff --git a/ramannoodle/io/vasp/__init__.py b/ramannoodle/io/vasp/__init__.py index 6a004c0..d90f2dd 100644 --- a/ramannoodle/io/vasp/__init__.py +++ b/ramannoodle/io/vasp/__init__.py @@ -19,7 +19,17 @@ def load_phonons_from_outcar(path: Path) -> Phonons: - """Extracts phonons from an OUTCAR""" + """Extracts phonons from a VASP OUTCAR file. + + Parameters + ---------- + path : Path + filepath + + Returns + ------- + Phonons + """ wavenumbers = [] eigenvectors = [] diff --git a/ramannoodle/polarizability/__init__.py b/ramannoodle/polarizability/__init__.py index 005b89b..886037f 100644 --- a/ramannoodle/polarizability/__init__.py +++ b/ramannoodle/polarizability/__init__.py @@ -9,6 +9,7 @@ from . import polarizability_utils from ..symmetry.symmetry_utils import is_orthogonal_to_all from ..symmetry import StructuralSymmetry +from ..exceptions import InvalidDOFException class PolarizabilityModel(ABC): # pylint: disable=too-few-public-methods @@ -61,7 +62,7 @@ def add_dof( # pylint: disable=too-many-locals # Check that the parent displacement is orthogonal to existing basis vectors result = is_orthogonal_to_all(parent_displacement, self._basis_vectors) if result != -1: - raise ValueError( + raise InvalidDOFException( f"new dof is not orthogonal with existing dof (index={result})" ) if len(magnitudes) == 0: @@ -99,13 +100,13 @@ def add_dof( # pylint: disable=too-many-locals # been provided duplicate = polarizability_utils.find_duplicates(interpolation_x) if duplicate is not None: - raise ValueError( + raise InvalidDOFException( f"due to symmetry, magnitude {duplicate} should not be specified" ) if len(interpolation_x) <= interpolation_dim: - raise ValueError( - f"insufficient data ({len(interpolation_x)}) available for" + raise InvalidDOFException( + f"insufficient magnitudes ({len(interpolation_x)}) available for" f"{interpolation_dim}-dimensional interpolation" ) assert len(interpolation_x) > 1 diff --git a/ramannoodle/spectrum/__init__.py b/ramannoodle/spectrum/__init__.py index bcc95b6..72fb659 100644 --- a/ramannoodle/spectrum/__init__.py +++ b/ramannoodle/spectrum/__init__.py @@ -22,7 +22,7 @@ def get_intensities(self) -> NDArray[np.float64]: return self._raw_intensities -class RamanSpectrum: # pylint: disable=too-few-public-methods +class RamanSpectrum: """Raman spectrum, with useful post-processing methods""" def __init__( diff --git a/ramannoodle/symmetry/__init__.py b/ramannoodle/symmetry/__init__.py index d0a6851..4903e5f 100644 --- a/ramannoodle/symmetry/__init__.py +++ b/ramannoodle/symmetry/__init__.py @@ -9,7 +9,7 @@ class StructuralSymmetry: - """Represents symmetries of a crystal structure.""" + """Symmetries of a crystal structure.""" def __init__( # pylint: disable=too-many-arguments self, diff --git a/test/tests/test_polarizability.py b/test/tests/test_polarizability.py index b523bd3..9111779 100644 --- a/test/tests/test_polarizability.py +++ b/test/tests/test_polarizability.py @@ -8,105 +8,11 @@ import pytest from ramannoodle.polarizability.polarizability_utils import find_duplicates -from ramannoodle.symmetry.symmetry_utils import ( - are_collinear, - is_orthogonal_to_all, - get_fractional_positions_permutation_matrix, -) from ramannoodle.polarizability import InterpolationPolarizabilityModel from ramannoodle.io.vasp import load_structural_symmetry_from_outcar +from ramannoodle.exceptions import InvalidDOFException - -@pytest.mark.parametrize( - "vector_1, vector_2, known", - [ - (np.array([-5.0, -5.0, 1.0]), np.array([1.0, 1.0, 0.0]), False), - (np.array([0.0, 0.0, -1.0]), np.array([1.0, 0.0, 0.0]), False), - (np.array([0.0, 0.0, 6.0]), np.array([0.0, 0.0, -3.0]), True), - (np.array([0.0, 0.0, -1.0]), np.array([1.0, 3.0, 1.0]), False), - ], -) -def test_are_collinear( - vector_1: NDArray[np.float64], vector_2: NDArray[np.float64], known: bool -) -> None: - """Test""" - assert are_collinear(vector_1, vector_2) == known - - -@pytest.mark.parametrize( - "vector_1, vectors, known", - [ - ( - np.array([1.0, 0.0, 0.0]), - np.array([[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, -1.0, 0.0]]), - 2, - ), - (np.array([1.0, 1.0, 0.0]), np.array([[0.0, 0.0, 1.0], [-1.0, 1.0, 0.0]]), -1), - ], -) -def test_check_orthogonal( - vector_1: NDArray[np.float64], vectors: list[NDArray[np.float64]], known: int -) -> None: - """Test""" - assert is_orthogonal_to_all(vector_1, vectors) == known - - -@pytest.mark.parametrize( - "outcar_path_fixture, known_nonequivalent_atoms," - "known_orthogonal_displacements, known_displacements_shape", - [ - ("test/data/TiO2_OUTCAR", 2, 36, [2] * 36), - ("test/data/STO_RATTLED_OUTCAR", 135, 1, [1]), - ("test/data/LLZO_OUTCAR", 9, 32, [1] * 32), - ], - indirect=["outcar_path_fixture"], -) -def test_structural_symmetry( - outcar_path_fixture: Path, - known_nonequivalent_atoms: int, - known_orthogonal_displacements: int, - known_displacements_shape: list[int], -) -> None: - """Test""" - - # Equivalent atoms test - symmetry = load_structural_symmetry_from_outcar(outcar_path_fixture) - assert symmetry.get_num_nonequivalent_atoms() == known_nonequivalent_atoms - - # Equivalent displacement test - displacement = ( - symmetry._fractional_positions * 0 # pylint: disable=protected-access - ) - displacement[0, 2] += 0.1 - print(displacement.shape) - displacements = symmetry.get_equivalent_displacements(displacement) - assert len(displacements) == known_orthogonal_displacements - assert [len(d["displacements"]) for d in displacements] == known_displacements_shape - - -@pytest.mark.parametrize( - "reference, permuted, known", - [ - ( - np.array( - [[0.2, 0.3, 0.4], [0.2, 0.8, 0.9], [0.0, 0.0, 1.0]], dtype=np.float64 - ), - np.array( - [[0.2, 0.3, 0.4], [0.0, 0.0, 1.0], [0.2, 0.8, 0.9]], dtype=np.float64 - ), - np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]], dtype=np.float64), - ) - ], -) -def test_get_fractional_positions_permutation_matrix( - reference: NDArray[np.float64], - permuted: NDArray[np.float64], - known: NDArray[np.float64], -) -> None: - """test""" - assert np.isclose( - get_fractional_positions_permutation_matrix(reference, permuted), known - ).all() +# pylint: disable=protected-access @pytest.mark.parametrize( @@ -122,7 +28,7 @@ def test_find_duplicates(vectors: list[NDArray[np.float64]], known: bool) -> Non @pytest.mark.parametrize( - "outcar_path_fixture,displaced_atom_index, magnitudes,known_dof_added", + "outcar_path_fixture,displaced_atom_index, magnitudes, known_dof_added", [ ("test/data/STO_RATTLED_OUTCAR", 0, np.array([-0.05, 0.05, 0.01, -0.01]), 1), ("test/data/TiO2_OUTCAR", 0, np.array([0.01]), 72), @@ -138,12 +44,34 @@ def test_add_dof( """test""" symmetry = load_structural_symmetry_from_outcar(outcar_path_fixture) model = InterpolationPolarizabilityModel(symmetry) - displacement = ( - symmetry._fractional_positions * 0 # pylint: disable=protected-access - ) + displacement = symmetry._fractional_positions * 0 displacement[displaced_atom_index][0] = 1.0 polarizabilities = np.zeros((len(magnitudes), 3, 3)) model.add_dof(displacement, magnitudes, polarizabilities, 1) - assert ( - len(model._basis_vectors) == known_dof_added # pylint: disable=protected-access - ) + assert len(model._basis_vectors) == known_dof_added + assert np.isclose(np.linalg.norm(model._basis_vectors[0]), 1) + + +@pytest.mark.parametrize( + "outcar_path_fixture,displaced_atom_index, magnitudes", + [ + ("test/data/STO_RATTLED_OUTCAR", 0, np.array([0.01, 0.01])), + ("test/data/TiO2_OUTCAR", 0, np.array([-0.01, 0.01])), + ], + indirect=["outcar_path_fixture"], +) +def test_overspecified_dof( + outcar_path_fixture: Path, + displaced_atom_index: int, + magnitudes: NDArray[np.float64], +) -> None: + """test""" + symmetry = load_structural_symmetry_from_outcar(outcar_path_fixture) + model = InterpolationPolarizabilityModel(symmetry) + displacement = symmetry._fractional_positions * 0 + displacement[displaced_atom_index][0] = 1.0 + polarizabilities = np.zeros((len(magnitudes), 3, 3)) + with pytest.raises(InvalidDOFException) as error: + model.add_dof(displacement, magnitudes, polarizabilities, 1) + + assert "should not be specified" in str(error.value) diff --git a/test/tests/test_symmetry.py b/test/tests/test_symmetry.py new file mode 100644 index 0000000..741e9bb --- /dev/null +++ b/test/tests/test_symmetry.py @@ -0,0 +1,107 @@ +"""Testing for symmetry-related routines.""" + +from pathlib import Path + +import numpy as np +from numpy.typing import NDArray + +import pytest + +from ramannoodle.symmetry.symmetry_utils import ( + are_collinear, + is_orthogonal_to_all, + get_fractional_positions_permutation_matrix, +) +from ramannoodle.io.vasp import load_structural_symmetry_from_outcar + + +@pytest.mark.parametrize( + "vector_1, vector_2, known", + [ + (np.array([-5.0, -5.0, 1.0]), np.array([1.0, 1.0, 0.0]), False), + (np.array([0.0, 0.0, -1.0]), np.array([1.0, 0.0, 0.0]), False), + (np.array([0.0, 0.0, 6.0]), np.array([0.0, 0.0, -3.0]), True), + (np.array([0.0, 0.0, -1.0]), np.array([1.0, 3.0, 1.0]), False), + ], +) +def test_are_collinear( + vector_1: NDArray[np.float64], vector_2: NDArray[np.float64], known: bool +) -> None: + """Test""" + assert are_collinear(vector_1, vector_2) == known + + +@pytest.mark.parametrize( + "vector_1, vectors, known", + [ + ( + np.array([1.0, 0.0, 0.0]), + np.array([[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, -1.0, 0.0]]), + 2, + ), + (np.array([1.0, 1.0, 0.0]), np.array([[0.0, 0.0, 1.0], [-1.0, 1.0, 0.0]]), -1), + ], +) +def test_check_orthogonal( + vector_1: NDArray[np.float64], vectors: list[NDArray[np.float64]], known: int +) -> None: + """Test""" + assert is_orthogonal_to_all(vector_1, vectors) == known + + +@pytest.mark.parametrize( + "outcar_path_fixture, known_nonequivalent_atoms," + "known_orthogonal_displacements, known_displacements_shape", + [ + ("test/data/TiO2_OUTCAR", 2, 36, [2] * 36), + ("test/data/STO_RATTLED_OUTCAR", 135, 1, [1]), + ("test/data/LLZO_OUTCAR", 9, 32, [1] * 32), + ], + indirect=["outcar_path_fixture"], +) +def test_structural_symmetry( + outcar_path_fixture: Path, + known_nonequivalent_atoms: int, + known_orthogonal_displacements: int, + known_displacements_shape: list[int], +) -> None: + """Test""" + + # Equivalent atoms test + symmetry = load_structural_symmetry_from_outcar(outcar_path_fixture) + assert symmetry.get_num_nonequivalent_atoms() == known_nonequivalent_atoms + + # Equivalent displacement test + displacement = ( + symmetry._fractional_positions * 0 # pylint: disable=protected-access + ) + displacement[0, 2] += 0.1 + print(displacement.shape) + displacements = symmetry.get_equivalent_displacements(displacement) + assert len(displacements) == known_orthogonal_displacements + assert [len(d["displacements"]) for d in displacements] == known_displacements_shape + + +@pytest.mark.parametrize( + "reference, permuted, known", + [ + ( + np.array( + [[0.2, 0.3, 0.4], [0.2, 0.8, 0.9], [0.0, 0.0, 1.0]], dtype=np.float64 + ), + np.array( + [[0.2, 0.3, 0.4], [0.0, 0.0, 1.0], [0.2, 0.8, 0.9]], dtype=np.float64 + ), + np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]], dtype=np.float64), + ) + ], +) +def test_get_fractional_positions_permutation_matrix( + reference: NDArray[np.float64], + permuted: NDArray[np.float64], + known: NDArray[np.float64], +) -> None: + """test""" + assert np.isclose( + get_fractional_positions_permutation_matrix(reference, permuted), known + ).all() diff --git a/test/tests/test_vasp.py b/test/tests/test_vasp.py index 994956e..021f93e 100644 --- a/test/tests/test_vasp.py +++ b/test/tests/test_vasp.py @@ -1,4 +1,4 @@ -"""Testing for the VASP utilities""" +"""Tests for VASP-related routines""" from pathlib import Path from typing import TextIO