Skip to content

Commit

Permalink
improved argument handling
Browse files Browse the repository at this point in the history
  • Loading branch information
wolearyc committed Aug 4, 2024
1 parent e5f2ab6 commit 55c823e
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,4 @@ $RECYCLE.BIN/
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows,python,visualstudiocode
session
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"argmax",
"AVOGADROS",
"dtype",
"EATF",
"fft",
"fftfreq",
"fftpack",
Expand Down
56 changes: 56 additions & 0 deletions ramannoodle/globals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Defines some useful globals."""

from typing import Sequence

from numpy.typing import NDArray

ATOMIC_WEIGHTS = {
"H": 1.008,
"He": 4.002602,
Expand Down Expand Up @@ -244,3 +248,55 @@

RAMAN_TENSOR_CENTRAL_DIFFERENCE = 0.001
BOLTZMANN_CONSTANT = 8.617333262e-5 # Units: eV/K


def _shape_string(shape: Sequence[int | None]) -> str:
"""Get a string representing a shape.
Maps None --> "_", indicating that this element can
be anything.
"""
result = "("
for i in shape:
if i is None:
result += "_,"
else:
result += f"{i},"
return result[:-1] + ")"


def verify_ndarray(name: str, array: NDArray) -> None:
"""Verify type of NDArray .
We should avoid calling this function wherever possible (EATF)
"""
try:
_ = array.shape
except AttributeError as exc:
wrong_type = type(array).__name__
raise TypeError(f"{name} should be an ndarray, not a {wrong_type}") from exc


def verify_ndarray_shape(
name: str, array: NDArray, shape: Sequence[int | None]
) -> None:
"""Verify an NDArray's shape.
We should avoid calling this function whenever possible (EATF).
Parameters
----------
shape
int elements will be checked, None elements will not be.
"""
try:
if len(shape) != array.ndim:
shape_spec = f"{_shape_string(array.shape)} != {_shape_string(shape)}"
raise ValueError(f"{name} has wrong shape: {shape_spec}")
for d1, d2 in zip(array.shape, shape, strict=True):
if d2 is not None and d1 != d2:
shape_spec = f"{_shape_string(array.shape)} != {_shape_string(shape)}"
raise ValueError(f"{name} has wrong shape: {shape_spec}")
except AttributeError as exc:
wrong_type = type(array).__name__
raise TypeError(f"{name} should be an ndarray, not a {wrong_type}") from exc
13 changes: 11 additions & 2 deletions ramannoodle/io/io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,14 @@ def pathify(filepath: str | Path) -> Path:
def pathify_as_list(filepaths: str | Path | list[str] | list[Path]) -> list[Path]:
"""Convert filepaths to list of Paths."""
if isinstance(filepaths, list):
return [Path(item) for item in filepaths]
return [Path(filepaths)]
paths = []
for item in filepaths:
try:
paths.append(Path(item))
except TypeError as exc:
raise TypeError(f"{item} cannot be resolved as a filepath") from exc
return paths
try:
return [Path(filepaths)]
except TypeError as exc:
raise TypeError(f"{filepaths} cannot be resolved as a filepath") from exc
3 changes: 3 additions & 0 deletions ramannoodle/io/vasp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def read_phonons_from_outcar(filepath: str | Path) -> Phonons:
Raises
------
FileNotFoundError
InvalidFileException
If the OUTCAR has an unexpected format.
"""
Expand Down Expand Up @@ -96,6 +97,7 @@ def read_positions_and_polarizability_from_outcar(
Raises
------
FileNotFoundError
InvalidFileException
If the OUTCAR has an unexpected format.
"""
Expand Down Expand Up @@ -126,6 +128,7 @@ def read_structural_symmetry_from_outcar(
If the OUTCAR has an unexpected format.
SymmetryException
If OUTCAR was read but the symmetry search failed
FileNotFoundError
"""
lattice = np.array([])
fractional_positions = np.array([])
Expand Down
105 changes: 79 additions & 26 deletions ramannoodle/polarizability/interpolation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from ... import io
from ...io.io_utils import pathify_as_list
from ...globals import verify_ndarray_shape


def get_amplitude(
Expand All @@ -34,7 +35,7 @@ class InterpolationPolarizabilityModel(PolarizabilityModel):
"""Polarizability model based on interpolation around degrees of freedom.
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
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 -- often with fewer
calculations -- by using first-order interpolations around atomic displacements.
Expand All @@ -49,13 +50,21 @@ class InterpolationPolarizabilityModel(PolarizabilityModel):
2D array with shape (3,3) giving polarizability of system at equilibrium. This
would usually correspond to the minimum energy structure.
Raises
------
ValueError
TypeError
"""

def __init__(
self,
structural_symmetry: StructuralSymmetry,
equilibrium_polarizability: NDArray[np.float64],
) -> None:
verify_ndarray_shape(
"equilibrium_polarizability", equilibrium_polarizability, (3, 3)
)
self._structural_symmetry = structural_symmetry
self._equilibrium_polarizability = equilibrium_polarizability
self._cartesian_basis_vectors: list[NDArray[np.float64]] = []
Expand All @@ -75,12 +84,27 @@ def get_polarizability(
-------
:
2D array with shape (3,3)
Raises
------
TypeError
ValueError
"""
delta_polarizability: NDArray[np.float64] = np.zeros((3, 3))
for basis_vector, interpolation in zip(
self._cartesian_basis_vectors, self._interpolations
):
amplitude = np.dot(basis_vector.flatten(), cartesian_displacement.flatten())
try:
amplitude = np.dot(
basis_vector.flatten(), cartesian_displacement.flatten()
)
except AttributeError as exc:
raise TypeError("cartesian_displacement is not an ndarray") from exc
except ValueError as exc:
raise ValueError(
"cartesian_displacement has incompatible length "
f"({len(cartesian_displacement)}!={len(basis_vector)})"
) from exc
delta_polarizability += interpolation(amplitude)

return delta_polarizability + self._equilibrium_polarizability
Expand Down Expand Up @@ -114,12 +138,25 @@ def add_dof( # pylint: disable=too-many-locals
must be less than the number of total number of amplitudes after
symmetry considerations.
Raises
------
InvalidDOFException
ValueError
TypeError
"""
parent_displacement = displacement / (np.linalg.norm(displacement) * 10)
try:
parent_displacement = displacement / (np.linalg.norm(displacement) * 10)
except TypeError as exc:
raise TypeError("displacement is not an ndarray") from exc
verify_ndarray_shape("amplitudes", amplitudes, (None,))
verify_ndarray_shape(
"polarizabilities", polarizabilities, (len(amplitudes), 3, 3)
)

parent_cartesian_basis_vector = (
self._structural_symmetry.get_cartesian_displacement(parent_displacement)
)

# Check that the parent displacement is orthogonal to existing basis vectors
result = is_orthogonal_to_all(
parent_cartesian_basis_vector, self._cartesian_basis_vectors
Expand All @@ -128,13 +165,6 @@ def add_dof( # pylint: disable=too-many-locals
raise InvalidDOFException(
f"new dof is not orthogonal with existing dof (index={result})"
)
if len(amplitudes) == 0:
raise ValueError("no amplitudes provided")
if len(amplitudes) != len(polarizabilities):
raise ValueError(
f"unequal numbers of amplitudes ({len(amplitudes)})) and "
f"polarizabilities ({len(polarizabilities)})"
)

displacements_and_transformations = (
self._structural_symmetry.get_equivalent_displacements(parent_displacement)
Expand Down Expand Up @@ -165,7 +195,7 @@ def add_dof( # pylint: disable=too-many-locals
interpolation_y.append(
(np.linalg.inv(rotation) @ delta_polarizability @ rotation)
)

interpolation_x = np.array(interpolation_x)
# If duplicate amplitudes are generated, too much data has
# been provided
duplicate = polarizability_utils.find_duplicates(interpolation_x)
Expand All @@ -174,27 +204,39 @@ def add_dof( # pylint: disable=too-many-locals
f"due to symmetry, amplitude {duplicate} should not be specified"
)

if len(interpolation_x) - 1 <= interpolation_order:
if len(interpolation_x) <= interpolation_order:
raise InvalidDOFException(
f"insufficient amplitudes ({len(interpolation_x)}) available for"
f"insufficient points ({len(interpolation_x)}) available for "
f"{interpolation_order}-order interpolation"
)
assert len(interpolation_x) > 1

child_cartesian_basis_vector = (
self._structural_symmetry.get_cartesian_displacement(child_displacement)
)
child_cartesian_basis_vector /= np.linalg.norm(child_cartesian_basis_vector)
basis_vectors_to_add.append(child_cartesian_basis_vector)

sort_indices = np.argsort(interpolation_x)
interpolations_to_add.append(
make_interp_spline(
x=np.array(interpolation_x)[sort_indices],
y=np.array(interpolation_y)[sort_indices],
k=interpolation_order,
bc_type=None,
try:
interpolations_to_add.append(
make_interp_spline(
x=np.array(interpolation_x)[sort_indices],
y=np.array(interpolation_y)[sort_indices],
k=interpolation_order,
bc_type=None,
)
)
)
except ValueError as exc:
if "non-negative k" in str(exc):
raise ValueError(
f"invalid interpolation_order: {interpolation_order} < 1"
) from exc
raise exc
except TypeError as exc:
raise TypeError(
"interpolation_order should be int, not "
f"{type(interpolation_order).__name__}"
) from exc

self._cartesian_basis_vectors += basis_vectors_to_add
self._interpolations += interpolations_to_add
Expand All @@ -215,6 +257,14 @@ def add_dof_from_files(
----------
filepaths
file_format
Raises
------
TypeError
FileNotFoundError
InvalidDOFException
ValueError
"""
# Extract displacements, polarizabilities, and basis vector
displacements = []
Expand All @@ -224,10 +274,13 @@ def add_dof_from_files(
fractional_positions, polarizability = io.read_positions_and_polarizability(
filepath, file_format
)
displacement = calculate_displacement(
fractional_positions,
self._structural_symmetry.get_fractional_positions(),
)
try:
displacement = calculate_displacement(
fractional_positions,
self._structural_symmetry.get_fractional_positions(),
)
except ValueError as exc:
raise InvalidDOFException(f"incompatible outcar: {filepath}") from exc
displacements.append(displacement)
polarizabilities.append(polarizability)
result = is_collinear_with_all(displacements[0], displacements)
Expand Down
21 changes: 15 additions & 6 deletions ramannoodle/polarizability/polarizability_utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
"""Useful utilities (largely linalg for calculating polarizability model)."""

import itertools
from typing import Iterable

import numpy as np
from numpy.typing import NDArray
from numpy.typing import NDArray, ArrayLike


def find_duplicates(vectors: list[NDArray[np.float64]]) -> NDArray[np.float64] | None:
def find_duplicates(vectors: Iterable[ArrayLike]) -> NDArray | None:
"""Return duplicate vector in a list or None if no duplicates found."""
for vector_1, vector_2 in itertools.combinations(vectors, 2):
if np.isclose(vector_1, vector_2):
return vector_1
return None
try:
combinations = itertools.combinations(vectors, 2)
except TypeError as exc:
wrong_type = type(vectors).__name__
raise TypeError(f"vectors should be iterable, not {wrong_type}") from exc
try:
for vector_1, vector_2 in combinations:
if np.isclose(vector_1, vector_2).all():
return np.array(vector_1)
return None
except TypeError as exc:
raise TypeError("elements of vectors are not array_like") from exc
Loading

0 comments on commit 55c823e

Please sign in to comment.