Source code for spglib.utils

"""Various base and utility definitions."""
# Copyright (C) 2015 Atsushi Togo
# This file is part of spglib.
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

import dataclasses
import typing
import warnings
from collections.abc import Mapping, Sequence
from typing import Union

import numpy as np

import spglib

from . import _spglib
from ._compat.typing import TypeAlias
from ._compat.warnings import deprecated
from .error import SpglibError, _set_no_error

if typing.TYPE_CHECKING:
    from collections.abc import Iterator
    from typing import Any

__all__ = [
    "Lattice",
    "Positions",
    "Numbers",
    "Magmoms",
    "Cell",
    "get_version",
    "spg_get_version",
    "spg_get_version_full",
    "spg_get_commit",
]

warnings.filterwarnings(
    "module", category=DeprecationWarning, message=r"dict interface.*"
)


Lattice: TypeAlias = Sequence[Sequence[float]]
"""Basis vectors in ``(3, 3)`` shape."""
Positions: TypeAlias = Sequence[Sequence[float]]
"""Point coordinates of atoms in ``(N, 3)`` shape."""
Numbers: TypeAlias = Sequence[int]
"""Integer numbers for identifying atoms in ``(N,)`` shape."""
Magmoms: TypeAlias = Union[Sequence[float], Sequence[Sequence[float]]]
"""Magnetic moments of atoms in either ``(N,)`` or ``(N, 3)`` shape."""

# Compatibility alias for some deprecated interfaces
Cell: TypeAlias = Union["spglib.spg.SpgCell", "spglib.msg.MsgCell"]
"""Either :py:type:`spglib.spg.SpgCell` or :py:type:`spglib.msg.MsgCell`."""


@dataclasses.dataclass(eq=True, frozen=True)
class DictInterface(Mapping[str, "Any"]):
    """Base class for dataclass with dict interface.

    .. versionadded:: 2.5.0
    .. deprecated:: 2.5.0
        Dict-like interface (``obj['field']``) are deprecated.
        Please use attribute interface instead (``obj.field``)
    """

    @deprecated("dict interface is deprecated. Use attribute interface instead")
    def __getitem__(self, key: str) -> Any:
        """Return the value of the key."""
        return dataclasses.asdict(self)[key]

    def __len__(self) -> int:
        """Return the number of fields."""
        return len(dataclasses.fields(self))

    def __iter__(self) -> Iterator[str]:
        """Return an iterator over the keys."""
        return iter(dataclasses.asdict(self))


[docs] @deprecated("Use __version__ or spg_get_version instead") def get_version() -> tuple[int, int, int]: """Return version number of spglib with tuple of three numbers. .. versionadded:: 1.8.3 .. deprecated:: 2.3.0 Use :py:func:`spg_get_version` and ``spglib.__version__`` instead """ _set_no_error() return _spglib.version_tuple()
[docs] def spg_get_version() -> str: """Get the X.Y.Z version of the detected spglib C library. .. versionadded:: 2.3.0 :return: version string """ _set_no_error() return _spglib.version_string()
[docs] def spg_get_version_full() -> str: """Get the full version of the detected spglib C library. .. versionadded:: 2.3.0 :return: full version string """ _set_no_error() return _spglib.version_full()
[docs] def spg_get_commit() -> str: """Get the commit of the detected spglib C library. .. versionadded:: 2.3.0 :return: commit string """ _set_no_error() return _spglib.commit()
def _expand_cell( cell: Cell, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None]: try: lattice = np.array(np.transpose(cell[0]), dtype="double", order="C") positions = np.array(cell[1], dtype="double", order="C") numbers = np.array(cell[2], dtype="intc") if len(cell) == 4: magmoms = np.array(cell[3], order="C", dtype="double") elif len(cell) == 3: magmoms = None else: raise TypeError("cell has to be a tuple of 3 or 4 elements.") # Sanity check if lattice.shape != (3, 3): raise TypeError("lattice has to be a (3, 3) array.") if not (positions.ndim == 2 and positions.shape[1] == 3): raise TypeError("positions has to be a (num_atoms, 3) array.") num_atoms = positions.shape[0] if numbers.ndim != 1: raise TypeError("numbers has to be a (num_atoms,) array.") if len(numbers) != num_atoms: raise TypeError( "numbers has to have the same number of atoms as positions." ) if magmoms is not None: if len(magmoms) != num_atoms: raise TypeError( "magmoms has to have the same number of atoms as positions." ) if magmoms.ndim == 1: # collinear pass elif magmoms.ndim == 2: # non-collinear if magmoms.shape[1] != 3: raise TypeError( "non-collinear magmoms has to be a (num_atoms, 3) array." ) else: raise TypeError("magmoms has to be a 1D or 2D array.") except Exception as exc: # Note: these will eventually be moved to the C++ side # For now we just recast them to SpglibError raise SpglibError(f"Generic Spglib error:\n{exc}") from exc return (lattice, positions, numbers, magmoms)