"""MagneticSpaceGroup operations."""
# Copyright (C) 2015 Atsushi Togo
# This file is part of spglib.
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import dataclasses
from typing import Any
import numpy as np
from numpy.typing import ArrayLike, NDArray
from . import _spglib
from ._compat.typing import TypeAlias
from .error import _set_no_error, _set_or_throw_error
from .utils import DictInterface, Lattice, Magmoms, Numbers, Positions, _expand_cell
__all__ = [
"MsgCell",
"SpglibMagneticDataset",
"MagneticSpaceGroupType",
"get_magnetic_spacegroup_type",
"get_magnetic_spacegroup_type_from_symmetry",
"get_magnetic_symmetry",
"get_magnetic_symmetry_dataset",
"get_magnetic_symmetry_from_database",
]
MsgCell: TypeAlias = tuple[Lattice, Positions, Numbers, Magmoms]
"""
Magnetic crystal structure represented by a tuple of
(:py:type:`spglib.utils.Lattice`, :py:type:`spglib.utils.Positions`,
:py:type:`spglib.utils.Numbers`, :py:type:`spglib.utils.Magmoms`).
"""
[docs]
@dataclasses.dataclass(eq=False, frozen=True)
class SpglibMagneticDataset(DictInterface):
"""Spglib magnetic dataset information.
See :ref:`magnetic_spglib_dataset` in detail.
.. versionadded:: 2.5.0
"""
# Magnetic space-group type
uni_number: int
"""UNI number between 1 to 1651"""
msg_type: int
"""Magnetic space groups (MSG) is classified by its family space group (FSG) and
maximal space subgroup (XSG).
FSG is a non-magnetic space group obtained by ignoring time-reversal term in MSG.
XSG is a space group obtained by picking out non time-reversal operations in MSG.
- msg_type==1 (type-I):
MSG, XSG, FSG are all isomorphic.
- msg_type==2 (type-II):
XSG and FSG are isomorphic, and MSG is generated from XSG and pure time reversal
operations.
- msg_type==3 (type-III):
XSG is a proper subgroup of MSG with isomorphic translational subgroups.
- msg_type==4 (type-IV):
XSG is a proper subgroup of MSG with isomorphic point group.
"""
hall_number: int
"""For type-I, II, III, Hall number of FSG; for type-IV, that of XSG"""
tensor_rank: int
"""Rank of magmoms."""
# Magnetic symmetry operations
n_operations: int
"""Number of magnetic symmetry operations"""
rotations: NDArray[np.intc]
"""Rotation (matrix) parts of symmetry operations
shape: (n_operations, 3, 3)
"""
translations: NDArray[np.double]
"""Translation (vector) parts of symmetry operations
shape: (n_operations, 3)
"""
time_reversals: NDArray[np.intc]
"""Time reversal part of magnetic symmetry operations.
True indicates time reversal operation, and False indicates an ordinary operation.
shape: (n_operations, )
"""
# Equivalent atoms
n_atoms: int
"""Number of atoms in the input cell"""
equivalent_atoms: NDArray[np.intc]
"""Symmetrically equivalent atoms, where 'symmetrically' means found symmetry
operations.
"""
# Transformation to standardized setting
transformation_matrix: NDArray[np.intc]
"""Transformation matrix from input lattice to standardized
shape: (3, 3)
"""
origin_shift: NDArray[np.double]
"""Origin shift from standardized to input origin
shape: (3, )
"""
# Standardized crystal structure
n_std_atoms: int
"""Number of atoms in standardized unit cell"""
std_lattice: NDArray[np.double]
"""Row-wise lattice vectors of the standardized unit cell
shape: (3, 3)
"""
std_types: NDArray[np.intc]
"""Identity numbers of atoms in the standardized unit cell
shape: (n_std_atoms, )
"""
std_positions: NDArray[np.double]
"""Fractional coordinates of atoms in the standardized unit cell
shape: (n_std_atoms, 3)
"""
std_tensors: NDArray[np.double]
"""
shape:
(n_std_atoms, ) for collinear magnetic moments.
(n_std_atoms, 3) for vector non-collinear magnetic moments.
"""
std_rotation_matrix: NDArray[np.double]
"""Rigid rotation matrix to rotate from standardized basis vectors to idealized
standardized basis vectors"""
# Intermediate data in symmetry search
primitive_lattice: NDArray[np.double]
"""Basis vectors of primitive lattice.
shape: (3, 3)
"""
[docs]
@dataclasses.dataclass(eq=True, frozen=True)
class MagneticSpaceGroupType(DictInterface):
"""Magnetic space group type information.
.. versionadded:: 2.5.0
"""
uni_number: int
"""Serial number of UNI (or BNS) symbols"""
litvin_number: int
"""Serial number in Litvin's [Magnetic group tables](https://www.iucr.org/publ/978-0-9553602-2-0)"""
bns_number: str
"""BNS number e.g. '151.32'"""
og_number: str
"""OG number e.g. '153.4.1270'"""
number: int
"""ITA's serial number of space group for reference setting"""
type: int
"""Type of MSG from 1 to 4"""
[docs]
def get_magnetic_symmetry(
cell: MsgCell,
symprec: float = 1e-5,
angle_tolerance: float = -1.0,
mag_symprec: float = -1.0,
is_axial: bool | None = None,
with_time_reversal: bool = True,
_throw: bool = False,
) -> dict[str, Any] | None:
r"""Find magnetic symmetry operations from a crystal structure and site tensors.
Parameters
----------
cell : tuple
Crystal structure given either in tuple.
In the case given by a tuple, it has to follow the form below,
(basis vectors, atomic points, types in integer numbers, ...)
- basis vectors : array_like
shape=(3, 3), order='C', dtype='double'
.. code-block::
[[a_x, a_y, a_z],
[b_x, b_y, b_z],
[c_x, c_y, c_z]]
- atomic points : array_like
shape=(num_atom, 3), order='C', dtype='double'
Atomic position vectors with respect to basis vectors, i.e.,
given in fractional coordinates.
- types : array_like
shape=(num_atom, ), dtype='intc'
Integer numbers to distinguish species.
- magmoms:
case-I: Scalar
shape=(num_atom, ), dtype='double'
Each atomic site has a scalar value. With is_magnetic=True,
values are included in the symmetry search in a way of
collinear magnetic moments.
case-II: Vectors
shape=(num_atom, 3), order='C', dtype='double'
Each atomic site has a vector. With is_magnetic=True,
vectors are included in the symmetry search in a way of
non-collinear magnetic moments.
symprec : float
Symmetry search tolerance in the unit of length.
angle_tolerance : float
Symmetry search tolerance in the unit of angle deg.
Normally it is not recommended to use this argument.
See a bit more detail at :ref:`variables_angle_tolerance`.
If the value is negative, an internally optimized routine is used to judge
symmetry.
mag_symprec : float
Tolerance for magnetic symmetry search in the unit of magnetic moments.
If not specified, use the same value as symprec.
is_axial: None or bool
Set `is_axial=True` if `magmoms` does not change their sign by improper
rotations. If not specified, set `is_axial=False` when
`magmoms.shape==(num_atoms, )`, and set `is_axial=True` when
`magmoms.shape==(num_atoms, 3)`. These default settings correspond to
collinear and non-collinear spins.
with_time_reversal: bool
Set `with_time_reversal=True` if `magmoms` change their sign by time-reversal
operations. Default is True.
Returns
-------
symmetry: dict or None
Rotation parts and translation parts of symmetry operations represented
with respect to basis vectors and atom index mapping by symmetry
operations.
- 'rotations' : ndarray
shape=(num_operations, 3, 3), order='C', dtype='intc'
Rotation (matrix) parts of symmetry operations
- 'translations' : ndarray
shape=(num_operations, 3), dtype='double'
Translation (vector) parts of symmetry operations
- 'time_reversals': ndarray
shape=(num_operations, ), dtype='bool\_'
Time reversal part of magnetic symmetry operations.
True indicates time reversal operation, and False indicates
an ordinary operation.
- 'equivalent_atoms' : ndarray
shape=(num_atoms, ), dtype='intc'
- 'primitive_lattice': ndarray
shape=(3, 3), dtype='double'
Notes
-----
.. versionadded:: 2.0
"""
_set_no_error(_throw)
lattice, positions, numbers, magmoms = _expand_cell(cell)
if magmoms is None:
raise TypeError("Specify magnetic moments in cell.")
max_size = len(positions) * 96
rotations = np.zeros((max_size, 3, 3), dtype="intc", order="C")
translations = np.zeros((max_size, 3), dtype="double", order="C")
equivalent_atoms = np.zeros(len(magmoms), dtype="intc")
primitive_lattice = np.zeros((3, 3), dtype="double", order="C")
# (magmoms.ndim - 1) has to be equal to the rank of physical
# tensors, e.g., ndim=1 for collinear, ndim=2 for non-collinear.
if magmoms.ndim == 1 or magmoms.ndim == 2:
spin_flips = np.zeros(max_size, dtype="intc")
else:
spin_flips = None
# Infer is_axial value from tensor_rank to keep backward compatibility
if is_axial is None:
if magmoms.ndim == 1:
is_axial = False # Collinear spin
elif magmoms.ndim == 2:
is_axial = True # Non-collinear spin
try:
num_sym = _spglib.symmetry_with_site_tensors(
rotations,
translations,
equivalent_atoms,
primitive_lattice,
spin_flips,
lattice,
positions,
numbers,
magmoms,
int(with_time_reversal * 1),
int(is_axial * 1),
float(symprec),
float(angle_tolerance),
float(mag_symprec),
)
except Exception as exc:
_set_or_throw_error(exc, _throw)
return None
spin_flips = np.array(spin_flips[:num_sym], dtype="intc", order="C")
# True for time reversal operation, False for ordinary operation
time_reversals = spin_flips == -1
return {
"rotations": np.array(rotations[:num_sym], dtype="intc", order="C"),
"translations": np.array(translations[:num_sym], dtype="double", order="C"),
"time_reversals": time_reversals,
"equivalent_atoms": equivalent_atoms,
"primitive_lattice": np.array(
np.transpose(primitive_lattice),
dtype="double",
order="C",
),
}
[docs]
def get_magnetic_symmetry_dataset(
cell: MsgCell,
is_axial: bool | None = None,
symprec: float = 1e-5,
angle_tolerance: float = -1.0,
mag_symprec: float = -1.0,
) -> SpglibMagneticDataset | None:
"""Search magnetic symmetry dataset from an input cell. If it fails, return None.
Parameters
----------
cell, is_axial, symprec, angle_tolerance, mag_symprec:
See :func:`get_magnetic_symmetry`.
Returns
-------
dataset : :class:`SpglibMagneticDataset` or None
Notes
-----
.. versionadded:: 2.0
""" # noqa: E501
_set_no_error()
lattice, positions, numbers, magmoms = _expand_cell(cell)
tensor_rank = magmoms.ndim - 1
# If is_axial is not specified, select collinear or non-collinear spin cases
if is_axial is None:
if tensor_rank == 0:
is_axial = False # Collinear spin
elif tensor_rank == 1:
is_axial = True # Non-collinear spin
try:
spg_ds = _spglib.magnetic_dataset(
lattice,
positions,
numbers,
magmoms,
int(tensor_rank),
is_axial,
float(symprec),
float(angle_tolerance),
float(mag_symprec),
)
except Exception as exc:
_set_or_throw_error(exc)
return None
return SpglibMagneticDataset(**spg_ds)
[docs]
def get_magnetic_spacegroup_type(uni_number: int) -> MagneticSpaceGroupType | None:
"""Translate UNI number to magnetic space group type information.
If fails, return None.
Parameters
----------
uni_number : int
UNI number between 1 to 1651
Returns
-------
magnetic_spacegroup_type: :class:`MagneticSpaceGroupType` | None
Notes
-----
.. versionadded:: 2.0
"""
_set_no_error()
try:
msg_type = _spglib.magnetic_spacegroup_type(int(uni_number))
except Exception as exc:
_set_or_throw_error(exc)
return None
return MagneticSpaceGroupType(**msg_type)
[docs]
def get_magnetic_spacegroup_type_from_symmetry(
rotations: ArrayLike[np.intc],
translations: ArrayLike[np.double],
time_reversals: ArrayLike[np.intc],
lattice: ArrayLike[np.double] | None = None,
symprec: float = 1e-5,
) -> MagneticSpaceGroupType | None:
"""Return magnetic space-group type information from symmetry operations.
Parameters
----------
rotations, translations, time_reversals:
See returns of :func:`get_magnetic_symmetry`.
lattice : (Optional) array_like (3, 3)
Basis vectors a, b, c given in row vectors. This is used as the measure of
distance. Default is None, which gives unit matrix.
symprec: float
See :func:`get_symmetry`.
Returns
-------
magnetic_spacegroup_type: :class:`MagneticSpaceGroupType` | None
"""
rots = np.array(rotations, dtype="intc", order="C")
trans = np.array(translations, dtype="double", order="C")
timerev = np.array(time_reversals, dtype="intc", order="C")
if lattice is None:
latt = np.eye(3, dtype="double", order="C")
else:
latt = np.array(lattice, dtype="double", order="C")
_set_no_error()
try:
msg_type = _spglib.magnetic_spacegroup_type_from_symmetry(
rots, trans, timerev, latt, float(symprec)
)
except Exception as exc:
_set_or_throw_error(exc)
return None
return MagneticSpaceGroupType(**msg_type)
[docs]
def get_magnetic_symmetry_from_database(
uni_number: int, hall_number: int = 0
) -> dict[str, Any] | None:
"""Return magnetic symmetry operations from UNI number between 1 and 1651.
If fails, return None.
Optionally alternative settings can be specified with Hall number.
Parameters
----------
uni_number : int
UNI number between 1 and 1651.
hall_number : int, optional
The Hall symbol is given by the serial number in between 1 and 530.
Returns
-------
symmetry : dict
- 'rotations'
- 'translations'
- 'time_reversals'
0 and 1 indicate ordinary and anti-time-reversal operations, respectively.
Notes
-----
.. versionadded:: 2.0
"""
_set_no_error()
rotations = np.zeros((384, 3, 3), dtype="intc")
translations = np.zeros((384, 3), dtype="double")
time_reversals = np.zeros(384, dtype="intc")
try:
num_sym = _spglib.magnetic_symmetry_from_database(
rotations,
translations,
time_reversals,
int(uni_number),
int(hall_number),
)
except Exception as exc:
_set_or_throw_error(exc)
return None
return {
"rotations": np.array(rotations[:num_sym], dtype="intc", order="C"),
"translations": np.array(translations[:num_sym], dtype="double", order="C"),
"time_reversals": np.array(
time_reversals[:num_sym],
dtype="intc",
order="C",
),
}